diff --git a/config/sys.config b/config/sys.config new file mode 100644 index 0000000..eb8cc99 --- /dev/null +++ b/config/sys.config @@ -0,0 +1,17 @@ +[ + {rolnik, [ + {lower_limit, 5}, + {upper_limit, 30}, + {temp_check_freq, 1000}, + {checks_per_sample, 60}, + {normal_status_return_limit, 0.5}, + {smtp_config, [ + {relay, {192,168,0,111}}, + {ssl, true}, + {username, "login@mail.com"}, + {password, "mailpassword"} + ]}, + {alarm_sender_email, "login@mail.com"}, + {alarm_receiver_email, "receiveremail@mail.com"} + ]} +]. \ No newline at end of file diff --git a/config/test.config b/config/test.config new file mode 100644 index 0000000..d90b8f3 --- /dev/null +++ b/config/test.config @@ -0,0 +1,28 @@ +[ + {rolnik, [ + {lower_limit, 5}, + {upper_limit, 30}, + {temp_check_freq, 1000}, + {checks_per_sample, 60}, + {normal_status_return_limit, 0.5}, + {smtp_config, [ + {relay, {192,168,0,111}}, + {ssl, true}, + {username, "login@mail.com"}, + {password, "mailpassword"} + ]}, + {alarm_sender_email, "login@mail.com"}, + {alarm_receiver_email, "receiveremail@mail.com"} + ]}, + {grisp, [ + {drivers, [ + {spi, grisp_spi_drv_emu}, + {gpio, grisp_gpio_drv_emu}, + {i2c, grisp_i2c_drv_emu} + ]}, + {devices, [ + {spi1, pmod_nav}, + {spi2, pmod_gyro} + ]} + ]} +]. diff --git a/grisp/grisp_base/files/erl_inetrc b/grisp/grisp_base/files/erl_inetrc new file mode 100644 index 0000000..758f85a --- /dev/null +++ b/grisp/grisp_base/files/erl_inetrc @@ -0,0 +1,11 @@ +% Add hosts +{host, {192,168,0,32}, ["yourpc-name"]}. + +% Do not monitor the hosts file +{hosts_file, ""}. + +% Disable caching +{cache_size, 0}. + +% Specify lookup method +{lookup, [file, native]}. \ No newline at end of file diff --git a/grisp/grisp_base/files/grisp.ini.mustache b/grisp/grisp_base/files/grisp.ini.mustache new file mode 100644 index 0000000..30794e6 --- /dev/null +++ b/grisp/grisp_base/files/grisp.ini.mustache @@ -0,0 +1,11 @@ +[boot] +image_path = /media/mmcsd-0-0/{{release_name}}/erts-{{erts_vsn}}/bin/beam.bin + +[erlang] +args = erl.rtems -- -home . -pa . -root {{release_name}} -boot {{release_name}}/releases/{{release_version}}/{{release_name}} -internal_epmd epmd_sup -kernel inetrc "./erl_inetrc" -sname {{release_name}} -setcookie test -config {{release_name}}/releases/{{release_version}}/sys.config + +[network] +ip_self = dhcp +wlan = enable +hostname = grisp +wpa = wpa_supplicant.conf diff --git a/grisp/grisp_base/files/wpa_supplicant.conf b/grisp/grisp_base/files/wpa_supplicant.conf new file mode 100644 index 0000000..be1e2b0 --- /dev/null +++ b/grisp/grisp_base/files/wpa_supplicant.conf @@ -0,0 +1,5 @@ +network={ + ssid="network_name" + key_mgmt=WPA-PSK + psk="password" +} diff --git a/rebar.config b/rebar.config index 72b5df4..7e552ca 100644 --- a/rebar.config +++ b/rebar.config @@ -1,4 +1,8 @@ -{deps, [grisp]}. +{deps, [ + {grisp, {git, "git://github.com/grisp/grisp.git"}}, + {epmd, {git, "https://github.com/erlang/epmd", {ref, "4d1a59"}}}, + {gen_smtp, ".*", {git, "https://github.com/Vagabond/gen_smtp.git", "master"}} +]}. {erl_opts, [debug_info]}. @@ -7,15 +11,30 @@ {version, "20.2"} ]}, {deploy, [ - {destination, "/tmp/sd_card"} + {pre_script, "rm -rf /run/media/seb/GRISP/*"}, + {destination, "/run/media/seb/GRISP"}, + {post_script, "umount /run/media/seb/GRISP"} ]}, {toolchain, [ - {root, "/Users/michalslaski/dev/GRiSP/grisp-software/rtems-install/rtems-4.12"} + {root, "/home/seb/GRiSP/grisp-software/rtems-install/rtems-4.12"} ]} ]}. {shell, [{apps, []}]}. {relx, [ - {release, {rolnik, "0.1.0"}, [rolnik]} + {release, + {rolnik, "0.1.0"}, + [{epmd, none}, gen_smtp, rolnik], + [{sys_config, "./config/sys.config"}]} +]}. + + +{profiles, [ + {test, [ + {shell, [{apps, [rolnik]}, + {config, "config/test.config"} + ]} + ]} ]}. + diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..165a292 --- /dev/null +++ b/rebar.lock @@ -0,0 +1,18 @@ +{"1.1.0", +[{<<"epmd">>, + {git,"https://github.com/erlang/epmd", + {ref,"4d1a595b9d5c32fc0e55f462da381de62de23bf0"}}, + 0}, + {<<"gen_smtp">>, + {git,"https://github.com/Vagabond/gen_smtp.git", + {ref,"e07022695735fe3b3eaf0fca52d85e91eef2a67f"}}, + 0}, + {<<"grisp">>, + {git,"git://github.com/grisp/grisp.git", + {ref,"23860074abb8de6c77cee5a11e1a5b2685060260"}}, + 0}, + {<<"mapz">>,{pkg,<<"mapz">>,<<"0.3.0">>},1}]}. +[ +{pkg_hash,[ + {<<"mapz">>, <<"438D24746CE5A252101E00B2032EFDF7FC69EB32689D3B805DE5E6DD7F52614F">>}]} +]. diff --git a/src/rolnik.app.src b/src/rolnik.app.src index a2dcb26..914fe18 100644 --- a/src/rolnik.app.src +++ b/src/rolnik.app.src @@ -6,6 +6,7 @@ {applications, [ kernel, stdlib, + gen_smtp, grisp ]}, {env,[]}, diff --git a/src/rolnik_notifier.erl b/src/rolnik_notifier.erl new file mode 100644 index 0000000..5f18797 --- /dev/null +++ b/src/rolnik_notifier.erl @@ -0,0 +1,66 @@ +%% @doc +%% @end +-module(rolnik_notifier). + +-behaviour(gen_server). + +%% API +-export([start_link/1, alarm/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3]). + +-record(state, {socket_info, sender_email, receiver_email}). + +%% API + +%% @doc Starts rolnik_notifier which handles sending alarm messages to client +%% when temperature status changes from normal +-spec start_link(Args :: list()) -> {ok, pid()} | ignore | {error, term()}. +start_link(Args)-> + gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []). + +%% @doc Starts rolnik_notifier which handles sending alarm messages to client +-spec alarm(Status :: atom()) -> ok. +alarm(Status) -> + gen_server:call(?MODULE, {alarm, Status}). + +%% gen_server callbacks + +init([SmtpConfig, Sender, Receiver]) -> + process_flag(trap_exit, true), + SocketInfo = open_socket_for_notify(SmtpConfig), + {ok, #state{socket_info = SocketInfo, sender_email = Sender, + receiver_email = Receiver}}. + +handle_call({alarm, below}, _From, State) -> + Message = "Subject: Temperature Warning\r\nFrom: GRISP board \r\n + Temperature dropped below the lower limit.", + send(Message, State), + {reply, ok, State}; +handle_call({alarm, above}, _From, State) -> + Message = "Subject: Temperature Warning\r\nFrom: GRISP board \r\n + Temperature rised above the upper limit.", + send(Message, State), + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% Internal functions + +open_socket_for_notify(SmtpConfig) -> + {ok, SocketInfo} = gen_smtp_client:open(SmtpConfig), + SocketInfo. + +send(Message, #state{socket_info = SocketInfo, sender_email = Sender, + receiver_email = Receiver}) -> + % Info backup to implement + {ok, _Info} = gen_smtp_client:deliver(SocketInfo, {Sender, [Receiver], + Message}). diff --git a/src/rolnik_sup.erl b/src/rolnik_sup.erl index fdb126b..7c66936 100644 --- a/src/rolnik_sup.erl +++ b/src/rolnik_sup.erl @@ -11,9 +11,59 @@ -export([init/1]). %--- API ----------------------------------------------------------------------- - -start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). +%% @doc Starts rolnik top level supervisor. +-spec start_link() -> {ok, pid()} | ignore | {error, term()}. +start_link() -> + {ok, Pid} = supervisor:start_link({local, ?MODULE}, ?MODULE, []), + %% start event handlers + {ok, LowerLimit} = application:get_env(rolnik, lower_limit), + {ok, UpperLimit} = application:get_env(rolnik, upper_limit), + {ok, ChecksPerSample} = application:get_env(rolnik, checks_per_sample), + {ok, StatusReturnLimit} = application:get_env(rolnik, normal_status_return_limit), + ok = gen_event:add_handler(rolnik_temp_notify_manager, + rolnik_temp_limit_handler, + [LowerLimit, UpperLimit, ChecksPerSample, + StatusReturnLimit]), + ok = gen_event:add_handler(rolnik_temp_notify_manager, + rolnik_temp_backup_handler, []), + {ok, Pid}. %--- Callbacks ----------------------------------------------------------------- -init([]) -> {ok, { {one_for_all, 0, 1}, []} }. +init([]) -> + {ok, SmtpConfig} = application:get_env(rolnik, smtp_config), + {ok, Sender} = application:get_env(rolnik, alarm_sender_email), + {ok, Receiver} = application:get_env(rolnik, alarm_receiver_email), + {ok, TempCheckFreq} = application:get_env(rolnik, temp_check_freq), + SensorId = get_sensor_id(), + SupFlags = #{strategy => one_for_one, intensity => 1, period => 5}, + ChildSpecs = [ + %% start rolnik_notifier + #{id => rolnik_notifier_id, + start => {rolnik_notifier, start_link, [[SmtpConfig, + Sender, Receiver]]}, + restart => permanent, + shutdown => brutal_kill, + type => worker, + modules => [rolnik_notifier]}, + %% start event manager + {rolnik_temp_notify_manager, {gen_event, start_link, + [{local, rolnik_temp_notify_manager}]}, + permanent, 5000, worker, [dynamic]}, + %% start rolnik_temp_checker + #{id => rolnik_temp_checker_id, + start => {rolnik_temp_checker, start_link, [[TempCheckFreq, + SensorId]]}, + restart => permanent, + shutdown => brutal_kill, + type => worker, + modules => [rolnik_temp_checker]} + ], + {ok, {SupFlags, ChildSpecs}}. + +%% Internal functions + +get_sensor_id() -> + [ID] = grisp_onewire:transaction(fun() -> grisp_onewire:search() end), + ID. + diff --git a/src/rolnik_temp_backup_handler.erl b/src/rolnik_temp_backup_handler.erl new file mode 100644 index 0000000..339eb42 --- /dev/null +++ b/src/rolnik_temp_backup_handler.erl @@ -0,0 +1,29 @@ +-module(rolnik_temp_backup_handler). +-behaviour(gen_event). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, + terminate/2]). + +-record(state, {temps = []}). + +init([]) -> + {ok, #state{}}. + +handle_event(TimeTempData, #state{temps = TempsBackup}) -> + UpdatedTempsBackup = [TimeTempData | TempsBackup], + NewState = #state{temps = UpdatedTempsBackup}, + {ok, NewState}. + +handle_call(_, State) -> + {ok, ok, State}. + +handle_info(_, State) -> + {ok, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. + +%% Internal functions diff --git a/src/rolnik_temp_checker.erl b/src/rolnik_temp_checker.erl new file mode 100644 index 0000000..7a1e862 --- /dev/null +++ b/src/rolnik_temp_checker.erl @@ -0,0 +1,69 @@ +%% @doc +%% @end +-module(rolnik_temp_checker). + +-behaviour(gen_server). + +%% API +-export([start_link/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {temp_check_freq, sensor_id}). + +%% API + +%% @doc Starts rolnik_temp_checker which notifies rolnik_temp_notify_manager +%% about temperature with given frequency +-spec start_link(Args :: list()) -> {ok, pid()} | ignore | {error, term()}. +start_link(Args) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []). + +%% gen_server callbacks + +init([TempCheckFreq, SensorId]) -> + process_flag(trap_exit, true), + %% Request first update after giving time for whole app to start + ?MODULE ! temp_update, + ok = led_start_notify(), + {ok, #state{temp_check_freq = TempCheckFreq, sensor_id = SensorId}}. + +handle_call(_Msg, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(temp_update, State = #state{temp_check_freq = Lag, sensor_id = Id}) -> + ok = request_temp_update_after_lag(Lag), + {ok, NewTemp} = get_temp(Id), + ok = temp_notify(NewTemp), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% Internal functions +led_start_notify() -> + grisp_led:color(1, aqua), + ok. + +request_temp_update_after_lag(Lag) -> + erlang:send_after(Lag, ?MODULE, temp_update), + ok. + +get_temp(Id) -> + ok = onewire_ds18b20:convert(Id, 500), + Temp = onewire_ds18b20:temp(Id), + {ok, Temp}. + +temp_notify(Temp) -> + Time = erlang:localtime(), + TimeTempData = {Time, Temp}, + gen_event:notify(rolnik_temp_notify_manager, TimeTempData), + ok. diff --git a/src/rolnik_temp_limit_handler.erl b/src/rolnik_temp_limit_handler.erl new file mode 100644 index 0000000..d33b6a2 --- /dev/null +++ b/src/rolnik_temp_limit_handler.erl @@ -0,0 +1,86 @@ +-module(rolnik_temp_limit_handler). +-behaviour(gen_event). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, + terminate/2]). + +-record(state, {current_sample_temps = [], current_temp_status = normal, + upper_limit, lower_limit, checks_per_sample, + status_return_limit}). + +init([LowerLimit, UpperLimit, ChecksPerSample, StatusReturnLimit]) -> + {ok, #state{lower_limit = LowerLimit, upper_limit = UpperLimit, + checks_per_sample = ChecksPerSample, + status_return_limit = StatusReturnLimit}}. + +handle_event({_Time, Temp}, State = #state{current_temp_status = CurrStatus, + current_sample_temps = Temps, + checks_per_sample = ChecksPerSample}) + when length(Temps) == (ChecksPerSample - 1) -> + UpdatedTemps = [Temp | Temps], + NewTempStatus = new_temp_status(State, UpdatedTemps), + case make_decision(NewTempStatus, CurrStatus) of + do_nothing -> ok; + alarm -> send(NewTempStatus) + end, + NewState = State#state{current_sample_temps = [], + current_temp_status = NewTempStatus}, + {ok, NewState}; +handle_event({_Time, Temp}, State = #state{current_sample_temps = Temps}) -> + UpdatedTemps = [Temp | Temps], + NewState = State#state{current_sample_temps = UpdatedTemps}, + {ok, NewState}. + +handle_call(_, State) -> + {ok, ok, State}. + +handle_info(_, State) -> + {ok, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, _State) -> + ok. + +%% Internal functions + +new_temp_status(#state{current_temp_status = CurrStatus, + lower_limit = LowerLimit, upper_limit = UpperLimit, + status_return_limit = StatusReturnLimit}, Temps) -> + AvrgTemp = average(Temps), + verify_average_temp(CurrStatus, AvrgTemp, LowerLimit, UpperLimit, + StatusReturnLimit). + +average(List) -> + lists:sum(List) / length(List). + +verify_average_temp(normal, Temp, LowerLimit, _UpperLimit, _StatusReturnLimit) + when Temp < LowerLimit -> + below; +verify_average_temp(normal, Temp, _LowerLimit, UpperLimit, _StatusReturnLimit) + when Temp > UpperLimit -> + above; +verify_average_temp(below, Temp, LowerLimit, _UpperLimit, StatusReturnLimit) + when Temp > (LowerLimit + StatusReturnLimit) -> + normal; +verify_average_temp(above, Temp, _LowerLimit, UpperLimit, StatusReturnLimit) + when Temp < (UpperLimit - StatusReturnLimit) -> + normal; +verify_average_temp(below, _Temp, _LowerLimit, _UpperLimit, _StatusReturnLimit) -> + below; +verify_average_temp(above, _Temp, _LowerLimit, _UpperLimit, _StatusReturnLimit) -> + above; +verify_average_temp(normal, _Temp, _LowerLimit, _UpperLimit, _StatusReturnLimit) -> + normal. + + +make_decision(below, normal) -> + alarm; +make_decision(above, normal) -> + alarm; +make_decision(_NewStatus, _CurrStatus) -> + do_nothing. + +send(Status) -> + ok = rolnik_notifier:alarm(Status).