Erlang -- gen_statem

gen_statem

gen_statem是Erlang/OTP 19.0引入的新behavior。用于替换gen_fsm

gen_statem支持2种回调模式:

  1. state_functions
    要求状态名StateName必须是原子,并且回调函数的名称与状态同名。这一点与gen_fsm一样。
  2. handle_event_function
    状态名可以是任何形式,此时回调函数为Module:handle_event/4,适用于所有状态下。

回调函数Module:callback_mode()指定gen_statem的回调模式。其返回值为state_functions|handle_event_function|[state_functions, state_enter]|[handle_event_function, state_enter]

callback_mode/0函数的返回列表中加入state_enter,会在每次状态改变的时候调用回调函数,参数为(enter, OldState, ...)调用一次回调函数,即进入状态回调。

behavior函数和回调函数之间的关系如下:

gen_statem module            Callback module
-----------------            ---------------
gen_statem:start
gen_statem:start_link -----> Module:init/1

Server start or code change
                      -----> Module:callback_mode/0

gen_statem:stop       -----> Module:terminate/3

gen_statem:call
gen_statem:cast
erlang:send
erlang:'!'            -----> Module:StateName/3,Module:handle_event/3 <- (state_functions)
                             Module:handle_event/4 <- (handle_event_function)

-                     -----> Module:terminate/3

-                     -----> Module:code_change/4

可以通过gen_statem:call(ServerRef, Request)gen_statem:call(ServerRef, Request, Timeout)发送一个同步消息即事件{call,From}gen_statem进程。
可以通过gen_statem:cast(ServerRef, Request)发送一个异步消息即事件castgen_statem进程。
可以通过erlang:send()发送一个消息即事件infogen_statem进程。

gen_statem回调函数:

  • state_functions模式下:
Module:StateName(enter, OldState, Data) -> StateEnterResult(StateName)
Module:StateName(EventType, EventContent, Data) -> StateFunctionResult
  • handle_event_function模式下:
Module:handle_event(enter, OldState, StateName, Data) -> StateEnterResult(StateName)
Module:handle_event(EventType, EventContent, StateName, Data) -> HandleEventResult

回调函数返回值格式如下:

  • 进入状态回调返回值
StateEnterResult(StateName) ->
    {next_state, StateName, NewData} |
    {next_state, StateName, NewData, Actions} |
    {keep_state, NewData} |
    {keep_state, NewData, Actions} |
    keep_state_and_data |
    {keep_state_and_data, Actions} |
    {repeat_state, NewData} |
    {repeat_state, NewData, Actions} |
    repeat_state_and_data |
    {repeat_state_and_data, Actions}    
  • 常规回调函数返回值
StateFunctionResult ->
    {next_state, NewStateName, NewData} |
    {next_state, NewStateName, NewData, Actions} |
    {keep_state, NewData} |
    {keep_state, NewData, Actions} |
    keep_state_and_data |
    {keep_state_and_data, Actions} |
    {repeat_state, NewData} |
    {repeat_state, NewData, Actions} |
    repeat_state_and_data |
    {repeat_state_and_data, Actions}

其中,Actions表示状态迁移动作,在回调函数返回后指定gen_statem去执行,主要包含:

[postpone |
 {postpone, true | false} |
 {next_event, EventType :: event_type(), EventContent :: term()} |
 hibernate |
 {hibernate, true | false} |
 Timeout |
 {timeout, Time, EventContent} |
 {timeout, Time, EventContent, Options} |
 {{timeout, Name}, Time, EventContent} |
 {{timeout, Name}, Time, EventContent, Options} |
 {state_timeout, Time, EventContent} |
 {state_timeout, Time, EventContent, Options} |
 {reply, From, Reply}]

