Skip to content
Oleksiy Kebkal edited this page Jan 25, 2017 · 16 revisions

MODULE

fsm behavior

MODULE SUMMARY

Each agent is explicitly defined in the form of a finite state machine or a pushdown automation and is driven by events, generated externally by interface processors or internally by event handles or timers. Each agent is fault-isolated. Incorrect agent implementation should not affect the behavior of an error-free agent.

DESCRIPTION

State transition table

A state transition table defines a finite state machine or pushdown automation as a list of transitions trans(), returned by Module:trans/0. Each transition state_trans() is a tuple of a state and a list of transitions to another state. The transitions tuples trans_arrow() are doubles or quadruples. If you have only double tuples as trans_arrow() in a transition table, this table defines a finite state machine, otherwise it is a pushdown automation.

The requirement to explicitly define a state transition table and support of the pushdown automation option are the key differences from standard Erlang behaviors gen_fsm and gen_statem.

Below is an example of a state transition table, defined as the ?TRANS macro, returned by a trans() callback in the fsm behavior implementation src/fsm_triv_alh.erl:

-define(TRANS, [
                {idle,
                 [{internal, idle},
                  {answer_timeout, idle},
                  {send_data, transmit},
                  {backoff, transmit},
                  {sendend, idle},
                  {recvend, idle},
                  {sendstart, sense},
                  {recvstart, sense}
                 ]},
                {transmit,
                 [{data_sent, idle}
                 ]},
                {sense,
                 [{answer_timeout, sense},
                  {backoff, sense},
                  {send_data, sense},
                  {sendstart, sense},
                  {recvstart, sense},
                  {sendend, idle},
                  {recvend, idle}
                 ]},
                {alarm,[]}
               ]).
trans() -> ?TRANS.

The finite state machine, defined above, has states idle, transmit, sense and alarm and events internal, answer_timeout, send_data, backoff, sendend, recvend, sendstart, recvstart, data_sent. By default, a transition to the alarm state occurs, if a reaction to a particular event is not defined in the state transition table in any given state.

States and events in state transition tables can be any atom. The fsm behavior implementation must export event handlers for each event. The handle function name is built as handle_ plus the state name. For transition tables, defined above, the following handlers are implemented and exported in the src/fsm_triv_alh.erl:

-export([handle_idle/3, handle_alarm/3, handle_sense/3, handle_transmit/3]).

The handlers are not explicitly called in the fsm implementation. The handler call that corresponds to the current state and event occurs from fsm:run_event()/3. The fsm:run_event()/3 must be used in the Module:handle_event/3 callback implementation, when incoming messages are preprocessed and converted to an event.

Interface Processors

Agents are connected to each other and to external processes via interface processors. These convert incoming raw data from external interfaces to well-defined messages, and outgoing messages from corresponding agents - back to raw data. Each agent specifies particular interface processors, required to be connected to it.

Interface processors implement the role_worker behavior.

When new messages arrive from an interface processor, the Module:handle_event/3 is called, the message is passed as a third parameter Term. The Term reception may trigger an event in the Module:handle_event/3 and cause a fsm:run_event/3 call.

In the example below, if double tuple is received from any interface processor and the first tuple element is the send_data atom, a send_data event is triggered by modification of the event field of the internal state SM, and fsm:run_event/3 is called to handle this event according to the state transition table.

