-module(grisp_connect_ntp). -behaviour(gen_statem). -include_lib("kernel/include/logger.hrl"). %--- Exports ------------------------------------------------------------------- % API functions -export([start_link/0]). -export([get_time/0, get_time/1]). % Behaviour gen_statem callback functions -export([callback_mode/0]). -export([init/1]). % Behaviour gen_statem states callback functions -export([waiting_ip/3]). -export([refresh_time/3]). -export([ready/3]). %--- Types --------------------------------------------------------------------- -record(data, { retry_count = 0 :: non_neg_integer() }). %--- Macros -------------------------------------------------------------------- -define(NTP_PORT, 123). % NTP's UDP port -define(EPOCH, 2208988800). % offset yr 1900 to unix epochù -define(HANDLE_COMMON, ?FUNCTION_NAME(EventType, EventContent, Data) -> handle_common(EventType, EventContent, ?FUNCTION_NAME, Data)). %--- API FUNCTIONS ------------------------------------------------------------- start_link() -> gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). get_time() -> get_time(random_ntp_server()). get_time(Host) -> gen_statem:call(?MODULE, {?FUNCTION_NAME, Host}). %--- BEHAVIOUR gen_statem CALLBACK FUNCTIONS ----------------------------------- callback_mode() -> [state_functions, state_enter]. init([]) -> {ok, waiting_ip, #data{}}. %--- BEHAVIOUR gen_statem STATES CALLBACK FUNCTIONS ----------------------------- waiting_ip(enter, _OldState, _Data) -> % First IP check do not have any delay {keep_state_and_data, [{state_timeout, 0, check_ip}]}; waiting_ip(state_timeout, check_ip, Data) -> case grisp_connect_utils:check_inet_ipv4() of {ok, _IP} -> {next_state, refresh_time, Data}; invalid -> {keep_state_and_data, [{state_timeout, 1000, check_ip}]} end; ?HANDLE_COMMON. refresh_time(enter, _OldState, #data{retry_count = RetryCount}) -> Delay = grisp_connect_utils:retry_delay(RetryCount), {keep_state_and_data, [{state_timeout, Delay, request_time}]}; refresh_time(state_timeout, request_time, Data = #data{retry_count = RetryCount}) -> NTPServer = random_ntp_server(), case refresh_current_time(NTPServer) of {error, Reason} -> ?LOG_INFO("Failed to get time from NTP server ~s: ~w", [NTPServer, Reason]), {next_state, waiting_ip, Data#data{retry_count = RetryCount + 1}}; {ok, Datetime} -> ?LOG_INFO("GRiSP clock set from NTP to ~s", [format_datetime(Datetime)]), {next_state, ready, Data#data{retry_count = 0}} end; ?HANDLE_COMMON. ready(enter, _OldState, _Data) -> Period = refresh_period(), ?LOG_DEBUG("Schedule NTP time refresh in ~w seconds", [Period]), {keep_state_and_data, [{state_timeout, Period * 1000, refresh_time}]}; ready({call, From}, {get_time, Host}, _Data) -> Reply = case do_get_time(Host) of {error, _Reason} = Error -> Error; {ok, Time} -> {ok, Time} end, {keep_state_and_data, [{reply, From, Reply}]}; ready(state_timeout, refresh_time, Data) -> {next_state, refresh_time, Data}; ?HANDLE_COMMON. handle_common({call, _From}, {get_time, _Host}, _State, _Data) -> {keep_state_and_data, [postpone]}; handle_common({call, From}, Msg, State, _Data) -> ?LOG_WARNING("Unexpected call from ~w in state ~w: ~w", [From, State, Msg]), {keep_state_and_data, [{reply, From, {error, unexpected_call}}]}; handle_common(cast, Msg, State, _Data) -> ?LOG_WARNING("Unexpected cast in state ~w: ~w", [State, Msg]), keep_state_and_data; handle_common(info, Msg, State, _Data) -> ?LOG_DEBUG("Unexpected message in state ~w: ~w", [State, Msg]), keep_state_and_data. %--- INTERNAL FUNCTIONS -------------------------------------------------------- format_datetime({{Year, Month, Day}, {Hour, Min, Sec}}) -> iolist_to_binary(io_lib:format( "~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B", [Year, Month, Day, Hour, Min, Sec] )). refresh_current_time(NTPServer) -> case do_get_time(NTPServer) of {error, _Reason} = Error -> Error; {ok, Time} -> try RefTS1970 = round(proplists:get_value(receiveTimestamp, (tuple_to_list(Time)))), CurrSecs = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) + RefTS1970, CurrDateTime = calendar:gregorian_seconds_to_datetime(CurrSecs), grisp_rtems:clock_set({CurrDateTime, 0}), {ok, CurrDateTime} catch _:Reason -> {error, Reason} end end. ntp_servers() -> {ok, NTPServers} = application:get_env(grisp_connect, ntp_servers), NTPServers. % NTP refresh perido in seconds refresh_period() -> {ok, Period} = application:get_env(grisp_connect, ntp_refresh_period), Period. random_ntp_server() -> lists:nth(rand:uniform(length(ntp_servers())), ntp_servers()). do_get_time(Host) -> case ntp_request(Host, create_ntp_request()) of {error, _Reason} = Error -> Error; {ok, Resp} -> try process_ntp_response(Resp) of Time -> {ok, Time} catch _:Reason -> {error, Reason} end end. ntp_request(Host, Binary) -> case gen_udp:open(0, [binary, {active, false}]) of {error, _Reason} = Error -> Error; {ok, Socket} -> try gen_udp:send(Socket, Host, ?NTP_PORT, Binary) of {error, _Reason} = Error -> Error; ok -> case gen_udp:recv(Socket, 0, 500) of {error, _Reason} = Error -> Error; {ok, {_Address, _Port, Resp}} -> {ok, Resp} end after gen_udp:close(Socket) end end. process_ntp_response(Ntp_response) -> <> = Ntp_response, {NowMS, NowS, NowUS} = erlang:timestamp(), NowTimestamp = NowMS * 1.0e6 + NowS + NowUS/1000, TransmitTimestamp = XmtI - ?EPOCH + binfrac(XmtF), {{li, LI}, {vn, Version}, {mode, Mode}, {stratum, Stratum}, {poll, Poll}, {precision, Precision}, {rootDelay, RootDel}, {rootDispersion, RootDisp}, {referenceId, R1, R2, R3, R4}, {referenceTimestamp, RtsI - ?EPOCH + binfrac(RtsF)}, {originateTimestamp, OtsI - ?EPOCH + binfrac(OtsF)}, {receiveTimestamp, RcvI - ?EPOCH + binfrac(RcvF)}, {transmitTimestamp, TransmitTimestamp}, {clientReceiveTimestamp, NowTimestamp}, {offset, TransmitTimestamp - NowTimestamp}}. create_ntp_request() -> << 0:2, 4:3, 3:3, 0:(3*8 + 3*32 + 4*64) >>. binfrac(Bin) -> binfrac(Bin, 2, 0). binfrac(0, _, Frac) -> Frac; binfrac(Bin, N, Frac) -> binfrac(Bin bsr 1, N*2, Frac + (Bin band 1)/N).