其中,

  • postpone表示在当前状态下延缓事件,等待状态改变时再重新触发;
  • hibernate表示挂起gen_statem进程直至下一个事件到来;
  • TimeOut{timeout, Time, EventContent}{timeout, Time, EventContent, Options}表示事件超时,一定时间内没有事件达到就会触发该超时。事件超时的定时器在有事件达到的时候就会被取消。超时后会触发事件timeout
  • {{timeout, Name}, Time, EventContent}{{timeout, Name}, Time, EventContent, Options}表示一般超时,一定时间后触发,与状态改变、事件更新无关。超时后会触发事件{timeout, Name}。可以通过设置一个同名的超时动作来重启这个定时器,如果想要终止这个定时器只需要将超时改成infinity即可;
  • {state_timeout, Time, EventContent}{state_timeout, Time, EventContent, Options}表示状态超时,一定时间内状态没有变化就会触发该超时。状态超时会在状态改变时被取消。超时后会触发事件state_timeout
  • {next_event, EventType, EventContent :: term()}表示像事件队列头部插入一个事件,该事件将会被优先处理,可用于触发所有事件。{next_event, internal, EventContent}触发的事件为internal,表示内部事件,用于区分外部事件;
  • {reply, From, Reply}用于给From返回值。除此之后也可以通过调用gen_statem:reply(From, Reply)进行返回。

需要注意的是,在Actions列表中的后面的Action是会覆盖前面的同类型的Action,例如,列表中的所有事件超时只有最后一个会生效。

EventType事件类型包含:

{call, From} | cast | info | timeout | {timeout, Name} | state_timeout | internal

{call, From}castinfo这3种事件是通过接口产生的外部事件,其他的则是在执行过程中由gen_statem产生的。

  • castgen_statem:cast(ServerRef, Msg)生成,其中Msg会传递给参数EventContent
  • {call,From}gen_statem:call(ServerRef, Request)生成,其中Request会传递给参数EventContentFrom是通过迁移动作{reply,From,Reply}或在回调模块中调用gen_statem:reply(From, Reply)进行回复时的回复地址。
  • info由任何发送到gen_statem进程的普通消息生成。进程消息会传递给参数EventContent
  • state_timeout由迁移动作{state_timeout,Time,EventContent}状态定时器超时生成。
  • {timeout,Name}由迁移动作{{timeout,Name},Time,EventContent}一般定时器超时生成。
  • timeout由迁移动作{timeout,Time,EventContent}(或者它的缩写形式Time)事件定时器超时生成。
  • internal由迁移动作{next_event,internal,EventContent}生成。上面所有事件类型都可以通过使用next_event动作:{next_event,EventType,EventContent}生成。
一个例子,分别使用两种回调模式实现
  • 回调模式为state_function
-module(pushbutton). 
-behaviour(gen_statem). 
-export([start/0,push/0,get_count/0,stop/0]). 
-export([terminate/3,code_change/4,init/1,callback_mode/0]). 
-export([on/3,off/3]). 
name() -> pushbutton_statem. 
% The registered server name 
%% API. This example uses a registered name name() 
%% and does not link to the caller. 
start() -> 
    gen_statem:start({local,name()}, ?MODULE, [], []). 
push() -> 
    gen_statem:call(name(), push). 
get_count() -> 
    gen_statem:call(name(), get_count). 
stop() -> 
    gen_statem:stop(name()). 
%% Mandatory callback functions 
terminate(_Reason, _State, _Data) -> 
    void. 
code_change(_Vsn, State, Data, _Extra) -> 
    {ok,State,Data}. 
init([]) -> 
    %% Set the initial state + data. Data is used only as a counter.
    State = off, Data = 0, 
    {ok,State,Data}. 
callback_mode() -> 
    state_functions. 
%%% state callback(s) 
off({call,From}, push, Data) -> 
    %% Go to 'on', increment count and reply 
    %% that the resulting status is 'on' 
    {next_state,on,Data+1,[{reply,From,on}]}; 
off(EventType, EventContent, Data) -> 
    handle_event(EventType, EventContent, Data). 
on({call,From}, push, Data) -> 
    %% Go to 'off' and reply that the resulting status is 'off' 
    {next_state,off,Data,[{reply,From,off}]}; 
