Reputation: 2747
There is an locked door example about gen_fsm
in the Elrang Otp System Documentation. I have a question about timeout. I will copy the code here first:
-module(code_lock).
-behaviour(gen_fsm).
-export([start_link/1]).
-export([button/1]).
-export([init/1, locked/2, open/2]).
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, code_lock, lists:reverse(Code), []).
button(Digit) ->
gen_fsm:send_event(code_lock, {button, Digit}).
init(Code) ->
{ok, locked, {[], Code}}.
locked({button, Digit}, {SoFar, Code}) ->
case [Digit|SoFar] of
Code ->
do_unlock(),
{next_state, open, {[], Code}, 30000};
Incomplete when length(Incomplete)<length(Code) ->
{next_state, locked, {Incomplete, Code}};
_Wrong ->
{next_state, locked, {[], Code}}
end.
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
Here is the question: when the door is opened, if I press the button, the gen_fsm
will have an {button, Digit}
event at the state open
. An error will occurs. But if I add these code after open function:
open(_Event, State) ->
{next_state, open, State}.
Then if I press the button in 30s, the timeout will not be occurs. The door will be opened forever. What should I do?
Thanks.
I know I could use send_event_after
or something like that. But I don't think it is a good idea. Because the state you excepted to handle the message may be changed in a complex application.
For example, if I have a function to lock the door manually after the door opened in 30s. Then locked
will handle the timeout
message, which is not the excepted behaviour.
Upvotes: 1
Views: 215
Reputation: 91865
You could maintain the remaining timeout in StateData
. To do this, add a third item to the tuple:
init(Code) ->
{ok, locked, {[], Code, infinity}}.
You'll need to change locked
to set the initial value:
locked({button, Digit}, {SoFar, Code, _Until}) ->
case [Digit|SoFar] of
Code ->
do_unlock(),
Timeout = 30000,
Now = to_milliseconds(os:timestamp()),
Until = Now + Timeout,
{next_state, open, {[], Code, Until}, Timeout};
Incomplete when length(Incomplete)<length(Code) ->
{next_state, locked, {Incomplete, Code, infinity}};
_Wrong ->
{next_state, locked, {[], Code, infinity}}
end.
And, if a button is pressed while open, calculate the new timeout and go around again:
open({button, _Digit}, {_SoFar, _Code, Until} = State) ->
Now = to_milliseconds(os:timestamp()),
Timeout = Until - Now,
{next_state, open, State, Timeout};
You'll also need the following helper function:
to_milliseconds({Me, S, Mu}) ->
(Me * 1000 * 1000 * 1000) + (S * 1000) + (Mu div 1000).
Upvotes: 1
Reputation: 14042
Using the fsm timeout, it is not possible - as far as I know - to avoid the re-initialization of it:
If none of these solutions satisfy you, you can use an external process to create the timeout:
-module(code_lock).
-behaviour(gen_fsm).
-export([start_link/1]).
-export([button/1,stop/0]).
-export([init/1, locked/2, open/2,handle_event/3,terminate/3]).
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, code_lock, lists:reverse(Code), []).
button(Digit) ->
gen_fsm:send_event(code_lock, {button, Digit}).
stop() ->
gen_fsm:send_all_state_event(code_lock, stop).
init(Code) ->
{ok, locked, {[], Code}}.
locked({button, Digit}, {SoFar, Code}) ->
case [Digit|SoFar] of
Code ->
do_unlock(),
timeout(10000,code_lock),
{next_state, open, {[], Code}};
Incomplete when length(Incomplete)<length(Code) ->
{next_state, locked, {Incomplete, Code}};
_Wrong ->
{next_state, locked, {[], Code}}
end.
open(timeout, State) ->
do_lock(),
{next_state, locked, State};
open(_, State) ->
{next_state, open, State}.
handle_event(stop, _StateName, StateData) ->
{stop, normal, StateData}.
terminate(normal, _StateName, _StateData) ->
ok.
do_lock() -> io:format("locking the door~n").
do_unlock() -> io:format("unlocking the door~n").
timeout(X,M) ->
spawn(fun () -> receive
after X -> gen_fsm:send_event(M,timeout)
end
end).
There are a bunch of functions in the module timer to do that, preferable to my custom example.
maybe a better usage of the Fsm timeout should be in the lock state:
EDIT: to Bin Wang: what you say in your update is correct, but you cannot avoid to manage this situation. I don't know any built in function that cover your use case. To satisfy it you will need to manage the unexpected timeout message in the lock state, but to avoid multiple timeout running, you will need also to stop the current one before to go to lock state. Note that this does not prevent you to manage the timeout message in lock state, because there is a race between the message to stop the timer and the timeout itself. I wrote for one of my application a general purpose apply_after function that can be canceled, stopped and resumed:
applyAfter_link(T, F, A) ->
V3 = time_ms(),
spawn_link(fun () -> applyAfterp(T, F, A, V3) end).
applyAfterp(T, F, A, Time) ->
receive
cancel -> ok;
suspend when T =/= infinity ->
applyAfterp(infinity, F, A, T + Time - time_ms());
suspend ->
applyAfterp(T, F, A, Time);
resume when T == infinity ->
applyAfterp(Time, F, A, time_ms());
resume ->
Tms = time_ms(), applyAfterp(T + Time - Tms, F, A, Tms)
after T ->
%% io:format("apply after time ~p, function ~p, arg ~p , stored time ~p~n",[T,F,A,Time]),
catch F(A)
end.
time_us() ->
{M, S, U} = erlang:now(),
1000000 * (1000000 * M + S) + U.
time_ms() -> time_us() div 1000.
You will need to sore the Pid of the timeout process in the FSM state.
Upvotes: 0
Reputation: 1814
You should be specifying a timeout at the open function "open(_Event, State)"
Since the next state is proceeded without timeout.. the door will remain open forever and no where a timeout occurs..
The newly defined function should be
open(_Event, State) -> {next_state, open, State, 30000}. %% State should be re-initialized
Upvotes: 0