From ea1963553ffb06eb1bda636f328718f91136ed9c Mon Sep 17 00:00:00 2001
From: Raimo Niskanen
- This behavior module provides a state machine. Two
-
+ This reference manual describes types generated from the types
+ in the
+ To get an overview of the concepts and operation of One for finite-state machines
- ( One without restriction on the state data type
- that uses one callback function for all states
-
- This is a new behavior in Erlang/OTP 19.0.
- It has been thoroughly reviewed, is stable enough
- to be used by at least two heavy OTP applications,
- and is here to stay.
- Depending on user feedback, we do not expect
- but can find it necessary to make minor
- not backward compatible changes into Erlang/OTP 20.0.
+ This behavior appeared in Erlang/OTP 19.0.
+ In OTP 19.1 a backwards incompatible change of
+ the return tuple from
+
- The
+ Two
+ One for finite-state machines
+ ( One without restriction on the state data type
+ that uses one callback function for all states
+
The callback model(s) for
+ There is also a server start option
+
-
+
+
+
+
- This is a new behavior in Erlang/OTP 19.0. - It has been thoroughly reviewed, is stable enough - to be used by at least two heavy OTP applications, and is here to stay. - Depending on user feedback, we do not expect - but can find it necessary to make minor - not backward compatible changes into Erlang/OTP 20.0. -
-
+ The callback module contains functions that implement
+ the state machine.
+ When an event occurs,
+ the
+ The behaviour engine holds the state machine state, + server data, timer references, a queue of posponed messages + and other metadata. It receives all process messages, + handles the system messages, and calls the callback module + with machine specific events. +
+
The
- In mode
-
-StateName(EventType, EventContent, Data) -> - ... code for actions here ... - {next_state, NewStateName, NewData}. --
- This form is used in most examples here for example in section
-
- In mode
-
-handle_event(EventType, EventContent, State, Data) -> - ... code for actions here ... - {next_state, NewState, NewData} --
- See section
-
- Both these modes allow other return tuples; see
-
+ See the section
+
+ The callback mode is selected by implementing a callback function
+
+ The
+
+ The short version: choose
The two
- The
-StateName(enter, _OldState, Data) -> - ... code for state entry actions here ... - {keep_state, NewData}; -StateName(EventType, EventContent, Data) -> - ... code for actions here ... - {next_state, NewStateName, NewData}.+
+ This form is the one mostly used in the
+
+ See section
+
- Depending on how your state machine is specified,
- this can be a very useful feature,
- but it forces you to handle the state enter calls in all states.
- See also the
-
+ State enter calls are also handled by the event handler and have
+ slightly different arguments. See the section
+
+ The event handler return values are defined in the description of
+
+ Set next state and update the server data.
+ If the
+ See section
+
+ If
+ Same as the
+ Same as the
+ Same as the
+ Stop the server with reason
+ Same as the
+ To decide the first state the
+
- In the first section
-
- For details, see the
-
Events are categorized in different
@@ -350,12 +531,20 @@ StateName(EventType, EventContent, Data) -> they come from:
+ The
+StateName(enter, OldState, Data) -> + ... code for state enter actions here ... + {keep_state, NewData}; +StateName(EventType, EventContent, Data) -> + ... code for actions here ... + {next_state, NewStateName, NewData}.+
+ Since the state enter call is not an event there are restrictions
+ on the allowed return value and
+
+ The first state that is entered will get a state enter call
+ with
+ You may repeat the state enter call using the
+ Depending on how your state machine is specified,
+ this can be a very useful feature,
+ but it forces you to handle the state enter calls in all states.
+ See also the
+
Say you have a state machine specification
- that uses state entry actions.
- Allthough you can code this using self-generated events
+ that uses state enter actions.
+ Allthough you can code this using inserted events
(described in the next section), especially if just
- one or a few states has got state entry actions,
+ one or a few states has got state enter actions,
this is a perfect use case for the built in
- You can repeat the state entry code by returning one of
+ You can repeat the state enter code by returning one of
It can sometimes be beneficial to be able to generate events
to your own state machine.
@@ -1279,14 +1543,18 @@ open(state_timeout, lock, Data) ->
One example for this is to pre-process incoming data, for example
decrypting chunks or collecting characters up to a line break.
+
Purists may argue that this should be modelled with a separate
state machine that sends pre-processed events
- to the main state machine.
- But to decrease overhead the small pre-processing state machine
+ to the main state machine,
+ but to decrease overhead the small pre-processing state machine
can be implemented in the common state event handling
of the main state machine using a few state data variables
that then sends the pre-processed events as internal events
to the main state machine.
+ Using internal events also can make it easier
+ to synchronize the state machines.
The following example uses an input model where you give the lock
@@ -1800,10 +2068,23 @@ handle_event(
Another not uncommon scenario is to use the event time-out
- to triger hibernation after a certain time of inactivity.
+ to trigger hibernation after a certain time of inactivity.
+ There is also a server start option
+
- This server probably does not use
+ This particular server probably does not use
heap memory worth hibernating for.
To gain anything from hibernation, your server would
have to produce some garbage during callback execution,
--
cgit v1.2.3
From e7f98a17f139865520e3f961e53d4a722b158223 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.
-
+ ]]>
+ The code is explained in the next sections. Function
Function
- The event is made into a message and sent to the
- 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
In state
@@ -986,9 +996,9 @@ open(state_timeout, lock, Data) ->
Consider a
+ Another way to do it is through a convenience macro
+
This example uses
@@ -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
+
- The following example uses an input model where you give the lock
- characters with
If you start this program with
Notice that this state diagram does not specify how to handle
a button event in the state
--
cgit v1.2.3
From 3ed7d729cab697b9f668dadb563d629de10f593d Mon Sep 17 00:00:00 2001
From: Raimo Niskanen If you in
If
Note that a state enter call will be done
@@ -723,12 +727,19 @@ handle_event(_, _, State, Data) ->
Transition options can be set by
- If the state changes, is the initial state,
+ If
+
Any
+ Should this state enter call return any of
+ the mentioned
Any event cancels an
@@ -826,7 +844,7 @@ handle_event(_, _, State, Data) ->
When a new message arrives the
+ If there are enqueued events to process
+ when hibrnation is requested,
+ this is optimized by not hibernating but instead calling
+
Note that it is not possible nor needed to cancel this time-out,
@@ -978,7 +1002,9 @@ handle_event(_, _, State, Data) ->
If
@@ -1004,7 +1030,9 @@ handle_event(_, _, State, Data) ->
Actions that set
-
Sets the
-
- Stores the specified
- Short for
- It replies to a caller waiting for a reply in
+ It does not set any
+
Note the fact that you can use
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl
index 34507bfd1c..f7dc0050b3 100644
--- a/lib/stdlib/src/gen_statem.erl
+++ b/lib/stdlib/src/gen_statem.erl
@@ -143,7 +143,7 @@
timeout_action() |
reply_action().
-type timeout_action() ::
- (Timeout :: event_timeout()) | % {timeout,Timeout}
+ (Time :: event_timeout()) | % {timeout,Time,Time}
{'timeout', % Set the event_timeout option
Time :: event_timeout(), EventContent :: term()} |
{'timeout', % Set the event_timeout option
@@ -327,7 +327,8 @@
%% Type validation functions
-compile(
{inline,
- [callback_mode/1, state_enter/1, from/1, event_type/1]}).
+ [callback_mode/1, state_enter/1,
+ event_type/1, from/1, timeout_event_type/1]}).
%%
callback_mode(CallbackMode) ->
case CallbackMode of
@@ -344,23 +345,26 @@ state_enter(StateEnter) ->
false
end.
%%
-from({Pid,_}) when is_pid(Pid) -> true;
-from(_) -> false.
-%%
-event_type({call,From}) ->
- from(From);
event_type(Type) ->
case Type of
{call,From} -> from(From);
+ %%
cast -> true;
info -> true;
- timeout -> true;
- state_timeout -> true;
internal -> true;
- {timeout,_} -> true;
- _ -> false
+ _ -> timeout_event_type(Type)
+ end.
+%%
+from({Pid,_}) when is_pid(Pid) -> true;
+from(_) -> false.
+%%
+timeout_event_type(Type) ->
+ case Type of
+ timeout -> true;
+ state_timeout -> true;
+ {timeout,_Name} -> true;
+ _ -> false
end.
-
-define(
@@ -1178,12 +1182,6 @@ loop_event_result(
[Event|Events])
end.
--compile({inline, [hibernate_in_trans_opts/1]}).
-hibernate_in_trans_opts(false) ->
- (#trans_opts{})#trans_opts.hibernate;
-hibernate_in_trans_opts(#trans_opts{hibernate = Hibernate}) ->
- Hibernate.
-
%% Ensure that Actions are a list
loop_event_actions(
Parent, Debug, S,
@@ -1216,10 +1214,16 @@ loop_event_actions_list(
S#state{
state = NextState,
data = NewerData,
- hibernate = TransOpts#trans_opts.hibernate},
+ hibernate = hibernate_in_trans_opts(TransOpts)},
[Event|Events])
end.
+-compile({inline, [hibernate_in_trans_opts/1]}).
+hibernate_in_trans_opts(false) ->
+ (#trans_opts{})#trans_opts.hibernate;
+hibernate_in_trans_opts(#trans_opts{hibernate = Hibernate}) ->
+ Hibernate.
+
parse_actions(false, Debug, S, Actions) ->
parse_actions(true, Debug, S, Actions, #trans_opts{});
parse_actions(TransOpts, Debug, S, Actions) ->
@@ -1335,15 +1339,15 @@ parse_actions_next_event(
parse_actions_timeout(
StateCall, Debug, S, Actions, TransOpts,
- {TimerType,Time,TimerMsg,TimerOpts} = AbsoluteTimeout) ->
+ {TimeoutType,Time,TimerMsg,TimerOpts} = AbsoluteTimeout) ->
%%
- case classify_timer(Time, listify(TimerOpts)) of
+ case classify_timeout(TimeoutType, Time, listify(TimerOpts)) of
absolute ->
parse_actions_timeout_add(
StateCall, Debug, S, Actions,
TransOpts, AbsoluteTimeout);
relative ->
- RelativeTimeout = {TimerType,Time,TimerMsg},
+ RelativeTimeout = {TimeoutType,Time,TimerMsg},
parse_actions_timeout_add(
StateCall, Debug, S, Actions,
TransOpts, RelativeTimeout);
@@ -1355,8 +1359,8 @@ parse_actions_timeout(
end;
parse_actions_timeout(
StateCall, Debug, S, Actions, TransOpts,
- {_,Time,_} = RelativeTimeout) ->
- case classify_timer(Time, []) of
+ {TimeoutType,Time,_} = RelativeTimeout) ->
+ case classify_timeout(TimeoutType, Time, []) of
relative ->
parse_actions_timeout_add(
StateCall, Debug, S, Actions,
@@ -1369,14 +1373,16 @@ parse_actions_timeout(
end;
parse_actions_timeout(
StateCall, Debug, S, Actions, TransOpts,
- Timeout) ->
- case classify_timer(Timeout, []) of
+ Time) ->
+ case classify_timeout(timeout, Time, []) of
relative ->
+ RelativeTimeout = {timeout,Time,Time},
parse_actions_timeout_add(
- StateCall, Debug, S, Actions, TransOpts, Timeout);
+ StateCall, Debug, S, Actions,
+ TransOpts, RelativeTimeout);
badarg ->
[error,
- {bad_action_from_state_function,Timeout},
+ {bad_action_from_state_function,Time},
?STACKTRACE(),
Debug]
end.
@@ -1662,10 +1668,15 @@ call_state_function(
%% -> absolute | relative | badarg
-classify_timer(Time, Opts) ->
- classify_timer(Time, Opts, false).
-%%
-classify_timer(Time, [], Abs) ->
+classify_timeout(TimeoutType, Time, Opts) ->
+ case timeout_event_type(TimeoutType) of
+ true ->
+ classify_time(false, Time, Opts);
+ false ->
+ badarg
+ end.
+
+classify_time(Abs, Time, []) ->
case Abs of
true when
is_integer(Time);
@@ -1678,9 +1689,9 @@ classify_timer(Time, [], Abs) ->
_ ->
badarg
end;
-classify_timer(Time, [{abs,Abs}|Opts], _) when is_boolean(Abs) ->
- classify_timer(Time, Opts, Abs);
-classify_timer(_, Opts, _) when is_list(Opts) ->
+classify_time(_, Time, [{abs,Abs}|Opts]) when is_boolean(Abs) ->
+ classify_time(Abs, Time, Opts);
+classify_time(_, _, Opts) when is_list(Opts) ->
badarg.
%% Stop and start timers as well as create timeout zero events
@@ -1711,15 +1722,7 @@ parse_timers(
{TimerType,Time,TimerMsg} ->
parse_timers(
TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
- TimerType, Time, TimerMsg, []);
- 0 ->
- parse_timers(
- TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
- timeout, zero, 0, []);
- Time ->
- parse_timers(
- TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
- timeout, Time, Time, [])
+ TimerType, Time, TimerMsg, [])
end.
parse_timers(
diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl
index 3f48fe1590..053233df9b 100644
--- a/lib/stdlib/test/gen_statem_SUITE.erl
+++ b/lib/stdlib/test/gen_statem_SUITE.erl
@@ -60,7 +60,8 @@ tcs(start) ->
tcs(stop) ->
[stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10];
tcs(abnormal) ->
- [abnormal1, abnormal1clean, abnormal1dirty, abnormal2];
+ [abnormal1, abnormal1clean, abnormal1dirty,
+ abnormal2, abnormal3, abnormal4];
tcs(sys) ->
[sys1, call_format_status,
error_format_status, terminate_crash_format,
@@ -524,6 +525,43 @@ abnormal2(Config) ->
process_flag(trap_exit, OldFl),
ok = verify_empty_msgq().
+%% Check that bad return actions makes the stm crash. Note that we must
+%% trap exit since we must link to get the real bad_return_ error
+abnormal3(Config) ->
+ OldFl = process_flag(trap_exit, true),
+ {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []),
+
+ %% bad return value in the gen_statem loop
+ {{{bad_action_from_state_function,badaction},_},_} =
+ ?EXPECT_FAILURE(gen_statem:call(Pid, badaction), Reason),
+ receive
+ {'EXIT',Pid,{{bad_action_from_state_function,badaction},_}} -> ok
+ after 5000 ->
+ ct:fail(gen_statem_did_not_die)
+ end,
+
+ process_flag(trap_exit, OldFl),
+ ok = verify_empty_msgq().
+
+%% Check that bad timeout actions makes the stm crash. Note that we must
+%% trap exit since we must link to get the real bad_return_ error
+abnormal4(Config) ->
+ OldFl = process_flag(trap_exit, true),
+ {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []),
+
+ %% bad return value in the gen_statem loop
+ BadTimeout = {badtimeout,4711,ouch},
+ {{{bad_action_from_state_function,BadTimeout},_},_} =
+ ?EXPECT_FAILURE(gen_statem:call(Pid, BadTimeout), Reason),
+ receive
+ {'EXIT',Pid,{{bad_action_from_state_function,BadTimeout},_}} -> ok
+ after 5000 ->
+ ct:fail(gen_statem_did_not_die)
+ end,
+
+ process_flag(trap_exit, OldFl),
+ ok = verify_empty_msgq().
+
shutdown(Config) ->
process_flag(trap_exit, true),
@@ -1806,10 +1844,12 @@ idle(cast, {connect,Pid}, Data) ->
idle({call,From}, connect, Data) ->
gen_statem:reply(From, accept),
{next_state,wfor_conf,Data,infinity}; % NoOp timeout just to test API
-idle(cast, badreturn, _Data) ->
- badreturn;
idle({call,_From}, badreturn, _Data) ->
badreturn;
+idle({call,_From}, badaction, Data) ->
+ {keep_state, Data, [badaction]};
+idle({call,_From}, {badtimeout,_,_} = BadTimeout, Data) ->
+ {keep_state, Data, BadTimeout};
idle({call,From}, {delayed_answer,T}, Data) ->
receive
after T ->
diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml
index 5269d23487..b44e169a9a 100644
--- a/system/doc/design_principles/statem.xml
+++ b/system/doc/design_principles/statem.xml
@@ -67,8 +67,8 @@ State(S) x Event(E) -> Actions(A), State(S')
As
Like most
See section
-
@@ -401,8 +404,8 @@ State(S) x Event(E) -> Actions(A), State(S')
In the first
There are more specific state-transition actions
- that a callback function can order the
Since the state enter call is not an event there are restrictions
on the allowed return value and
-
In state
10,000 is a time-out value in milliseconds.
After this time (10 seconds), a time-out occurs.
@@ -1024,7 +1028,7 @@ handle_common({call,From}, code_length, #{code := Code} = Data) ->
Another way to do it is through a convenience macro
-
This example uses
@@ -1059,6 +1063,14 @@ open(...) -> ... ;
when you want to stay in the current state but do not know or
care about what it is.
+ If the common event handler needs to know the current state
+ a function
When ordered to shut down, the
This makes the
It is ordered by the state transition action
-
- This type of time-out is useful to for example act on inactivity.
+ This type of time-out is useful for example to act on inactivity.
Let us restart the code sequence
if no button is pressed for say 30 seconds:
Whenever we receive a button event we start an event time-out
of 30 seconds, and if we get an event type
Here is how to accomplish the state time-out
in the previous example by instead using a generic time-out
- named
- Just as
-
- Another way to handle a late time-out can be to not cancel it,
- but to ignore it if it arrives in a state
- where it is known to be late.
+ In this particular case we do not need to cancel the timeout
+ since the timeout event is the only possible reason to
+ change the state from
+ Instead of bothering with when to cancel a time-out,
+ a late time-out event can be handled by ignoring it
+ if it arrives in a state where it is known to be late.
The most versatile way to handle time-outs is to use
Erlang Timers; see
-
Removing the
Postponing is ordered by the state transition
-
@@ -1392,14 +1411,17 @@ open(cast, {button,_}, Data) ->
open(cast, {button,_}, Data) ->
{keep_state,Data,[postpone]};
...
- ]]>
+]]>
Since a postponed event is only retried after a state change,
you have to think about where to keep a state data item.
You can keep it in the server
- The state transition
-
You can repeat the state enter code by returning one of
It can sometimes be beneficial to be able to generate events
to your own state machine.
- This can be done with the state transition
-
@@ -1643,10 +1669,10 @@ locked(
internal, {button,Digit},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
- ]]>
+]]>
If you start this program with
Notice that this state diagram does not specify how to handle
a button event in the state
- Notice that postponing buttons from the
Suppose now that we call
- So we make the
- If a process now calls
+ We could say that the button was pressed too early
+ so it is not to be recognized as the lock button.
+ Or we can make the lock button part of the state so
+ when we then change the lock button in the locked state,
+ the change becomes a state change
+ and all postponed events are retried,
+ therefore the lock is immediately locked!
We define the state as
- It can be an ill-fitting model for a physical code lock
- that the
The atom
To change that we would need to insert
action
@@ -2201,7 +2180,8 @@ handle_event(
This particular server probably does not use
heap memory worth hibernating for.
To gain anything from hibernation, your server would
- have to produce some garbage during callback execution,
+ have to produce non-insignificant garbage
+ during callback execution,
for which this example server can serve as a bad example.
+ If your process logic is convenient to describe as a state machine,
+ and you want any of these
+ If so, or if possibly needed in future versions,
+ then you should consider using
+ For simple state machines not needing these fetures
+
The two
-
The mode enables the use of non-atom states, for example,
complex states or even hierarchical states.
+ See section
+
Same as the
- In the first
-
Since the state enter call is not an event there are restrictions
on the allowed return value and
-
You may repeat the state enter call using the
@@ -1301,8 +1371,8 @@ open(cast, {button,_}, Data) ->
...
]]>
- An specific generic time-out can just as a
-
Postponing is ordered by the state transition
The
-
You return a list containing
One reason to use this is when you have a state item
that when changed should cancel the
-
- Another not uncommon scenario is to use the event time-out
+ Another not uncommon scenario is to use the
+
- For simple state machines not needing these fetures
+ For simple state machines not needing these features
@@ -46,9 +46,15 @@
To get an overview of the concepts and operation of
@@ -67,7 +73,7 @@
Any
- These relations are interpreted as follows:
if we are in state
As
- Events are handled by one callback functions per state.
+ Events are handled by one callback function per state.
- The callback mode is selected by implementing a callback function
+ The callback mode is selected by implementing
+ a mandatory callback function
The two
This can be done, for example, by focusing on one state at the time
@@ -386,7 +388,7 @@ State(S) x Event(E) -> Actions(A), State(S')
Same as the
Same as the
+
+
+
-
+
- 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}.
]]>
do_lock(),
- Data = #{code => Code, remaining => Code},
- {ok,locked,Data}.
+ Data = #{code => Code, length => length(Code), buttons => []},
+ {ok, locked, Data}.
]]>
- state_functions.
- ]]>
+ state_functions.
+ ]]>
@@ -884,7 +890,7 @@ button(Digit) ->
- 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}.
]]>
+ {next_state, open, Data}.
+ ]]>
...
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)}]}.
]]>
+
+
+ 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.
+ ]]>
+
- {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}
...
]]>
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.
+
- 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]};
+...
]]>
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]};
...
]]>
-
@@ -232,8 +234,10 @@ erlang:'!' -----> Module:StateName/3
whenever a new state is entered; see
@@ -851,13 +869,19 @@ handle_event(_, _, State, Data) ->
io:format("Lock~n", []).
do_unlock() ->
@@ -925,7 +929,7 @@ locked(
+ ]]>
code_length() ->
gen_statem:call(?NAME, code_length).
--define(HANDLE_COMMON(T, C, D),
- ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))).
+-define(HANDLE_COMMON,
+ ?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)}]}.
@@ -1047,7 +1051,7 @@ locked(...) -> ... ;
...
open(...) -> ... ;
?HANDLE_COMMON.
- ]]>
+]]>
handle_common(T, C, ?FUNCTION_NAME, D)).
+ ]]>
% Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
- [{{timeout,open_tm},10000,lock}]};
+ [{{timeout,open},10000,lock}]};
...
-open({timeout,open_tm}, lock, Data) ->
+open({timeout,open}, lock, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
- ]]>
+]]>
- {keep_state, Data#{button := Button};
+ {keep_state, Data#{button := Button}};
handle_common(cast, {up,Button}, Data) ->
case Data of
#{button := Button} ->
@@ -1660,7 +1686,7 @@ handle_common(cast, {up,Button}, Data) ->
open(internal, {button,_}, Data) ->
{keep_state,Data,[postpone]};
...
- ]]>
+]]>
process_flag(trap_exit, true),
- Data = #{code => Code, length => Length, buttons => []},
+ Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
callback_mode() ->
[state_functions,state_enter].
-define(HANDLE_COMMON,
- ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))).
+ ?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%%
handle_common(cast, {down,Button}, Data) ->
{keep_state, Data#{button => Button}};
@@ -1763,14 +1789,13 @@ locked(
if
NewButtons =:= Code -> % Correct
do_unlock(),
- {next_state, open, Data,
- [{state_timeout,10000,lock}]};
+ {next_state, open, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30000,button}]}
end;
?HANDLE_COMMON.
- ]]>
+]]>
do_unlock(),
@@ -1789,7 +1814,7 @@ do_unlock() ->
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
- ]]>
+ ]]>
+
[handle_event_function,state_enter].
-
+ ]]>
+
do_lock(),
@@ -1829,14 +1855,13 @@ handle_event(
if
NewButtons =:= Code -> % Correct
do_unlock(),
- {next_state, open, Data,
- [{state_timeout,10000,lock}]};
+ {next_state, open, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30000,button}]}
end;
]]>
-
@@ -1844,12 +1869,11 @@ handle_event(enter, _OldState, open, _Data) ->
{keep_state_and_data, [{state_timeout,10000,lock}]};
handle_event(state_timeout, lock, open, Data) ->
{next_state, locked, Data};
-handle_event(cast, {button,_}, open, _) ->
+handle_event(internal, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
]]>
-
{keep_state, Data#{button => Button}};
handle_event(cast, {up,Button}, _State, Data) ->
@@ -1862,13 +1886,11 @@ handle_event(cast, {up,Button}, _State, Data) ->
end;
handle_event({call,From}, code_length, _State, #{length := Length}) ->
{keep_state_and_data, [{reply,From,Length}]}.
-
-...
- ]]>
+ ]]>
code_lock:start_link([a,b,c], x).
+{ok,<0.666.0>}
+2> code_lock:button(a).
+ok
+3> code_lock:button(b).
+ok
+4> code_lock:button(c).
+ok
+Open
+5> code_lock:button(y).
+ok
+6> code_lock:set_lock_button(y).
+x
+% What should happen here? Immediate lock or nothing?
+]]>
+
- case {EventType, EventContent} of
- {enter, _OldState} ->
- do_lock(),
- {keep_state, Data#{buttons := []}};
- {state_timeout, button} ->
- {keep_state, Data#{buttons := []}};
- {{call,From}, {button,Digit}} ->
- #{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}]};
- #{} ->
- {keep_state, Data#{buttons := NewButtons},
- [{reply,From,ok},
- {state_timeout,30000,button}]}
- end
- end;
+%% State: open
+handle_event(enter, _OldState, {open,_}, _Data) ->
+ do_unlock(),
+ {keep_state_and_data,
+ [{state_timeout,10000,lock}]};
+handle_event(state_timeout, lock, {open,_}, Data) ->
+ {next_state, locked, Data};
+handle_event(cast, {button,LockButton}, {open,LockButton}, Data) ->
+ {next_state, {locked,LockButton}, Data};
+handle_event(cast, {button,_}, {open,_}, Data) ->
+ {keep_state_and_data,[postpone]};
]]>
- case {EventType, EventContent} of
- {enter, _OldState} ->
- do_unlock(),
- {keep_state_and_data,
- [{state_timeout,10000,lock}]};
- {state_timeout, lock} ->
- {next_state, {locked,LockButton}, Data};
- {{call,From}, {button,Digit}} ->
- if
- Digit =:= LockButton ->
- {next_state, {locked,LockButton}, Data,
- [{reply,From,locked}]};
- true ->
- {keep_state_and_data,
- [postpone]}
- end
- end.
+ {call,From}, {set_lock_button,NewLockButton},
+ {StateName,OldLockButton}, Data) ->
+ {next_state, {StateName,NewLockButton}, Data,
+ [{reply,From,OldLockButton}]}.
]]>
@@ -2099,27 +2102,7 @@ do_unlock() ->
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
-format_status(Opt, [_PDict,State,Data]) ->
- StateData =
- {State,
- maps:filter(
- fun (code, _) -> false;
- (remaining, _) -> false;
- (_, _) -> true
- end,
- Data)},
- case Opt of
- terminate ->
- StateData;
- normal ->
- [{data,[{"State",StateData}]}]
- end.
]]>
-
- case {EventType, EventContent} of
- {enter, _OldState} ->
- do_unlock(),
- {keep_state_and_data,
- [{state_timeout,10000,lock},
- hibernate]};
+handle_event(enter, _OldState, {open,_}, _Data) ->
+ do_unlock(),
+ {keep_state_and_data,
+ [{state_timeout,10000,lock},
+ hibernate]};
...
- ]]>
+]]>
+
+
-
@@ -749,11 +755,11 @@ handle_event(_, _, State, Data) ->