on(EventType, EventContent, Data) -> 
    handle_event(EventType, EventContent, Data). 
%% Handle events common to all states 
handle_event({call,From}, get_count, Data) -> 
    %% Reply with the current count 
    {keep_state,Data,[{reply,From,Data}]}; 
handle_event(_, _, Data) -> 
    %% Ignore all other events 
    {keep_state,Data}.
  • 回调模式为handle_event_function
-module(pushbutton). 
-behaviour(gen_statem). 
-export([start/0,push/0,get_count/0,stop/0]). 
-export([terminate/3,code_change/4,init/1,callback_mode/0]). 
-export([on/3,off/3]). 
name() -> pushbutton_statem. 
% The registered server name 
%% API. This example uses a registered name name() 
%% and does not link to the caller. 
start() -> 
    gen_statem:start({local,name()}, ?MODULE, [], []). 
push() -> 
    gen_statem:call(name(), push). 
get_count() -> 
    gen_statem:call(name(), get_count). 
stop() -> 
    gen_statem:stop(name()). 
%% Mandatory callback functions 
terminate(_Reason, _State, _Data) -> 
    void. 
code_change(_Vsn, State, Data, _Extra) -> 
    {ok,State,Data}. 
init([]) -> 
    %% Set the initial state + data. Data is used only as a counter.
    State = off, Data = 0, 
    {ok,State,Data}. 
callback_mode() -> 
    handle_event_function. 
%%% state callback(s) 
handle_event({call,From}, push, off, Data) -> 
    %% Go to 'on', increment count and reply 
    %% that the resulting status is 'on' 
    {next_state,on,Data+1,[{reply,From,on}]}; 
handle_event({call,From}, push, on, Data) -> 
    %% Go to 'off' and reply that the resulting status is 'off' 
    {next_state,off,Data,[{reply,From,off}]}; 
%% %% Event handling common to all states 
handle_event({call,From}, get_count, State, Data) -> 
    %% Reply with the current count 
    {next_state,State,Data,[{reply,From,Data}]}; 
handle_event(_, _, State, Data) -> 
    %% Ignore all other events 
    {next_state,State,Data}.

官方文档连接:http://erlang.org/doc/design_principles/statem.html
更详细的翻译及讲解可见:https://www.cnblogs.com/-wyp/p/6892632.html#_label2

2021/05/31 补充
  1. OTP 22.3新增可以通过迁移动作{change_callback_module, NewModule}{push_callback_module, NewModule}
    pop_callback_modulegen_statem运行时改变回调模块。上述动作不能在状态进入回调中使用。

  2. 回调模式是回调模块的属性,在服务器启动时设置,并且可以在回调模块改变时改变。

  3. 返回值:repeat_state vs keep_state
    开启进入状态回调的情况下,repeat_state会再次执行进入状态回调,相当于再次进入该状态;而keep_state则不会。

  4. init函数的返回值中使用posepone动作是无效的,因为当前没有任何事件可以延迟。

  5. 状态进入回调并非一个事件,不能在其返回值中改变状态、插入事件、延迟enter事件或者改变回调模块。

  6. 启动相同类型(state_timeout{time_out, Name}timeout)的超时定时器会取消旧的正在运行的定时器,相当于重新开启一个定时器。同名但事件内容不同的定时器相当于同一个定时器。
    OTP 22.1之前,可通过设置一个同类型的超时时间为infinity的定时器来取消超时。
    OTP 22.1之后,可通过{TimoutType, update, NewEventContent}来更新超时事件的内容,通过{TimoutType, cancel}来取消超时定时器。
    零超时计时器不会被启动,但会即刻插入超时事件。

测试代码
  • code_lock.erl
-module(code_lock).
-behaviour(gen_statem).
-define(NAME, code_lock).

