-
Notifications
You must be signed in to change notification settings - Fork 24
Tutorial: Vending machine FSM
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 to
out_of_service
sate; - the machine can be put to a
maintenance
state, when goods can be added and removed, and the prices for goods can be set.
We will start with a state machine that can only turn on and off, it will have two states - on
and off
and will handle two events power_on
and power_off
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 a vending
namespace.
namespace vending {
struct vending_def : ::afsm::def::state_machine<vending_def> {
//@{
/** @name Substates definition */
struct off : state<off> {};
struct on : 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 an events
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's exact type is used for selecting transitions.
namespace vending {
namespace events {
struct power_on {};
struct power_off {};
} /* namespace events */
struct vending_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>;
The ::afsm::state_machine
template defines process_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 of process_event
is an event_process_result
enum value (defined here). 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 functions ok(event_process_result)
and done(event_process_result)
.
To check that a machine is in a given state there is a member function template is_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 code here. 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.
As long as when on a vending machine can do different things, we will need to convert the on
state to a nested state machine.
So we will replace struct on : state<on> {};
with:
struct on : state_machine<on> {
//@{
/** @name Substates definition */
struct serving : state<serving> {};
struct maintenance : state<maintenance> {};
struct out_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 >
>;
};
As far as it's dull to play around with an empty vending machine, we will add some goods to it.
struct goods_entry {
int amount;
float price;
};
using goods_storage = ::std::map<::std::size_t, goods_entry>;
struct vending_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 of vending_sm
template instance will be passed over to the constructor of vending_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 machines must be default-constructible.
Next we will add handling of set_price
and withdraw_money
events to the maintenance
sub state.
namespace events {
// Skip ...
struct set_price {
::std::size_t p_no;
float price;
};
struct withdraw_money {};
// Skip ...
} /* namespace events */
struct vending_def : ::afsm::def::state_machine<vending_def> {
// Skip ...
struct on : state_machine<on> {
// Skip ...
struct maintenance : 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.
A transition 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.
struct vending_def : ::afsm::def::state_machine<vending_def> {
struct on : state_machine<on> {
// Skip ...
struct maintenance : state<maintenance> {
//@{
/** @name Actions */
struct set_price {
template < typename FSM, typename SourceState, typename TargetState >
void
operator()(events::set_price&& price, FSM& fsm, SourceState&, TargetState&) const
{
root_machine(fsm).set_price(price.p_no, price.price);
}
};
struct clear_balance {
template < typename FSM, typename SourceState, typename TargetState >
void
operator()(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
*/
void
set_price(::std::size_t p_no, float price)
{
auto f = goods.find(p_no);
if (f != goods.end()) {
f->second.price = price;
}
}
void
clear_balance()
{ balance = 0; }
// Skip ...
};
To access the outermost state machine object, a free function root_machine
can be used with a parameter of a nested state machine or a state. In this case the FSM
template parameter will evaluate to ::afsm::inner_state_machine< vending_def::on >
and both of SourceState
and TargetState
will evaluate to ::afsm::state< vending_def::on::maintenance >
as the action will be called for in-state transition.
See also Transition Actions
All the above can be found in example program vending_nested_sm.cpp
Currently the maintenance mode is not protected by no means. We will add a factory_code
to the machine and check it before transiting to maintenance
mode. We will pass a code to check in the start_maintenance
event.
A transition guard is a functional object returning a boolean. A true
value allows the transition, false
- prohibits it.
struct vending_def : ::afsm::def::state_machine<vending_def> {
// Skip ...
static const int factory_code = 2147483647;
// Skip ...
int secret;
// Skip ...
};
struct on : state_machine<on> {
// A type alias for actual state machine, that will be passed to actions
// and guards, just for demonstration purposes
using on_fsm = ::afsm::inner_state_machine<on, vending_fsm>;
// Forward declaration
struct maintenance;
//@{
/** @name Guards */
struct check_secret {
template < typename State >
bool
operator()(on_fsm const& fsm, State const&, events::start_maintenance const& evt) const
{
return root_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 by none
type. An empty guard can either skipped or set to none
(which is the default for the guard).
A guard can be negated by not_
template, guards can be combined by and_
an or_
templates.
Guards are convenient way for conditional transitions.
See also Transition Guards.
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.
struct vending_def : ::afsm::def::state_machine<vending_def> {
// Skip ...
using history = ::afsm::def::tags::has_history;
// Skip ...
struct on : state_machine<on, history> {
};
// Skip ...
};
We added history for the on
state to save the inner state when turned off and back on.
See also History.
A default 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 specifying none
where an event type is expected. Default transitions must 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 to out_of_service
state on out_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 guard is_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 the is_empty
guard will be checked and if it fulfills, the state machine will transit from serving
to out_of_service
state automatically.
struct vending_def : ::afsm::def::state_machine<vending_def> {
// Skip ...
//@{
/** @name Guards */
struct is_empty {
template < typename FSM, typename State >
bool
operator()(FSM const& fsm, State const&) const
{
return root_machine(fsm).is_empty();
}
};
// Skip ...
//@}
// Skip ...
bool
is_empty() const
{
return count() == 0;
}
::std::size_t
count() const
{
return ::std::accumulate(
goods.begin(), goods.end(), 0ul,
[](::std::size_t cnt, goods_storage::value_type const& i)
{
return cnt + i.second.amount;
}
);
}
// Skip ...
};
struct on : 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 ...
};
Each state and state machine can define entry
and exit
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 the serving
state and we will handle collecting money and dispensing items by the active
substate of serving
. 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.
struct active : state<active> {
template < typename FSM >
void
on_enter(events::money&& money, FSM&)
{
balance += money.amount;
}
template < typename FSM >
void
on_exit(events::select_item&& item, FSM& fsm)
{
// Subtract current user balance
auto& root = root_machine(fsm);
balance -= root.get_price(item.p_no);
if (balance > 0) {
// Give change
}
}
float balance{0};
};
See also Entry and Exit Actions
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 to root_machine
function.
To access the state machine from the definition code a cast to wrapper type is needed.
struct vending_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()
{ return static_cast<vending_fsm&>(*this); }
vending_fsm const& rebind() const
{ return static_cast<vending_fsm const&>(*this); }
// Skip ...
// Posting events to self example.
void
add_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.
- Home
- Tutorial
-
Concepts
- State Hierarchy
- Entry/Exit Actions
- Transition Actions
- Transition Guards
- Internal Transitions
- Default Transitions
- Event Deferring
- History
- Orthogonal Regions
- Event Priority
- Common Base
- Thread Safety
- Exception Safety Guarantees
- Under the Hood
- Event Processing
- Performance