handle_event(MM, #sm{env = #{got_sync := Got_sync}} = SM, Term) ->
  case Term of
...
    {send_data, _} when Got_sync ->
      fsm:run_event(MM, SM#sm{event=send_data}, Term);
...
  end.

Interface processors must be bound to the fsm implementation in the fsm_worker implementation module based on the configuration file.

Timers

A timer can be started with a fsm:set_timeout/3 call within the fsm implementation. This call modifies the SM internal state, new state is returned by fsm:set_timeout/3. When the timer expires, Module:handle_event/3 is called with the Term parameter defined as a {timeout, Name} double.

In the example below, any expired timer triggers an event with a name that matches the expired timer's:

handle_event(MM, #sm{env = #{got_sync := Got_sync}} = SM, Term) ->
  case Term of
    {timeout, Event} ->
      fsm:run_event(MM, SM#sm{event=Event}, {});
...

Event preprocessor

The central callback for event processing is Module:handle_event/3 that pre-processes incoming messages. Here messages, received from interface processors or timers, are transformed into events of the particular state machine.

Message handling can be viewed as internal state modification, the updated state must be returned by Module:handle_event/3. Internal state is passed as the first parameter SM and its updated value must be returned by Module:handle_event/3. The second input parameter MM identifies the interface processor, if the Module:handle_event/3 call was triggered by a message that arrived over the interface processor. The last parameter contains either the message, received from the interface processor, or a double that identifies the expired timer.

Based on the input parameters, Module:handle_event/3 either triggers events for the state machine and calls fsm:run_event/3, or handles the input by itself if there is no need for calling the state machine's handlers.

In the example below, any instant message, received from an acoustic modem, is cast to the triv_alt interface processor. Here, there is no need for updating the state machine with the reception of an instant message.

handle_event(MM, #sm{env = #{got_sync := Got_sync}} = SM, Term) ->
  case Term of
...
    {async, _PID, {recvim,_,_,_,_,_,_,_,_, Payl}} ->
      fsm:cast(SM, triv_alh, {send, {binary, list_to_binary([Payl,"\n"])} });
...
  end.

Event handlers

When fsm:run_event/3 is called, fsm searches for a transition that matches the state, event and transition table of the internal state SM. If the transition is found, state is updated, and the corresponding event handler of the fsm behavior implementation is called. The internal state SM is passed as the first parameter of the fsm:run_event/3.

For example, let us assume, that the transition table is defined as in the example above, the current state is idle and the event is send_data. Upon the fsm:run_event/3 call from the fsm_triv_alh.erl module, the state will be updated to transmit and fsm_triv_alh:handle_transmit/3 will be called. The internal state value, updated by the fsm_triv_alh:handle_transmit/3, will be returned by the fsm:run_event/3 call.

See the implementation of fsm_triv_alh:handle_transmit/3 in the example below:

handle_transmit(_MM, #sm{event = Event} = SM, Term) ->
  Payl =
    case Term of
      {send_data, P}          -> P;
      _ when Event == backoff -> env:get(SM, delayed_msg)
    end,
  [
   env:put(__,got_sync,false),   
   fsm:clear_timeout(__, backoff),
   update_backoff(__, decrement),
   fsm:send_at_command(__, {at,{pid,0},"*SENDIM",255,noack,Payl}),
   fsm:set_event(__, data_sent)
  ] (SM).

In the implementation above, payload data is extracted from Term and the subsequent sequence of calls is formed as a pipeline. In the pipeline, each value, returned by a call, becomes the __ input parameter of the next call.

The pipeline is an extension of Erlang syntax, defined in https://github.com/stolen/pipeline.

The implementation below uses standard Erlang syntax and is equivalent to the previous one.

handle_transmit(_MM, #sm{event = Event} = SM, Term) ->
  Payl =
    case Term of
      {send_data, P}          -> P;
      _ when Event == backoff -> env:get(SM, delayed_msg)
    end,
  SM1 = env:put(SM,got_sync,false),   
  SM2 = fsm:clear_timeout(SM1, backoff),
  SM3 = update_backoff(SM2, decrement),
  SM4 = fsm:send_at_command(SM3, {at,{pid,0},"*SENDIM",255,noack,Payl}),
  fsm:set_event(SM4, data_sent).

DATA TYPES

time() = {s, integer() > 0} | {ms, integer() > 0} | {us, integer() > 0}

trans() = [state_trans()]

state_trans() = {State :: atom(), [trans_arrow()]}

trans_arrow() =
  {Event :: atom(), State :: atom()} |
  {Event :: atom(), Pop :: atom(), Push :: atom(), State :: atom()}

at() anything that can be accepted by role_at:from_term

EXPORTS

clear_timeout(SM, Event) -> Result

Types

  SM = sm()
  Event = atom()
  Result = sm()
clear_timeouts(SM) -> Result

Types

  SM = sm()
  Result = sm()
clear_timeouts(SM, EventList) -> Result

Types

  SM = sm()
  EventList = [atom()]
  Result = sm()
set_timeout(SM, Time, Event) -> Result

Types

  SM = sm()
  Time = time()
  Event = atom()
  Result = sm()
check_timeout(SM, Event) -> Result

Types

  SM = sm()
  Event = atom()
  Result = true | false
set_event(SM, Event) -> Result

Types

  SM = sm()
  Event = atom()
  Result = sm()
run_event(MiddleMan, SM, Term) -> Result

Types

  MiddleMan = mm()
  SM = sm()
  Term = any()
  Result = sm()
send_at_command(SM, AT) -> Result

Types

  SM = sm()
  AT = at()
  Result = sm()
broadcast(SM, Target, Term) -> Result

Types

  SM = sm()
  Target = atom()
  Term = any()
  Result = sm()
cast(SM, Target, Term) -> Result

Types

  SM = sm()
  Target = atom()
  Term = any()
  Result = sm()

= cast(SM, MM, EOpts, Term, Condition) -> Result

Types

  SM = sm()
  MiddleMan = mm()
  EOpts = any()
  Term = any()
  Condition = fun((MiddleMan, Role :: atom(), EOpts) -> true | false)
  Result = sm()

= role_available(SM, Target) -> Result

Types

  SM = sm()
  Target = atom()
  Result = true | false

CALLBACK FUNCTIONS

The following functions must be exported from a fsm callback module.

Module:start_link(SM) -> Result

Types

  SM = sm()
  Result = {ok,pid()} | ignore | {error,any()}
Module:init(SM) -> Result

Types

  SM = sm()
  Result = sm()
Module:stop(SM) -> ok

Types

  SM = sm()
Module:handle_event(MM, SM, Term) -> Result =

Types

  SM = sm()
  MM = mm()
  Term = any()
  Result = sm()
Module:trans() -> Result

Types

  Result = trans()
Module:final() -> Result

Types

  Result = [atom()]
Module:init_event() -> Result

Types

  Result = atom()

SEE ALSO

role_worker, fsm_worker