-export([start_link/1, stop/0]).
-export([down/1, up/1, code_length/0]).
%% Test
-export([
    change_callback_module/0
    ,push_callback_module/0
    ,pop_callback_module/0
    ,next_state/0
    ,keep_state/0
    ,repeat_state/0
    ,stop/1
    ,stop_and_reply/1
    ,state_timeout/0
    ,timeout/1
    ,timeout_update/1
    ,generic_timeout/3
    ,generic_timeout_update/2
    ,generic_timeout_cancel/1
    ,erlang_timeout/2
    ,erlang_timeout_cancel/1
    ,postpone/1
]).
-export([init/1, callback_mode/0, terminate/3]).
-export([locked/3, open/3]).

start_link(Code) ->
    gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
stop() ->
    gen_statem:stop(?NAME).

down(Button) ->
    gen_statem:cast(?NAME, {down, Button}).
up(Button) ->
    gen_statem:cast(?NAME, {up, Button}).
code_length() ->
    gen_statem:call(?NAME, code_length).

%% Just For Test
%% 更改回调模块 Since OTP22.3
change_callback_module() ->
    gen_statem:cast(?NAME, change_callback_module).
%%% 更改为新的回调模块,将旧的回调模块入栈
push_callback_module() ->
    gen_statem:cast(?NAME, push_callback_module).
%%% 从栈中取出旧的回调模块并设置为当前回调模块
pop_callback_module() ->
    gen_statem:cast(?NAME, pop_callback_module).
%% 返回值 
%%% next_state vs keep_state vs repeat_state
%%%% 开启进入状态回调的情况下,repeat_state会再次执行进入状态回调,相当于再次进入该状态;而keep_state则不会。
next_state() ->
    gen_statem:cast(?NAME, next_state).
keep_state() ->
    gen_statem:cast(?NAME, keep_state).
repeat_state() ->
    gen_statem:cast(?NAME, repeat_state).
%%% 返回值 stop & stop_and_reply
stop(Reason) ->
    gen_statem:cast(?NAME, {stop, Reason}).
stop_and_reply(Reason) ->
    gen_statem:call(?NAME, {stop_and_reply, Reason}).
%% timeout
%%% state_timeout 状态超时
%%%% 按下按钮(down(1)、up(1))后,调用state_timeout,测试是否会覆盖按下按钮后的状态超时{state_timeout, button}
%%%% 结果证明会被覆盖
state_timeout() ->
    gen_statem:cast(?NAME, state_timeout).
%%% timeout 事件超时
%%%% 执行timeout(20000)、timeout(10000),查看是否覆盖
timeout(MiliS) ->
    gen_statem:cast(?NAME, {timeout, MiliS}).
%%%% 执行timeout(20000)、timeout_update(aaa),查看内容是否更改
%%%% 测试没有更改,因为此处是事件超时,调用后事件超时就被触发了,无法测试,改用一般超时进行测试
timeout_update(Msg) ->
    gen_statem:cast(?NAME, {timeout_update, Msg}).
%%% generic_timeout 一般超时
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout(aaa, 5000, aaa)查看是否覆盖
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout(bbb, 10000, aaa)查看是否覆盖
generic_timeout(Name, MiliS, Msg) ->
    gen_statem:cast(?NAME, {generic_timeout, Name, MiliS, Msg}).
%%%% Since OTP22.1
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout_update(aaa, aaaaa)查看是否更新
generic_timeout_update(Name, Msg) ->
    gen_statem:cast(?NAME, {generic_timeout_update, Name, Msg}).
%%%% Since OTP22.1
%%%% 执行generic_timeout(aaa, 10000, aaa)、generic_timeout_cancel(aaa)查看是否取消
generic_timeout_cancel(Name) ->
    gen_statem:cast(?NAME, {generic_timeout_cancel, Name}).
%%% erlang定时器
erlang_timeout(MiliS, Msg) ->
    gen_statem:cast(?NAME, {erlang_timeout, MiliS, Msg}).
erlang_timeout_cancel(Msg) ->
    gen_statem:cast(?NAME, {erlang_timeout_cancel, Msg}).
