-
Notifications
You must be signed in to change notification settings - Fork 14
Features
hsmcpp allows to use hierarchical state machine (HSM) in your project without worrying about the mechanism itself and instead focus on the structure and logic. I will not cover basics of HSM and instead will focus on how to use the library. You can familiarize yourself with HSM concept and terminology here:
- Introduction to Hierarchical State Machines
- Wikipedia: UML state machine
- Hierarchical Finite State Machine for AI Acting Engine
Since Finite State Machines (FSM) are just a simple case of HSM, those could be defined too using hsmcpp.
Here is an example of a simple HSM which only contains states and transitions:
Events are defined as an enum:
enum class MyEvents
{
EVENT_1,
EVENT_2,
EVENT_3,
EVENT_4
};
They could be later used when registering transitions.
States are defined as an enum:
enum class MyStates
{
StateA,
StateB,
StateC
};
State callbacks are optional and include:
-
entering
- called right before changing a state
- transition is canceled if callback returns FALSE
-
state changed
- called when HSM already changed it's current state
-
exiting
- called for previous state before starting to transition to a new state
- transition is canceled if callback returns FALSE
Assuming we create HSM as a separate object, here are possible ways to register a state:
HierarchicalStateMachine<MyStates, MyEvents> hsm;
HandlerClass hsmHandler;
hsm.registerState(MyStates::StateA);
hsm.registerState(MyStates::StateA, &hsmHandler, &HandlerClass::on_state_changed_a);
hsm.registerState(MyStates::StateA,
&hsmHandler,
&HandlerClass::on_state_changed_a,
&HandlerClass::on_entering_a);
hsm.registerState(MyStates::StateA,
&hsmHandler,
&HandlerClass::on_state_changed_a,
&HandlerClass::on_entering_a,
&HandlerClass::on_exiting_a);
hsm.registerState<HandlerClass>(MyStates::StateA,
&hsmHandler,
&HandlerClass::on_state_changed_a,
nullptr,
&HandlerClass::on_exiting_a);
Note that if you explicitly need to pass nullptr (as in the last example) you will need to provide class name as a template parameter.
Transition is an entity which allows to change current HSM state to a different one. It's definition includes:
- starting state: state from which transition is possible
- target state: new state which HSM which have if transition is successful
- triggering event: even which triggers transition
- condition (optional): additional logic to restrict transition
- callback (optional): will be called during transition
HSM applies following logic when trying to execute a transition:
It is possible to define multiple transitions between two states. As a general rule these transitions should be exclusive, but HSM doesn't enforces this. If multiple valid transitions are found for the same event then the first applicable one will be used (based on registration order). But this situation should be treated by developers as a bug in their code since it most probably will result in unpredictable behavior.
To register transition use registerTransition() API:
hsm.registerTransition(MyStates::StateA,
MyStates::StateB,
MyEvents::EVENT_1,
&hsmHandler,
&HandlerClass::on_event_1_transition,
&HandlerClass::event_1_condition);
hsm.registerTransition(MyStates::StateA,
MyStates::StateB,
MyEvents::EVENT_1,
[](const VariantList_t& args){ ... },
[](const VariantList_t& args){ ... return true; });
Call transition() API to trigger a transition.
hsm.transition(MyEvents::EVENT_1);
By default transitions are executed asynchronously and it's a recommended way to use them. When multiple events are sent at the same time they will be internally queued and executed sequentially. Potentially it's possbile to have multiple events queued when you need to send a new event which will make previous events obsolete (for example user want to cancel operation). In this case you can use transitionWithQueueClear() or transitionEx() to clear pending events:
hsm.transitionWithQueueClear(MyEvents::EVENT_1);
hsm.transitionEx(MyEvents::EVENT_1, true, false);
Keep in mind that current ongoing transition can't be canceled.
Normally if you try to send event which is not handled in current state it will be just ignored by HSM without any notification. But sometimes you might want to know in advance if transition would be possible or not. You can use isTransitionPossible() API for that. It will check if provided event will be accepted by HSM considering:
- current state
- pending events
- conditions assigned to transitions
Note: it is still possible for HSM to reject your event if after isTransitionPossible() some other thread will manage to trigger another transition be carefull when using it in multithreaded environment.
Transitions can be executed synchronously using transitionEx() API. It was added mostly for testing purposes (since async unit tests are a headache) and is strongly discouraged from usage in production code. But if you really have to then keep these things in mind:
- All callbacks will still be executed on dispatcher's thread. So will have a deadlock if you trigger synchronous transition from HSM callback.
- when using Glib based dispatcher you can't call sync transitions from Glib thread assigned to dispatcher (usually main thread). This will also result in deadlock.
Imagine we have the following state machine:
In this example EVENT_CANCEL must be added for any state except StateA. With increasing complexity of your state machine this can become a signifficant issue for maintainance. So such logic could be simplified using substates:
Substates allow states grouping to create a hierarchy inside your state machine. Any state could have substates added to it on the following conditions:
- any state can have only one parent;
- there is no depth limitations when creating substates, but circle inclusion is not allowed (A->B->C->A);
- parent states can't have callbacks (it's possible to register them, but they will be ignored);
- when state has substates an entry point must be specified;
- only one entry point can be specified.
Entering a substate is considered an atomic operation which can't be interrupted.
Adding a new substate is done using registerSubstate() API:
hsm.registerSubstate(MyStates::ParentState, MyStates::StateB, true));
hsm.registerSubstate(MyStates::ParentState, MyStates::StateC));
Note that ParentState must be a part of MyStates enum as any other state.