@@ 1,5 1,4 @@
% Timing functions.
-% This is... hoo boy, gonna get weird.
% See we need to have a single clock that everything uses, but it has to
% be able to be simulated as well. So this clock is going to stop and
% start, potentially run at a different rate than realtime, etc. It
@@ 8,24 7,15 @@
% away your state and start over", which *could* involve moving
% backwards.
%
-% This is kinda pubsub-y, consider the Erlang `pg` module to implement
-% it.
-%
-% This is gonna heck up *everything* downstream of it unless I abstract
-% it away real good. Guess that's *one* thing ROS does for me, and does
-% it well enough I don't notice.
-%
+% We are going to use the Erlang Term Storage to store a "global
+% mutable" value and have a little node to update it continuously.
+% Then a client can either get the latest time, or have a process
+% running that will keep track of the last time it was asked for
+% time and check for jumps, reset's, etc.
%
-% We could do this with a gen_server that receives requests for
-% subscriptions, but those aren't really designed to send un-asked-for
-% messages. So this might just be best as its own little thing.
-%
-% This is kinda similar to https://erlang.org/doc/man/timer.html
-% and might be best implemented using https://erlang.org/doc/man/erlang.html#send_after-4
-% but however that occurs, heck, the API will remain the same. So
-% let's just start writing code. This may or may not really end up
-% as a production quality end point, but I don't see a better way
-% to do it.
+% Time is Hard. See
+% https://www.erlang.org/doc/apps/erts/time_correction.html
+% for some of the details around it.
%
% The timer runs at a fixed rate. For now we will call it 100 Hz, so
% this is the maximum resolution it can manage. It uses Erlang
@@ 40,20 30,9 @@
% milliseconds. The user may not assume that they know what the rate or
% resolution of the timer is. Just that, as far as the robot is
% concerned, each message is "exact enough".
-%
-% There is also a "reset" message that can be sent to
-% clients to notify them that the clock has changed in some abrupt way,
-% and they need to throw out all their state and start over.
-% Following times have no relation to previous times, even if they
-% look like they do.
-%
-% This actually takes up a noticable amount of CPU time, which makes
-% me a little frumple, but the interface can stay the same I guess.
-%
-% TODO: Consider using ets (erlang term storage) instead, or atomics,
-% it has an update_counter function already
-module(gh_time).
--export([init/0, start/0, start_link/0, subscribe/1, unsubscribe/1, reset/0]).
+-export([init/0, start/0, start_link/0, raw_time/0, start_timeserver/0,
+ timeserver/1, timeserver_time/1]).
% Start the timer service
start() ->
@@ 64,63 43,86 @@ start_link() ->
spawn_link(?MODULE, init, []).
init() ->
- % We use the `pg` module to keep track of what processes
- % are subscribed to the timer: https://erlang.org/doc/man/pg.html
- % We don't care if it's already started or not.
- pg:start_link(),
register(gh_time, self()),
- loop(erlang:monotonic_time(milli_seconds)).
+ Now = erlang:monotonic_time(milli_seconds),
+ Table = ets:new(gh_time, [set, named_table]),
+ % Add initial time just in case
+ ets:insert(Table, {now, Now}),
+ loop(Table, Now).
-% Subscribe to timer updates
-subscribe(Pid) ->
- gh_time ! {subscribe, Pid}.
-
+% Call this function to get the raw time.
+% Users should do start_timeserver instead.
+raw_time() ->
+ [{now, Now}] = ets:lookup(gh_time, now),
+ Now.
-% Unsubscribe to timer updates
-unsubscribe(Pid) ->
- gh_time ! {unsubscribe, Pid}.
+% Actually it should start a time tracker thingy for the user.
+% It should run one per user process and keep track of its local
+% time info, so it can report dt and whether the time has jumped,
+% reset, or something else.
+%
+% TODO: Start loop if necessary???
+% Link this to the loop so we restart these if the loop screws up?
+% Ponder more later.
+start_timeserver() ->
+ Now = raw_time(),
+ spawn(?MODULE, timeserver, [Now]).
-% Reset the timer, notifying all clients
-reset() ->
- gh_time ! reset.
+timeserver_time(Timeserver) ->
+ Timeserver ! self(),
+ receive
+ Res -> Res
+ % TODO: Timeout?
+ end.
-loop(LastUpdate) ->
- Clients = pg:get_local_members(gh_time),
- case handle_messages() of
- ok -> ok;
- reset -> lists:foreach(fun(Client) -> Client ! gh_time_reset end, Clients)
- end,
+% This is the main loop that uses the Erlang Term Storage to
+% store what the last known time is. ETS is node-local, so
+% time is node-local.
+%
+% Time is monotonic. FOR NOW we do not do anything
+% Time 0 point is arbitrary???
+% Units??? -- selectable, milliseconds for now.
+% See https://www.erlang.org/doc/apps/erts/time_correction.html
+%
+% To consider later: alternate time sources such as a specific
+% chrony server, GPS time source, etc.
+loop(Table, LastUpdate) ->
% Milliseconds per tick
Ms = 10,
Now = erlang:monotonic_time(milli_seconds),
%io:format("Time is ~p, last update ~p, dt ~p~n", [Now, LastUpdate, Now - LastUpdate]),
- case (Now - LastUpdate) >= Ms of
- true -> lists:foreach(
- fun(Client) -> Client ! {gh_time, Now} end,
- Clients
- ),
- loop(LastUpdate + Ms);
- % Would be kinda nice if we could sleep for less than 1 ms, but
- % oh well. 0 is another option.
- false -> timer:sleep(1),
- loop(LastUpdate)
+ Dt = (Now - LastUpdate),
+ case Dt >= Ms of
+ % Update the value stored in ets
+ true ->
+ ets:insert(Table, {now, Now}),
+ loop(Table, LastUpdate + Ms);
+ % Not time to update, sleep a bit
+ false ->
+ % We try to sleep for Dt-1, min 1 ms
+ %SleepTime = max(1, Dt-1),
+ % Or we can just KISS
+ SleepTime = 1,
+ timer:sleep(SleepTime),
+ loop(Table, LastUpdate)
end.
-% Handles all incoming messages, and returns `reset` if a reset
-% was triggered, and `ok` otherwise
-handle_messages() ->
- handle_messages(ok).
-
-% Loop through all incoming messages and deal with them.
-handle_messages(NeedsReset) ->
+timeserver(LastUpdate) ->
+ % Hmmm, what should the max error be?
+ % Basically we want to warn the client if
+ % it hasn't called this frequently enough.
+ % Maybe that's the client's problem?
+ % Hmmmm. This is intended to keep track of
+ % it *for* the client. Maybe have a hz param.
+ MaxError = 10,
+ Now = raw_time(),
+ Dt = Now - LastUpdate,
+ CurrentState =
+ if Dt > MaxError -> skip;
+ Dt < 0 -> reset;
+ Dt =< MaxError -> ok
+ end,
receive
- {subscribe, Pid} ->
- pg:join(gh_time, Pid),
- handle_messages(NeedsReset);
- {unsubscribe, Pid} ->
- pg:leave(gh_time, Pid),
- handle_messages(NeedsReset);
- reset -> handle_messages(reset)
- after 0 ->
- NeedsReset
- end.
+ Pid -> Pid ! {CurrentState, Now, Dt}
+ end,
+ timeserver(Now).