%% postpone事件延迟
%%%% locked状态下调用postpone(aaa)、postpone(bbb),然后在调用next_state(),延迟的事件会再次触发
postpone(Msg) ->
    gen_statem:cast(?NAME, {postpone, Msg}).
%% next_event插入事件
%%%% 执行down(1)、up(1)
%% hibernation进程挂起

init(Code) ->
    process_flag(trap_exit, true),
    Data = #{code => Code, length => length(Code), buttons => [], timer => []},
    {ok, locked, Data}.

callback_mode() ->
    [state_functions, state_enter].

-define(HANDLE_COMMON,
    ?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%% All State Events
handle_common(cast, {down, Button}, Data) ->
    io:format("code_lock:handle_common cast {down, ~p}~n", [Button]),
    {keep_state, Data#{button => Button}};
handle_common(cast, {up, Button}, Data) ->
    io:format("code_lock:handle_common cast {up, ~p}~n", [Button]),
    case Data of
        #{button := Button} ->
            {keep_state, maps:remove(button, Data),
             [{next_event, internal, {button, Button}}]};
        #{} ->
            keep_state_and_data
    end;
handle_common(cast, change_callback_module, _Data) ->
    io:format("code_lock:change_callback_module -> code_lock_2~n", []),
    {keep_state_and_data, [{change_callback_module, code_lock_2}]};
handle_common(cast, push_callback_module, _Data) ->
    io:format("code_lock:push_callback_module -> code_lock_2~n", []),
    {keep_state_and_data, [{push_callback_module, code_lock_2}]};
handle_common(cast, pop_callback_module, _Data) ->
    io:format("code_lock:pop_callback_module~n", []),
    {keep_state_and_data, [pop_callback_module]};
handle_common(cast, keep_state, _Data) ->
    io:format("code_lock:keep_state~n", []),
    {keep_state, _Data};
handle_common(cast, repeat_state, _Data) ->
    io:format("code_lock:repeat_state~n", []),
    {repeat_state, _Data};
handle_common(cast, {stop, Reason}, _Data) ->
    io:format("code_lock:stop Reson:~p~n", [Reason]),
    {stop, Reason};
handle_common(cast, {timeout, MiliS}, _Data) ->
    io:format("code_lock:cast, {timeout, ~p}~n", [MiliS]),
    {keep_state_and_data, {timeout, MiliS, MiliS}};
handle_common(cast, {timeout_update, Msg}, _Data) ->
    io:format("code_lock:cast, {timeout_update, ~p}~n", [Msg]),
    {keep_state_and_data, {timeout, update, Msg}};
handle_common(cast, timeout_cancel, _Data) ->
    io:format("code_lock:cast, timeout_cancel~n", []),
    {keep_state_and_data, {timeout, cancel}};
handle_common(cast, {generic_timeout, Name, MiliS, Msg}, _Data) ->
    io:format("code_lock:cast, generic_timeout Name:~p, MiliS:~p, Msg:~p~n", [Name, MiliS, Msg]),
    {keep_state_and_data, {{timeout, Name}, MiliS, Msg}};
handle_common(cast, {generic_timeout_update, Name, Msg}, _Data) ->
    io:format("code_lock:cast, generic_timeout_update Name:~p, Msg:~p~n", [Name, Msg]),
    {keep_state_and_data, {{timeout, Name}, update, Msg}};
handle_common(cast, {generic_timeout_cancel, Name}, _Data) ->
    io:format("code_lock:cast, generic_timeout_cancel Name:~p~n", [Name]),
    {keep_state_and_data, {{timeout, Name}, cancel}};
handle_common(cast, {erlang_timeout, MiliS, Msg}, #{timer := TimerList} = Data) ->
    io:format("code_lock:cast, erlang_timeout MiliS:~p Msg:~p~n", [MiliS, Msg]),
    Ref = erlang:start_timer(MiliS, self(), Msg),
    NewTimerList = lists:keystore(Msg, 1, TimerList, {Msg, Ref}),
    {keep_state, Data#{timer := NewTimerList}};
handle_common(cast, {erlang_timeout_cancel, Msg}, #{timer := TimerList} = Data) ->
    io:format("code_lock:cast, erlang_timeout_cancel Msg:~p~n", [Msg]),
    case lists:keyfind(Msg, 1, TimerList) of
        {Msg, Ref} ->
            erlang:cancel_timer(Ref),
            NewTimerList = lists:keydelete(Msg, 1, TimerList);
        _ ->
            NewTimerList = TimerList
    end,
    {keep_state, Data#{timer := NewTimerList}};
handle_common({call,From}, {stop_and_reply, Reason}, _Data) ->
    io:format("code_lock:stop_and_reply Reson:~p~n", [Reason]),
    {stop_and_reply, Reason, {reply, From, Reason}};
handle_common({call,From}, code_length, #{code := Code}) ->
    {keep_state_and_data,
     [{reply,From,length(Code)}]};
handle_common(timeout, Time, _Data) ->
    io:format("code_lock:timeout Time:~pms~n", [Time]),
    keep_state_and_data;
handle_common({timeout, Name}, Msg, _Data) ->
    io:format("code_lock:generic timeout Name:~p Msg:~p~n", [Name, Msg]),
    keep_state_and_data;
handle_common(info, {timeout, Ref, Msg}, #{timer := TimerList} = Data) ->
    case lists:member({Msg, Ref}, TimerList) of
        true ->
            io:format("code_lock:info hit, timeout Msg:~p~n", [Msg]),
            NewTimerList = lists:keydelete(Msg, 1, TimerList);
        _ ->
            io:format("code_lock:info miss, timeout Msg:~p~n", [Msg]),
            NewTimerList = TimerList
    end,
    {keep_state, Data#{timer := NewTimerList}}.

%% State: locked
%% bad_state_enter_return_from_state_function
% locked(enter, _OldState, Data) ->
%     io:format("code_lock:enter lock~n", []),
%     do_lock(),
%     {next_state, open, Data};%% 状态进入回调不能改变状态
locked(enter, _OldState, Data) ->
    io:format("code_lock:enter lock~n", []),
    do_lock(),
    {keep_state, Data#{buttons := []}};
locked(state_timeout, button, Data) ->
    io:format("code_lock:locked state_timeout button~n", []),
    {keep_state, Data#{buttons := []}};
locked(state_timeout, cast, Data) ->
    io:format("code_lock:locked state_timeout cast~n", []),
    {keep_state, Data};
locked(
  internal, {button,Button},
  #{code := Code, length := Length, buttons := Buttons} = Data) ->
    io:format("code_lock:locked internal~n", []),
    NewButtons =
        if
            length(Buttons) < Length ->
                Buttons;
            true ->
                tl(Buttons)
        end ++ [Button],
    if
        NewButtons =:= Code -> % Correct
            {next_state, open, Data};
    true -> % Incomplete | Incorrect
            {keep_state, Data#{buttons := NewButtons},
             [{state_timeout,30000,button}]} %% 每次按下按钮都会重新计时
    end;
locked(cast, next_state, Data) ->
    io:format("code_lock:next_state open~n", []),
    {next_state, open, Data};
locked(cast, state_timeout, Data) ->
    io:format("code_lock:cast state_timeout locked~n", []),
    {keep_state, Data, [{state_timeout,10000,cast}]};
locked(cast, {postpone, Msg}, Data) ->
    io:format("code_lock:cast locked postpone Msg:~p~n", [Msg]),
    {keep_state, Data, [postpone]};
?HANDLE_COMMON.

%% State: open
open(enter, _OldState, _Data) ->
    io:format("code_lock:enter open~n", []),
    do_unlock(),
    {keep_state_and_data,
     [{state_timeout,10000,lock}]}; % Time in milliseconds
open(state_timeout, lock, Data) ->
    io:format("code_lock:open state_timeout lock~n", []),
    {next_state, locked, Data};
open(state_timeout, cast, Data) ->
    io:format("code_lock:open state_timeout cast~n", []),
    {next_state, locked, Data};
open(internal, {button,_}, _) ->
    io:format("code_lock:open internal~n", []),
    {keep_state_and_data, [postpone]};
open(cast, next_state, Data) ->
    io:format("code_lock:next_state locked~n", []),
    {next_state, locked, Data};
open(cast, state_timeout, Data) ->
    io:format("code_lock:cast state_timeout open~n", []),
    {keep_state, Data, [{state_timeout,5000,cast}]};
open(cast, {postpone, Msg}, _Data) ->
    io:format("code_lock:cast open postpone Msg:~p~n", [Msg]),
    keep_state_and_data;
?HANDLE_COMMON.

do_lock() ->
    io:format("code_lock:Locked~n", []).
do_unlock() ->
    io:format("code_lock:Open~n", []).

terminate(_Reason, State, _Data) ->
    State =/= locked andalso do_lock(),
    ok.
  • code_lock_2.erl
-module(code_lock_2).
-behaviour(gen_statem).
-export([init/1, callback_mode/0, terminate/3]).
-export([handle_event/4]).

callback_mode() ->
    [handle_event_function, state_enter].

init(Code) ->
    process_flag(trap_exit, true),
    Data = #{code => Code, length => length(Code), buttons => []},
    {ok, locked, Data}.

%% State: locked
handle_event(enter, _OldState, locked, Data) ->
    do_lock(),
    {keep_state, Data#{buttons := []}};
handle_event(state_timeout, button, locked, Data) ->
    {keep_state, Data#{buttons := []}};
handle_event(
  internal, {button,Button}, locked,
  #{code := Code, length := Length, buttons := Buttons} = Data) ->
    NewButtons =
        if
            length(Buttons) < Length ->
                Buttons;
            true ->
                tl(Buttons)
        end ++ [Button],
    if
        NewButtons =:= Code -> % Correct
            {next_state, open, Data};
    true -> % Incomplete | Incorrect
            {keep_state, Data#{buttons := NewButtons},
             [{state_timeout,30000,button}]} % Time in milliseconds
    end;

%% State: open
handle_event(enter, _OldState, open, _Data) ->
    do_unlock(),
    {keep_state_and_data,
     [{state_timeout,10000,lock}]}; % Time in milliseconds
handle_event(state_timeout, lock, open, Data) ->
    {next_state, locked, Data};
handle_event(internal, {button,_}, open, _) ->
    {keep_state_and_data,[postpone]};

%% Common events
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),
             [{next_event,internal,{button,Button}},
              {state_timeout,30000,button}]}; % Time in milliseconds
        #{} ->
            keep_state_and_data
    end;
handle_event(cast, change_callback_module, _State, Data) ->
    io:format("code_lock_2:change_callback_module -> code_lock~n", []),
    {keep_state, Data, [{change_callback_module, code_lock}]};
handle_event(cast, push_callback_module, _State, Data) ->
    io:format("code_lock_2:push_callback_module -> code_lock~n", []),
    {keep_state, Data, [{push_callback_module, code_lock}]};
handle_event(cast, pop_callback_module, _State, Data) ->
    io:format("code_lock_2:pop_callback_module~n", []),
    {keep_state, Data, [pop_callback_module]};
handle_event({call,From}, code_length, _State, #{length := Length}) ->
    io:format("code_lock_2:code_length~n", []),
    {keep_state_and_data,
     [{reply,From,Length}]};
handle_event(Event, EventData, State, Data) ->
    io:format("code_lock_2:not match Event:~p, EventData:~p, State:~p, Data:~p~n", [Event, EventData, State, Data]),
    {keep_state_and_data}.

do_lock() ->
    io:format("code_lock_2:Locked~n", []).
do_unlock() ->
    io:format("code_lock_2:Open~n", []).

terminate(_Reason, State, _Data) ->
    State =/= locked andalso do_lock(),
    ok.
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值