Обрабатывать события незаконности, не сбрасывая время ожидания в поведении gen_fsm

Пример запертой двери gen_fsm в системной документации Elrang Otp. У меня есть вопрос по поводу тайм-аута. Сначала я скопирую код здесь:

-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}.

Вот вопрос: когда дверь открывается, если я нажимаю кнопку, gen_fsm будет иметь {button, Digit} событие в государстве open, Произойдет ошибка. Но если я добавлю этот код после открытия функции:

open(_Event, State) ->
   {next_state, open, State}.

Тогда, если я нажму кнопку через 30 секунд, тайм-аут не будет. Дверь будет открыта навсегда. Что я должен делать?

Благодарю.

Обновить:

Я знаю, что мог бы использовать send_event_after или что-то типа того. Но я не думаю, что это хорошая идея. Потому что состояние, которое вы исключили для обработки сообщения, может быть изменено в сложном приложении.

Например, если у меня есть функция запирания двери вручную после того, как дверь открылась через 30 с. затем locked будет обращаться с timeout сообщение, которое не является исключительным поведением.

3 ответа

Вы можете сохранить оставшееся время ожидания в StateData, Для этого добавьте третий элемент в кортеж:

init(Code) ->
    {ok, locked, {[], Code, infinity}}.

Вам нужно будет изменить locked установить начальное значение:

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.

И, если кнопка открыта при открытии, рассчитайте новый тайм-аут и снова обойдите:

open({button, _Digit}, {_SoFar, _Code, Until} = State) ->
    Now = to_milliseconds(os:timestamp()),
    Timeout = Until - Now,
    {next_state, open, State, Timeout};

Вам также понадобится следующая вспомогательная функция:

to_milliseconds({Me, S, Mu}) ->
    (Me * 1000 * 1000 * 1000) + (S * 1000) + (Mu div 1000).

Используя таймаут fsm, насколько я знаю, невозможно избежать его повторной инициализации:

  • Если вы не укажете новый тайм-аут, когда пропустите событие, когда дверь открыта, как вы заметите, она останется открытой навсегда.
  • Если вы укажете один, он будет перезапущен с самого начала.

Если ни одно из этих решений не удовлетворяет вас, вы можете использовать внешний процесс для создания тайм-аута:

-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).

Для этого в модуле таймера есть несколько функций, предпочтительнее моего пользовательского примера.

может лучше использовать тайм-аут Fsm в состоянии блокировки:

  • дождитесь первой цифры без таймаута
  • вводится цифра и код завершен -> проверить ее и продолжить без тайм-аута (блокировка или открытие в зависимости от введенного кода)
  • вводится цифра, а код не завершен-> сохранить ее и продолжить с таймаутом
  • если происходит неожиданное событие -> перезапустить с начала без тайм-аута
  • если время ожидания истекло, перезапустите с начала без времени ожидания

РЕДАКТИРОВАТЬ: бен Ван: то, что вы говорите в своем обновлении, является правильным, но вы не можете избежать этой ситуации. Я не знаю ни одной встроенной функции, которая бы охватывала ваш вариант использования. Чтобы удовлетворить его, вам нужно будет управлять неожиданным сообщением тайм-аута в состоянии блокировки, но, чтобы избежать многократного запуска тайм-аута, вам также нужно будет остановить текущий, прежде чем перейти в состояние блокировки. Обратите внимание, что это не мешает вам управлять сообщением тайм-аута в состоянии блокировки, потому что между сообщением об остановке таймера и самим тайм-аутом идет гонка. Я написал для одного из моих приложений функцию общего назначения apply_after, которую можно отменить, остановить и возобновить:

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.

Вам нужно будет обработать Pid процесса тайм-аута в состоянии FSM.

Вы должны указать время ожидания в открытой функции "open(_Event, State)"

Поскольку следующее состояние выполняется без тайм-аута.. дверь останется открытой навсегда, и нигде не произойдет тайм-аут..

Вновь определенная функция должна быть

open(_Event, State) -> {next_state, open, State, 30000}. %% State должно быть повторно инициализировано

Другие вопросы по тегам