From 39347388543fff39be77edfde42f699d55f06060 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 19 Jan 2024 17:37:19 +0100 Subject: [PATCH] Add gen_statem to automatically connect to grisp.io on boot --- src/grisp_io_connection.erl | 137 ++++++++++++++++++++++++++++++++++ src/grisp_seawater.app.src | 3 +- src/grisp_seawater_client.erl | 2 + src/grisp_seawater_sup.erl | 3 +- 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/grisp_io_connection.erl diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl new file mode 100644 index 0000000..732590e --- /dev/null +++ b/src/grisp_io_connection.erl @@ -0,0 +1,137 @@ +%% @doc State machine to ensure connectivity with grisp.io +-module(grisp_io_connection). + +% API +-export([start_link/0]). +-export([connect/0]). + +-behaviour(gen_statem). +-export([init/1, terminate/3, code_change/4, callback_mode/0, handle_event/4]). + +-include_lib("kernel/include/logger.hrl"). + +-define(STD_TIMEOUT, 1000). + +% API + +start_link() -> + gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). + +connect() -> + gen_statem:cast(?MODULE, ?FUNCTION_NAME). + +% gen_statem CALLBACKS --------------------------------------------------------- + +init([]) -> + {ok, Connect} = application:get_env(grisp_seawater, connect), + NextState = case Connect of + true -> waiting_ip; + false -> idle + end, + {ok, NextState, []}. + +terminate(_Reason, _State, _Data) -> ok. + +code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. + +callback_mode() -> [handle_event_function, state_enter]. + +%%% STATE CALLBACKS ------------------------------------------------------------ + +% IDLE +handle_event(enter, _OldState, idle, _Data) -> + keep_state_and_data; +handle_event(cast, connect, idle, Data) -> + {next_state, waiting_ip, Data}; + +% WAITING_IP +handle_event(enter, _OldState, waiting_ip, Data) -> + {next_state, waiting_ip, Data, [{state_timeout, 0, retry}]}; +handle_event(state_timeout, retry, waiting_ip, Data) -> + case check_inet_ipv4() of + {ok, IP} -> + ?LOG_INFO("Detected IP: ~p", [IP]), + {next_state, connecting, Data}; + invalid -> + ?LOG_INFO("Waiting IP..."), + {next_state, waiting_ip, Data, [{state_timeout, ?STD_TIMEOUT, retry}]} + end; + +% CONNECTING +handle_event(enter, _OldState, connecting, _Data) -> + {ok, Domain} = application:get_env(grisp_seawater, seawater_domain), + {ok, Port} = application:get_env(grisp_seawater, seawater_port), + ?LOG_NOTICE("Connecting to ~p:~p",[Domain, Port]), + {keep_state_and_data, [{state_timeout, 0, retry}]}; +handle_event(state_timeout, retry, connecting, Data) -> + case grisp_seawater_client:connect() of + ok -> + ?LOG_NOTICE("Connection enstablished!"), + {next_state, pinging, Data}; + Error -> + ?LOG_ERROR("Connection failed with error ~p, Retryng ...",[Error]), + {keep_state_and_data, [{state_timeout, ?STD_TIMEOUT, retry}]} + end; + +% PINGING +handle_event(enter, _OldState, pinging, Data) -> + {next_state, pinging, Data, [{state_timeout, ?STD_TIMEOUT, retry}]}; +handle_event(state_timeout, retry, pinging, Data) -> + case grisp_seawater_client:ping() of + {ok, <<"pong">>} -> + {next_state, connected, Data}; + {ok, <<"pang">>} -> + ?LOG_WARNING("Device not linked!"), + {next_state, connected, Data}; + {error, disconnected} -> + {next_state, connecting, Data} + end; + +% CONNECTED +handle_event(enter, _OldState, connected, _Data) -> + keep_state_and_data; +handle_event(info, disconnected, connected, Data) -> + ?LOG_WARNING("Disconnected!"), + {next_state, waiting_ip, Data}; + +handle_event(E, OldS, NewS, Data) -> + ?LOG_ERROR("Unhandled Event = ~p, OldS = ~p, NewS = ~p",[E, OldS, NewS]), + {keep_state, Data}. + +% INTERNALS -------------------------------------------------------------------- + +check_inet_ipv4() -> + case get_ip_of_valid_interfaces() of + {_,_,_,_} = IP when IP =/= {127,0,0,1} -> {ok, IP}; + _ -> invalid + end. + +get_ipv4_from_opts([]) -> + undefined; +get_ipv4_from_opts([{addr, {_1, _2, _3, _4}} | _]) -> + {_1, _2, _3, _4}; +get_ipv4_from_opts([_ | TL]) -> + get_ipv4_from_opts(TL). + +has_ipv4(Opts) -> + get_ipv4_from_opts(Opts) =/= undefined. + +flags_are_ok(Flags) -> + lists:member(up, Flags) and + lists:member(running, Flags) and + not lists:member(loopback, Flags). + +get_valid_interfaces() -> + {ok, Interfaces} = inet:getifaddrs(), + [ + Opts + || {_Name, [{flags, Flags} | Opts]} <- Interfaces, + flags_are_ok(Flags), + has_ipv4(Opts) + ]. + +get_ip_of_valid_interfaces() -> + case get_valid_interfaces() of + [Opts | _] -> get_ipv4_from_opts(Opts); + _ -> undefined + end. diff --git a/src/grisp_seawater.app.src b/src/grisp_seawater.app.src index ae216ca..653293a 100644 --- a/src/grisp_seawater.app.src +++ b/src/grisp_seawater.app.src @@ -15,7 +15,8 @@ ]}, {env, [ {seawater_domain, "grisp.io"}, - {seawater_port, 7777} + {seawater_port, 7777}, + {connect, true} % keeps a constant connection with grisp.io ]}, {modules, []}, {links, []} diff --git a/src/grisp_seawater_client.erl b/src/grisp_seawater_client.erl index 0417baa..668f9c5 100644 --- a/src/grisp_seawater_client.erl +++ b/src/grisp_seawater_client.erl @@ -1,3 +1,4 @@ +%% @doc Websocket client to connect to grisp.io -module(grisp_seawater_client). -export([start_link/0]). @@ -108,6 +109,7 @@ handle_info({gun_down, C, ws, closed, [Stream]}, erlang:cancel_timer(Tref), gen_server:reply(Caller, {error, ws_closed}) end || {Caller, Tref} <- maps:values(Requests)], + grisp_io_connection ! disconnected, {noreply, #state{}}; handle_info(M, S) -> ?LOG_WARNING("Unandled WS message: ~p", [M]), diff --git a/src/grisp_seawater_sup.erl b/src/grisp_seawater_sup.erl index 70223f6..9fda891 100644 --- a/src/grisp_seawater_sup.erl +++ b/src/grisp_seawater_sup.erl @@ -36,7 +36,8 @@ init([]) -> }, ChildSpecs = [ worker(grisp_seawater_ntp, []), - worker(grisp_seawater_client, [])], + worker(grisp_seawater_client, []), + worker(grisp_io_connection, [])], {ok, {SupFlags, ChildSpecs}}. %% internal functions