Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
/afsmPublic

Tutorial: Vending machine FSM

Sergei Fedorov edited this pageNov 27, 2016 ·6 revisions

This tutorial uses most of features of afsm library such as state transitions, entry/exit actions, nested state machines, in-state event handling, default transitions and guards. Also it will expose some C++ template programming techniques used in afsm library.

We will create a hierarchical state machine for a vending machine. It will:

  • turn on and off;
  • when there goods in the machine it will accept money, dispence goods and deliver change;
  • when the machine runs out of goods, it will transit toout_of_service sate;
  • the machine can be put to amaintenance state, when goods can be added and removed, and the prices for goods can be set.

Basic Machine

We will start with a state machine that can only turn on and off, it will have two states -on andoff and will handle two eventspower_on andpower_off

vending-starter

First of all we need to include a single header of afsm library

#include<afsm/fsm.hpp>

Then we start defining our state machine. All state machine definitions must derive from::afsm::def::state_machine template, all states - from::afsm::def::state template. If the machine contains a terminal state, it must derive from::afsm::def::terminal_state template. There is no difference if we define nested states of nested states inside or outside state machine definition, but in this example we will define then inside the state machine class to follow the structure of the state machine and not to clutter the enclosing namespace. We will place all declarations inside avending namespace.

namespacevending {structvending_def : ::afsm::def::state_machine<vending_def> {//@{/** @name Substates definition*/structoff : state<off> {};structon  : state<on> {};//@}/** Initial state machine state*/using initial_state = off;};}/* namespace vending*/

Next we will add events and define a transition table. We will put events in anevents namespace, to easily distinguish the events from other entities in code. An event is a structure that must be either a copy- or move- constructible. It'sexact type is used for selecting transitions.

namespacevending {namespaceevents {structpower_on {};structpower_off {};}/* namespace events*/structvending_def : ::afsm::def::state_machine<vending_def> {// Skip .../** State transition table*/using transitions = transition_table </*  Start   Event               Next*/        tr< off,    events::power_on,   on      >,        tr< on,     events::power_off,  off     >    >;};}/* namespace vending*/

We are almost done with the basic state machine. All we are left to do - is to instantiate a state machine with a class template that will process events and handle state switching.

using vending_sm = ::afsm::state_machine<vending_def>;

Processing Events and Checking State

The::afsm::state_machine template definesprocess_event member function template that accepts instances of events, so turning on our vending machine will look like

vending_sm vm;vm.process_event(events::power_on{});

The result ofprocess_event is anevent_process_result enum value (definedhere).refuse means that the state machine cannot process the event in current state,process - the state machine processed the event and changed state,process_in_state means that the state machine processed the event without changing state,defer - the event was enqueued for processing later (either due to the machine is busy or the current state explicitly deferred the event). The result can be checked by functionsok(event_process_result) anddone(event_process_result).

To check that a machine is in a given state there is a member function templateis_in_state. Checks will look like this:

if (vm.is_in_state<vending_sm::on>()) {/**/ }if (vm.is_in_state<vending_sm::off>()) {/**/ }

You can find a fully compilable and runnable source codehere. The program defines all the above classes and makes some use of the machine - turns it on and off and outputs state checks in between.

Nested State Machines

As long as when on a vending machine can do different things, we will need to convert theon state to a nested state machine.

vending-basic

So we will replacestruct on : state<on> {}; with:

structon  : state_machine<on> {//@{/** @name Substates definition*/structserving : state<serving> {};structmaintenance : state<maintenance> {};structout_of_service : state<out_of_service> {};//@}/** Initial state machine state*/using initial_state = serving;/** State transition table*/using transitions = transition_table</*  Start           Event                       Next*/            tr< serving,        events::start_maintenance,  maintenance     >,            tr< serving,        events::out_of_goods,       out_of_service  >,            tr< out_of_service, events::start_maintenance,  maintenance     >,            tr< maintenance,    events::end_maintenance,    serving         >        >;    };

Passing Arguments to State Machine Constructor

As far as it's dull to play around with an empty vending machine, we will add some goods to it.

structgoods_entry {int     amount;float   price;};using goods_storage = ::std::map<::std::size_t, goods_entry>;structvending_def : ::afsm::def::state_machine<vending_def> {// Skip .../** Default constructor*/vending_def() : goods{} {}/** Constructor, moving goods container to data member*/vending_def(goods_storage&& g) : goods{::std::move(g)} {}    goods_storage       goods;};

We have added a data member containing goods loaded to the vending machine and a default constructor and a constructor accepting some set of goods. All parameters passed to the constructor ofvending_sm template instance will be passed over to the constructor ofvending_def class, so any number of constructors can be defined.

    vending_sm vm{ goods_storage{        {1, {10,15.0f}},        {5, {100,5.0f}}    }};

Please note that only the outermost state machine can have user constructors. All nested states and state machinesmust be default-constructible.

In-State Transitions

Next we will add handling ofset_price andwithdraw_money events to themaintenance sub state.vending-maintenance

namespaceevents {// Skip ...structset_price {    ::std::size_t   p_no;float           price;};structwithdraw_money {};// Skip ...}/* namespace events*/structvending_def : ::afsm::def::state_machine<vending_def> {// Skip ...structon  : state_machine<on> {// Skip ...structmaintenance : state<maintenance> {/** @name In-state transitions*/using internal_transitions = transition_table<                in< events::set_price >,                in< events::withdraw_money >            >;        };// Skip ...    };// Skip ...};

This way the state machine will consume events, but do nothing, so we need to add actions to the event handling.

Adding Transition Actions

Atransition action is a function object that is called when a state machine has left one state and did not yet enter another. The action is passed references to the event that triggered the transition, to the enclosing state machine (the closest scope) and references to the source and target states.

structvending_def : ::afsm::def::state_machine<vending_def> {structon  : state_machine<on> {// Skip ...structmaintenance : state<maintenance> {//@{/** @name Actions*/structset_price {template<typename FSM,typename SourceState,typename TargetState >voidoperator()(events::set_price&& price, FSM& fsm, SourceState&, TargetState&)const                {root_machine(fsm).set_price(price.p_no, price.price);                }            };structclear_balance {template<typename FSM,typename SourceState,typename TargetState >voidoperator()(events::withdraw_money&&, FSM& fsm, SourceState&, TargetState&)const                {root_machine(fsm).clear_balance();                }            };//@}/** @name In-state transitions*/using internal_transitions = transition_table</*  Event                   Action*/                in< events::set_price,      set_price       >,                in< events::withdraw_money, clear_balance   >            >;        };// Skip ...    };// Skip .../**     * Set price for an item     * @param p_no     * @param price*/voidset_price(::std::size_t p_no,float price)    {auto f = goods.find(p_no);if (f != goods.end()) {            f->second.price = price;        }    }voidclear_balance()    { balance =0; }// Skip ...};

To access the outermost state machine object, a free functionroot_machine can be used with a parameter of a nested state machine or a state. In this case theFSM template parameter will evaluate to::afsm::inner_state_machine< vending_def::on > and both ofSourceState andTargetState will evaluate to::afsm::state< vending_def::on::maintenance > as the action will be called for in-state transition.

See alsoTransition Actions

All the above can be found inexample program vending_nested_sm.cpp

Final State Machine

vending

Adding Guards

Currently the maintenance mode is not protected by no means. We will add afactory_code to the machine and check it before transiting tomaintenance mode. We will pass a code to check in thestart_maintenance event.

Atransition guard is a functional object returning a boolean. Atrue value allows the transition,false - prohibits it.

vending_def modifications

structvending_def : ::afsm::def::state_machine<vending_def> {// Skip ...staticconstint factory_code   =2147483647;// Skip ...int             secret;// Skip ...};

Add a guard object and add it to the transition table

structon  : state_machine<on> {// A type alias for actual state machine, that will be passed to actions// and guards, just for demonstration purposesusing on_fsm = ::afsm::inner_state_machine<on, vending_fsm>;// Forward declarationstructmaintenance;//@{/** @name Guards*/structcheck_secret {template<typename State >booloperator()(on_fsmconst& fsm, Stateconst&, events::start_maintenanceconst& evt)const        {returnroot_machine(fsm).secret == evt.secret;        }    };//@}// Skip ...using transitions = transition_table</*  Start           Event                       Next            Action  Guard*/// Skip ...        tr< serving,        events::start_maintenance,  maintenance,    none,   check_secret    >,        tr< out_of_service, events::start_maintenance,  maintenance,    none,   check_secret    >,// Skip ...    >;};

Please note that an empty action is denoted bynone type. An empty guard can either skipped or set tonone (which is the default for the guard).

A guard can be negated bynot_ template, guards can be combined byand_ anor_ templates.

Guards are convenient way for conditional transitions.

See alsoTransition Guards.

Preserving State`s State (History)

By default every state is cleared after the next state is entered. Technically the state object will be assigned a default-constructed instance.

We would want to preserve the state of the machine that it was in when turning the machine off and back on, so that it stays in maintenance mode for instance. It is achieved by adding a::afsm::def::tags::has_history to the state's definition.

structvending_def : ::afsm::def::state_machine<vending_def> {// Skip ...using history       = ::afsm::def::tags::has_history;// Skip ...structon  : state_machine<on, history> {    };// Skip ...};

We added history for theon state to save the inner state when turned off and back on.

See alsoHistory.

Default Transitions

Adefault transition is a transition that can happen any time the state machine changes state and the default transition condition is satisfied. Default transition is defined by specifyingnone where an event type is expected. Default transitionsmust have guards. There can be several default transitions originating in the same state provided that their guards are different. The transitions will be checked in their order of definition.

Earlier we added a transition toout_of_service state onout_of_goods event, but it's not too convenient to check if the machine is empty in every place of code that we modify the quantity of goods. We will add a guardis_empty that will check that the vending machine is out of goods and a transition that will be checked after each state transition, e.g. after each sale theis_empty guard will be checked and if it fulfills, the state machine will transit fromserving toout_of_service state automatically.

Guard predicate and function in the state machine

structvending_def : ::afsm::def::state_machine<vending_def> {// Skip ...//@{/** @name Guards*/structis_empty {template<typename FSM,typename State >booloperator()(FSMconst& fsm, Stateconst&)const        {returnroot_machine(fsm).is_empty();        }    };// Skip ...//@}// Skip ...boolis_empty()const    {returncount() ==0;    }    ::std::size_tcount()const    {return ::std::accumulate(            goods.begin(), goods.end(),0ul,            [](::std::size_t cnt, goods_storage::value_typeconst& i)            {return cnt + i.second.amount;            }        );    }// Skip ...};

Default transitions for theon state

structon  : state_machine<on, history> {// Skip ...using transitions = transition_table</*  Start           Event   Next            Action  Guard*//* Default transitions*//*-----------------+-------+---------------+-------+----------------*/        tr< serving,        none,   out_of_service, none,   is_empty        >,        tr< out_of_service, none,   serving,        none,   not_<is_empty>  >,// Skip ...    >;// Skip ...};

State Entry and Exit Actions

Each state and state machine can defineentry andexit actions for any event. The handlers can be either catch-all template member functions or a state can handle different events separately. In our example we added substates to theserving state and we will handle collecting money and dispensing items by theactive substate ofserving. It will add money to the state's variable holding amount of money entered by the state machine's user on state enter and will give change to the user on state's exit. We will check if the item exists and money entered is enough in transition guard. We will dispense item and update total number of money collected in transition action.

structactive : state<active> {template<typename FSM >voidon_enter(events::money&& money, FSM&)    {        balance += money.amount;    }template<typename FSM >voidon_exit(events::select_item&& item, FSM& fsm)    {// Subtract current user balanceauto& root =root_machine(fsm);        balance -= root.get_price(item.p_no);if (balance >0) {// Give change        }    }float       balance{0};};

See alsoEntry and Exit Actions

Posting Events to Self

It is often needed to post events while processing other events. The events are processed by the outermost state machine (the root machine). It is safe to post events to the root state machine at any time, for example from a transition or entry/exit action.

You can access the root machine when having an instance of any state wrapper in the state machine. State machine wrappers are accessible in any transition or entry/exit action via parameters. To obtain a reference to the root state machine you should pass a reference to a part of it toroot_machine function.

To access the state machine from the definition code a cast to wrapper type is needed.

structvending_def : ::afsm::def::state_machine<vending_def> {// This is the type alias for state machine wrapper.// It is the most simple one, with no mutexes or observers.using vending_fsm   = ::afsm::state_machine<vending_def>;// Skip ...// Convenience functions to cast current object to the wrapper type.    vending_fsm&rebind()    {returnstatic_cast<vending_fsm&>(*this); }    vending_fsmconst&rebind()const    {returnstatic_cast<vending_fsmconst&>(*this); }// Skip ...// Posting events to self example.voidadd_goods(events::load_goods&& entry)    {// Skip ...rebind().process_event(events::load_done{});    }};

Final state machine code,test that checks the state machine.

TODO Add links to documentation pages.

Clone this wiki locally

[8]ページ先頭

©2009-2025 Movatter.jp