From 2699cd204f8cc2b3a4f457ff6d25651508db42b3 Mon Sep 17 00:00:00 2001
From: Raimo Niskanen
@@ -377,7 +377,7 @@ State(S) x Event(E) -> Actions(A), State(S')
callback function is called before any
@@ -698,37 +699,44 @@ StateName(EventType, EventContent, Data) ->
-export([start_link/1]).
-export([button/1]).
--export([init/1,callback_mode/0,terminate/3,code_change/4]).
+-export([init/1,callback_mode/0,terminate/3]).
-export([locked/3,open/3]).
start_link(Code) ->
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
-button(Digit) ->
- gen_statem:cast(?NAME, {button,Digit}).
+button(Button) ->
+ gen_statem:cast(?NAME, {button,Button}).
init(Code) ->
do_lock(),
- Data = #{code => Code, remaining => Code},
+ Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
callback_mode() ->
state_functions.
-
+ ]]>
+
- case Remaining of
- [Digit] ->
+ cast, {button,Button},
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
+ NewButtons =
+ if
+ length(Buttons) < Length ->
+ Buttons;
+ true ->
+ tl(Buttons)
+ end ++ [Button],
+ if
+ NewButtons =:= Code -> % Correct
do_unlock(),
- {next_state, open, Data#{remaining := Code},
+ {next_state, open, Data#{buttons := []},
[{state_timeout,10000,lock}]};
- [Digit|Rest] -> % Incomplete
- {next_state, locked, Data#{remaining := Rest}};
- _Wrong ->
- {next_state, locked, Data#{remaining := Code}}
+ true -> % Incomplete | Incorrect
+ {next_state, locked, Data#{buttons := NewButtons}}
end.
-
+ ]]>
+
do_lock(),
{next_state, locked, Data};
@@ -743,8 +751,6 @@ do_unlock() ->
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
-code_change(_Vsn, State, Data, _Extra) ->
- {ok, State, Data}.
]]>
The code is explained in the next sections.
@@ -820,17 +826,17 @@ start_link(Code) -> in this case
do_lock(),
- Data = #{code => Code, remaining => Code},
- {ok,locked,Data}.
+ Data = #{code => Code, length => length(Code), buttons => []},
+ {ok, locked, Data}.
]]>
Function
- state_functions.
- ]]>
Function
+ state_functions.
+ ]]>
@@ -884,7 +890,7 @@ button(Digit) ->
- The event is made into a message and sent to the
- case Remaining of
- [Digit] -> % Complete
+ cast, {button,Button},
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
+ NewButtons =
+ if
+ length(Buttons) < Length ->
+ Buttons;
+ true ->
+ tl(Buttons)
+ end ++ [Button],
+ if
+ NewButtons =:= Code -> % Correct
do_unlock(),
- {next_state, open, Data#{remaining := Code},
+ {next_state, open, Data#{buttons := []},
[{state_timeout,10000,lock}]};
- [Digit|Rest] -> % Incomplete
- {next_state, locked, Data#{remaining := Rest}};
- [_|_] -> % Wrong
- {next_state, locked, Data#{remaining := Code}}
+ true -> % Incomplete | Incorrect
+ {next_state, locked, Data#{buttons := NewButtons}}
end.
-
-open(state_timeout, lock, Data) ->
- do_lock(),
- {next_state, locked, Data};
-open(cast, {button,_}, Data) ->
- {next_state, open, Data}.
]]>
- If the door is locked and a button is pressed, the pressed
- button is compared with the next correct button.
+ In state
- If the pressed button is incorrect, the server data - restarts from the start of the code sequence. -
-
- If the whole code is correct, the server changes states
- to
+ {next_state, open, Data}.
+ ]]>
In state
@@ -986,9 +996,9 @@ open(state_timeout, lock, Data) ->
Consider a
...
locked(...) -> ... ;
locked(EventType, EventContent, Data) ->
- handle_event(EventType, EventContent, Data).
+ handle_common(EventType, EventContent, Data).
...
open(...) -> ... ;
open(EventType, EventContent, Data) ->
- handle_event(EventType, EventContent, Data).
+ handle_common(EventType, EventContent, Data).
-handle_event({call,From}, code_length, #{code := Code} = Data) ->
+handle_common({call,From}, code_length, #{code := Code} = Data) ->
{keep_state, Data, [{reply,From,length(Code)}]}.
]]>
+
+
+ Another way to do it is through a convenience macro
+
+ gen_statem:call(?NAME, code_length).
+
+-define(HANDLE_COMMON(T, C, D),
+ ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))).
+%%
+handle_common({call,From}, code_length, #{code := Code} = Data) ->
+ {keep_state, Data, [{reply,From,length(Code)}]}.
+
+...
+locked(...) -> ... ;
+?HANDLE_COMMON.
+
+...
+open(...) -> ... ;
+?HANDLE_COMMON.
+ ]]>
+
This example uses
- {next_state, locked, Data#{remaining := Code}};
+locked(timeout, _, Data) ->
+ {next_state, locked, Data#{buttons := []}};
locked(
cast, {button,Digit},
- #{code := Code, remaining := Remaining} = Data) ->
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
...
- [Digit|Rest] -> % Incomplete
- {next_state, locked, Data#{remaining := Rest}, 30000};
+ true -> % Incomplete | Incorrect
+ {next_state, locked, Data#{buttons := NewButtons},
+ 30000}
...
]]>
@@ -1189,6 +1232,13 @@ locked( Whatever event you act on has already cancelled the event time-out...
+
+ Note that an event time-out does not work well with
+ when you have for example a status call as in
+
receive
- {button,Digit} when Remaining =:= [] ->
- do_unlock(),
- open(Code);
- {button,Digit} ->
- locked(Code, Remaining);
- {button,_} ->
- locked(Code, Code)
+ {button,Button} ->
+ NewButtons =
+ if
+ length(Buttons) < Length ->
+ Buttons;
+ true ->
+ tl(Buttons)
+ end ++ [Button],
+ if
+ NewButtons =:= Code -> % Correct
+ do_unlock(),
+ open(Code, Length);
+ true -> % Incomplete | Incorrect
+ locked(Code, Length, NewButtons)
+ end
end.
-
-open(Code) ->
+ ]]>
+
receive
after 10000 ->
do_lock(),
- locked(Code, Code)
+ locked(Code, Length, [])
end.
do_lock() ->
@@ -1483,7 +1545,7 @@ do_unlock() ->
...
init(Code) ->
process_flag(trap_exit, true),
- Data = #{code => Code},
+ Data = #{code => Code, length = length(Code)},
{ok, locked, Data}.
callback_mode() ->
@@ -1491,13 +1553,14 @@ callback_mode() ->
locked(enter, _OldState, Data) ->
do_lock(),
- {keep_state,Data#{remaining => Code}};
+ {keep_state,Data#{buttons => []}};
locked(
cast, {button,Digit},
- #{code := Code, remaining := Remaining} = Data) ->
- case Remaining of
- [Digit] ->
- {next_state, open, Data};
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
+...
+ if
+ NewButtons =:= Code -> % Correct
+ {next_state, open, Data};
...
open(enter, _OldState, _Data) ->
@@ -1557,48 +1620,50 @@ open(state_timeout, lock, Data) ->
to synchronize the state machines.
- The following example uses an input model where you give the lock
- characters with put_chars(Chars) and then call
- enter() to finish the input.
+ The following example uses an input model where the buttons
+ generate up/down events and the lock responds to an up
+ event after the corresponding down event.
- gen_statem:call(?NAME, {chars,Chars}).
+down(button) ->
+ gen_statem:cast(?NAME, {down,Button}).
-enter() ->
- gen_statem:call(?NAME, enter).
+up(button) ->
+ gen_statem:cast(?NAME, {up,Button}).
...
locked(enter, _OldState, Data) ->
do_lock(),
{keep_state,Data#{remaining => Code, buf => []}};
+locked(
+ internal, {button,Digit},
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
...
-
-handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) ->
- {keep_state, Data#{buf := [Chars|Buf],
- [{reply,From,ok}]};
-handle_event({call,From}, enter, #{buf := Buf} = Data) ->
- Chars = unicode:characters_to_binary(lists:reverse(Buf)),
- try binary_to_integer(Chars) of
- Digit ->
- {keep_state, Data#{buf := []},
- [{reply,From,ok},
- {next_event,internal,{button,Chars}}]}
- catch
- error:badarg ->
- {keep_state, Data#{buf := []},
- [{reply,From,{error,not_an_integer}}]}
+ ]]>
+
+ {keep_state, Data#{button := Button};
+handle_common(cast, {up,Button}, Data) ->
+ case Data of
+ #{button := Button} ->
+ {keep_state,maps:remove(button, Data),
+ [{next_event,internal,{button,Button}}]};
+ #{} ->
+ keep_state_and_data
end;
...
+
+open(internal, {button,_}, Data) ->
+ {keep_state,Data,[postpone]};
+...
]]>
If you start this program with code_lock:start([17])
- you can unlock with code_lock:put_chars(<<"001">>),
- code_lock:put_chars(<<"7">>), code_lock:enter() .
+ you can unlock with code_lock:down(17), code_lock:up(17).
@@ -1612,13 +1677,15 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->
modifications and some more using state enter calls,
which deserves a new state diagram:
-
+
+
Code Lock State Diagram Revisited
Notice that this state diagram does not specify how to handle
a button event in the state open . So, you need to
- read somewhere else that unspecified events
+ read here that unspecified events
must be ignored as in not consumed but handled in some other state.
Also, the state diagram does not show that the code_length/0
call must be handled in every state.
@@ -1636,8 +1703,8 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->
-define(NAME, code_lock_2).
-export([start_link/1,stop/0]).
--export([button/1,code_length/0]).
--export([init/1,callback_mode/0,terminate/3,code_change/4]).
+-export([down/1,up/1,code_length/0]).
+-export([init/1,callback_mode/0,terminate/3]).
-export([locked/3,open/3]).
start_link(Code) ->
@@ -1645,52 +1712,74 @@ start_link(Code) ->
stop() ->
gen_statem:stop(?NAME).
-button(Digit) ->
- gen_statem:cast(?NAME, {button,Digit}).
+down(Digit) ->
+ gen_statem:cast(?NAME, {down,Digit}).
+up(Digit) ->
+ gen_statem:cast(?NAME, {up,Digit}).
code_length() ->
gen_statem:call(?NAME, code_length).
-
+ ]]>
+
process_flag(trap_exit, true),
- Data = #{code => Code},
+ Data = #{code => Code, length => Length, buttons => []},
{ok, locked, Data}.
callback_mode() ->
[state_functions,state_enter].
-locked(enter, _OldState, #{code := Code} = Data) ->
+-define(HANDLE_COMMON,
+ ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))).
+%%
+handle_common(cast, {down,Button}, Data) ->
+ {keep_state, Data#{button => Button}};
+handle_common(cast, {up,Button}, Data) ->
+ case Data of
+ #{button := Button} ->
+ {keep_state, maps:remove(button, Data),
+ [{next_event,internal,{button,Data}}]};
+ #{} ->
+ keep_state_and_data
+ end;
+handle_common({call,From}, code_length, #{code := Code}) ->
+ {keep_state_and_data, [{reply,From,length(Code)}]}.
+ ]]>
+
do_lock(),
- {keep_state, Data#{remaining => Code}};
-locked(
- timeout, _,
- #{code := Code, remaining := Remaining} = Data) ->
- {keep_state, Data#{remaining := Code}};
+ {keep_state, Data#{buttons := []}};
+locked(state_timeout, button, Data) ->
+ {keep_state, Data#{buttons := []}};
locked(
- cast, {button,Digit},
- #{code := Code, remaining := Remaining} = Data) ->
- case Remaining of
- [Digit] -> % Complete
- {next_state, open, Data};
- [Digit|Rest] -> % Incomplete
- {keep_state, Data#{remaining := Rest}, 30000};
- [_|_] -> % Wrong
- {keep_state, Data#{remaining := Code}}
+ internal, {button,Digit},
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
+ NewButtons =
+ if
+ length(Buttons) < Length ->
+ Buttons;
+ true ->
+ tl(Buttons)
+ end ++ [Button],
+ if
+ NewButtons =:= Code -> % Correct
+ do_unlock(),
+ {next_state, open, Data,
+ [{state_timeout,10000,lock}]};
+ true -> % Incomplete | Incorrect
+ {keep_state, Data#{buttons := NewButtons},
+ [{state_timeout,30000,button}]}
end;
-locked(EventType, EventContent, Data) ->
- handle_event(EventType, EventContent, Data).
-
+?HANDLE_COMMON.
+ ]]>
+
do_unlock(),
{keep_state_and_data, [{state_timeout,10000,lock}]};
open(state_timeout, lock, Data) ->
{next_state, locked, Data};
-open(cast, {button,_}, _) ->
+open(internal, {button,_}, _) ->
{keep_state_and_data, [postpone]};
-open(EventType, EventContent, Data) ->
- handle_event(EventType, EventContent, Data).
-
-handle_event({call,From}, code_length, #{code := Code}) ->
- {keep_state_and_data, [{reply,From,length(Code)}]}.
+?HANDLE_COMMON.
do_lock() ->
io:format("Locked~n", []).
@@ -1700,8 +1789,6 @@ do_unlock() ->
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
-code_change(_Vsn, State, Data, _Extra) ->
- {ok,State,Data}.
]]>
@@ -1724,26 +1811,32 @@ callback_mode() ->
[handle_event_function,state_enter].
%% State: locked
-handle_event(
- enter, _OldState, locked,
- #{code := Code} = Data) ->
+handle_event(enter, _OldState, locked, Data) ->
do_lock(),
- {keep_state, Data#{remaining => Code}};
+ {keep_state, Data#{buttons := []}};
+handle_event(state_timeout, button, locked, Data) ->
+ {keep_state, Data#{buttons := []}};
handle_event(
- timeout, _, locked,
- #{code := Code, remaining := Remaining} = Data) ->
- {keep_state, Data#{remaining := Code}};
-handle_event(
- cast, {button,Digit}, locked,
- #{code := Code, remaining := Remaining} = Data) ->
- case Remaining of
- [Digit] -> % Complete
- {next_state, open, Data};
- [Digit|Rest] -> % Incomplete
- {keep_state, Data#{remaining := Rest}, 30000};
- [_|_] -> % Wrong
- {keep_state, Data#{remaining := Code}}
+ internal, {button,Digit}, locked,
+ #{code := Code, length := Length, buttons := Buttons} = Data) ->
+ NewButtons =
+ if
+ length(Buttons) < Length ->
+ Buttons;
+ true ->
+ tl(Buttons)
+ end ++ [Button],
+ if
+ NewButtons =:= Code -> % Correct
+ do_unlock(),
+ {next_state, open, Data,
+ [{state_timeout,10000,lock}]};
+ true -> % Incomplete | Incorrect
+ {keep_state, Data#{buttons := NewButtons},
+ [{state_timeout,30000,button}]}
end;
+ ]]>
+
@@ -1753,10 +1846,22 @@ handle_event(state_timeout, lock, open, Data) ->
{next_state, locked, Data};
handle_event(cast, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
+ ]]>
+
- {keep_state_and_data, [{reply,From,length(Code)}]}.
+handle_event(cast, {down,Button}, _State, Data) ->
+ {keep_state, Data#{button => Button}};
+handle_event(cast, {up,Button}, _State, Data) ->
+ case Data of
+ #{button := Button} ->
+ {keep_state, maps:remove(button, Data),
+ [{state_timeout,30000,button}]};
+ #{} ->
+ keep_state_and_data
+ end;
+handle_event({call,From}, code_length, _State, #{length := Length}) ->
+ {keep_state_and_data, [{reply,From,Length}]}.
...
]]>
@@ -1800,7 +1905,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->
@@ -1808,7 +1913,6 @@ format_status(Opt, [_PDict,State,Data]) ->
{State,
maps:filter(
fun (code, _) -> false;
- (remaining, _) -> false;
(_, _) -> true
end,
Data)},
@@ -1896,7 +2000,7 @@ format_status(Opt, [_PDict,State,Data]) ->
-export([start_link/2,stop/0]).
-export([button/1,code_length/0,set_lock_button/1]).
--export([init/1,callback_mode/0,terminate/3,code_change/4,format_status/2]).
+-export([init/1,callback_mode/0,terminate/3,format_status/2]).
-export([handle_event/4]).
start_link(Code, LockButton) ->
@@ -1911,10 +2015,11 @@ code_length() ->
gen_statem:call(?NAME, code_length).
set_lock_button(LockButton) ->
gen_statem:call(?NAME, {set_lock_button,LockButton}).
-
+ ]]>
+
process_flag(trap_exit, true),
- Data = #{code => Code, remaining => undefined},
+ Data = #{code => Code, length => length(Code), buttons => []},
{ok, {locked,LockButton}, Data}.
callback_mode() ->
@@ -1927,33 +2032,41 @@ handle_event(
[{reply,From,OldLockButton}]};
handle_event(
{call,From}, code_length,
- {_StateName,_LockButton}, #{code := Code}) ->
+ {_StateName,_LockButton}, #{length := Length}) ->
{keep_state_and_data,
- [{reply,From,length(Code)}]};
+ [{reply,From,Length}]};
+ ]]>
+
+handle_event(EventType, EventContent, {locked,LockButton}, Data) ->
case {EventType, EventContent} of
{enter, _OldState} ->
do_lock(),
- {keep_state, Data#{remaining := Code}};
- {timeout, _} ->
- {keep_state, Data#{remaining := Code}};
+ {keep_state, Data#{buttons := []}};
+ {state_timeout, button} ->
+ {keep_state, Data#{buttons := []}};
{{call,From}, {button,Digit}} ->
- case Remaining of
- [Digit] -> % Complete
+ #{length := Length, buttons := Buttons} = Data,
+ NewButtons =
+ if
+ length(Buttons) < Length ->
+ Buttons;
+ true ->
+ tl(Buttons)
+ end ++ [Button],
+ case Data of
+ #{code := NewButtons} ->
{next_state, {open,LockButton}, Data,
[{reply,From,ok}]};
- [Digit|Rest] -> % Incomplete
- {keep_state, Data#{remaining := Rest},
- [{reply,From,ok}, 30000]};
- [_|_] -> % Wrong
- {keep_state, Data#{remaining := Code},
- [{reply,From,ok}]}
- end
+ #{} ->
+ {keep_state, Data#{buttons := NewButtons},
+ [{reply,From,ok},
+ {state_timeout,30000,button}]}
+ end
end;
+ ]]>
+
do_unlock(),
- {keep_state_and_data, [{state_timeout,10000,lock}]};
+ {keep_state_and_data,
+ [{state_timeout,10000,lock}]};
{state_timeout, lock} ->
{next_state, {locked,LockButton}, Data};
{{call,From}, {button,Digit}} ->
@@ -1975,7 +2089,8 @@ handle_event(
[postpone]}
end
end.
-
+ ]]>
+
io:format("Locked~n", []).
do_unlock() ->
@@ -1984,8 +2099,6 @@ do_unlock() ->
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
-code_change(_Vsn, State, Data, _Extra) ->
- {ok,State,Data}.
format_status(Opt, [_PDict,State,Data]) ->
StateData =
{State,
@@ -2046,7 +2159,8 @@ handle_event(
{enter, _OldState} ->
do_unlock(),
{keep_state_and_data,
- [{state_timeout,10000,lock},hibernate]};
+ [{state_timeout,10000,lock},
+ hibernate]};
...
]]>
-- cgit v1.2.3