Skip to content

Features

igor-krechetov edited this page Jan 2, 2023 · 15 revisions

Contents

Overview

*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:

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:

wiki_features_transition

Events

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

wiki_features_state

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

State actions

Besides implementing logic inside HSM callbacks it's possible to define some operations as state actions. These actions are built-in commands that are executed automatically based on HSM activity.

Actions could be added using:

bool registerStateAction(const HsmStateEnum state,
                         const StateActionTrigger actionTrigger,
                         const StateAction action,
                         Args... args);

At the moment two triggers are supported:

enum class StateActionTrigger {
    ON_STATE_ENTRY,
    ON_STATE_EXIT
};
  • StateActionTrigger::ON_STATE_ENTRY will execute action when entering a state;
  • StateActionTrigger::ON_STATE_EXIT will execute action when exiting a state.

Note: actions will be executed only if ongoing transition wasn't blocked by entry/exit callbacks.

Supported actions are:

  • StateAction::START_TIMER
    • ARGS: int timerID, int intervalMs, bool singleshot
    • Description
      • starts a singleshot or repeating timer. singleshot timer will fire only once and then will stay idle until started again;
      • does nothing if timerID or interval are invalid;
      • does nothing if requested timer is already running;
  • StateAction::STOP_TIMER
    • ARGS: int timerID
    • Description
      • stops running timer without trigerring anything;
      • does nothing if requested timer is not running;
  • StateAction::RESTART_TIMER
    • ARGS: int timerID
    • Description
      • restarts timer with the same interval and mode (singleshot or repeating) as were specified in start timer;
      • does nothing if timer is not running;
      • Note: trying to restart expired singleshot timer will do nothing since this timer is considered as "not running". Use "start timer" instead.
  • StateAction::TRANSITION
    • ARGS: int eventID, {arg1, arg2, ...}
    • Description
      • add specified event with it's arguments to the end of transition queue;

See Timers chapter for details regarding their usage.

Transitions

wiki_features_transition

Transition is an entity that allows changing current HSM state to a different one. Its 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:

wiki_features_callbacks

It is possible to define multiple transitions between two states. As a general rule, these transitions should be exclusive, but HSM doesn't enforce 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.

Usage

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,
                       true);
hsm.registerTransition(MyStates::StateA,
                       MyStates::StateB,
                       MyEvents::EVENT_1,
                       [](const VariantVector_t& args){ ... },
                       [](const VariantVector_t& args){ ... return true; },
                       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 possible 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 careful when using it in multi-threaded environment.

Self transitions

Self-transitions are transitions for which starting and target states are the same.

simple self transition

To register a self-transition use registerSelfTransition() API:

hsm.registerSelfTransition(MyStates::StateA,
                           MyEvents::EVENT_1,
                           TransitionType::INTERNAL,
                           &hsmHandler,
                           &HandlerClass::on_event_1_transition,
                           &HandlerClass::event_1_condition,
                           true);

hsm.registerSelfTransition(MyStates::StateA,
                           MyEvents::EVENT_1,
                           TransitionType::INTERNAL,
                           [](const VariantVector_t& args){ ... },
                           [](const VariantVector_t& args){ ... return true; },
                           true);

Note: though using registerSelfTransition() is a recommended way for defining self-transitions, you can also use registerTransition() API. Keep in mind that in this case transition type will be automatically set to "external".

There are 2 types of self-transitions:

  • external
    • During external transition, state machine exits current active state and returns back to it right away. This results in all entry, exit and state actions being invoked. This also affects substates if current state contains any.
  • internal
    • An internal transition does not allow the exit and entry actions to be executed. So only transition callback will be executed without any impact on active states.

Difference between these two types can be demonstrated with this example: self transition example

Let's assume StateC is currently active. If EVENT_INTERNAL is triggered then only following callbacks will be executed:

self transition internal

If EVENT_EXTERNAL is triggered then all corresponding exit/enter callbacks will be processed:

self transition external

Note: hotice how after external self-transition StateB became active. This happened due to state machine exiting from ParentState_1 and, consequentially, exiting from it's substates too.

Conditional transitions

Sometimes transition should be executed only when a specific condition is met. This could be achieved by setting condition callback and expected value. Transition will be ignored if value returned by callback doesnt match expected one.

Transitions priority

Ideally, when designing state machine, you should avoid having multiple transitions which could be valid at the same time. This will make understanding the logic and debugging easier. But if for some reason your state machine will contain such transition, hsmcpp library will still handle them in a deterministic and predictable manner:

  • all valid transitions will be executed if they are defined on the same level;
  • self transitions are always executed before any outgoing transitions;
  • if transitions are defined on multiple levels (for example between substates and on the same level as a parent):
    • internal transitions between substates always have the highest priority. Outer transitions will be ignored;

Let's check the following example:

wiki_features_transition_priorities

  • If *StateA is active and *EVENT_1 is triggered:
    • first self transition for *StateA will be executed;
  • both states B and C will be activated (see Parallel section for details).
  • If *StateD is active and *EVENT_3 is triggered:
  • only StateD->StateE transition will be executed since internal transitions have higher priority.
  • If *StateE is active and *EVENT_3 is triggered:
    • first self transition for *ParentState will be executed;
  • then ParentState->StateF transition will be executed.

Synchronous transitions

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

Parallel

Up until now our state machines were handling a single state at a time and no more than one state could have been active at any given moment. That's easy to define and handle, but imagine that we are using HSM to define behavior of a system UI and have the following requirements:

  • embedded system with two displays;
  • each display can display any of the available UI applications (Media, Weather, Navigation);

Since any two of the 3 defined UI applications could be active at any given time we would need to create 3 separate HSMs to handle their logic separately. Sounds a bit inconvenient, but still ok at this point.

wiki_features_parallel_usecase_media wiki_features_parallel_usecase_navi wiki_features_parallel_usecase_weather

But what if eventually our requirements get extended and now we also need to add interaction between these apps? For example, ability to open weather forecast from Navigation. At this point, things will start getting messy since we will have to manually synchronize 2 separate HSM in code.

All of this could be avoided by using the parallel states feature. Essentially it allows HSM to have multiple active states and process their transitions in parallel.

wiki_features_parallel_usecase

This structure can be achieved by simply defining multiple transitions which will be valid at the same time:

  • non-conditional transitions with the same event;
  • conditional transitions with the same event (condition must be TRUE for both transitions)
hsm.registerTransition(MyStates::StateA, MyStates::StateB, MyEvents::EVENT_1);
hsm.registerTransition(MyStates::StateA, MyStates::StateC, MyEvents::EVENT_1);

// or

hsm.registerTransition(MyStates::StateA,
                       MyStates::StateB,
                       MyEvents::EVENT_1,
                       &hsmHandler,
                       nullptr,
                       &HandlerClass::event_1_condition);
hsm.registerTransition(MyStates::StateA,
                       MyStates::StateC,
                       MyEvents::EVENT_1,
                       &hsmHandler,
                       nullptr,
                       &HandlerClass::event_1_condition);

When defining HSM in SCXML format you can also use tag. This approach is a bit more restrictive and was added mostly for compatibility with SCXML format specification. Here is an example from Qt Creator:

Parallel in QtCreator

Note: it's important to understand that all transitions and callbacks are executed on a single thread. If you need actual parallel execution of multiple state machines then you would need to create multiple event dispatchers and handle such machines separately.

Substates

Imagine we have the following state machine:

wiki_features_substate_fsm_approach

In this example EVENT_CANCEL must be added for any state except StateA. With increasing complexity of your state machine, this can become a significant issue for maintenance. So such logic could be simplified using substates:

wiki_features_substate

Substates allow grouping of states 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/composite 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;
  • multiple entry points can be specified for each composite state.

Entering a substate is considered an atomic operation that can't be interrupted.

Usage

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.

Multiple entry points

If you define multiple entry points without any additional conditions they will automatically become parallel states and will get activated as soon as HSM transitions to their parent state.

Conditional entry points

It's quite common to have multiple ways to enter a parent state. But sometimes you might have a situation when you would want to have a different entry state depending on the triggering transition.

This could be done by specifying multiple entry points with conditions.

wiki_features_substate_cond_entries

When determining which entry point to activate hsmcpp follows these rules:

  • if there are no conditional entry points -> activate all entry points;
  • if there is one or more conditional entry point -> check if outer transition event matches with entry point transition event;
    • in case of multiple conditional entry points they will be checked in the same order as they were registered;
  • if there are multiple conditional entry points with the same matching event all of them will be activated;
  • non-conditional entry points will be ignored if there is at least one matching conditional entry point;
  • if none of the conditional entry points match outer transition -> non-conditional entry points will be activated.

Here is how above example will treated by HSM:

  • when entering *Playback state from *Idle it will activate only *Paused substate;
  • we have conditional transition, but LOAD != RESTART_DONE;
  • when entering *Playback state from *Restart it will activate only *Playing substate;
    • since there is a matching conditional entry point transition to *Paused will be ignored.

History

A history state is used to remember the previous state of a state machine when it was interrupted. The following diagram illustrates the use of history states. The example is a state machine belonging to a washing machine.

wiki_features_history

In this state machine, when a washing machine is running, it will progress from "Washing" through "Rinsing" to "Spinning". If there is a power cut, the washing machine will stop running and will go to the "Power Off" state. Then when the power is restored, the Running state is entered at the "History State" symbol meaning that it should resume where it last left-off.

Each history state can have default transitions defined. This transition is used when a composite state had never been active before (therefore it's history being empty).

Two types of history are supported:

  • shallow

  • deep

    Note: it's important to keep in mind, that when restoring history all corresponding callbacks will be called (on enter, on state, on exit).

Shallow history

Shallow history pseudostate represents the most recent active substate of its parent state (but not the substates of that substate). A composite state can have at most one shallow history vertex. A transition coming into the shallow history state is equivalent to a transition coming into the most recent active substate of a state. The entry action of the state represented by the shallow history is performed.

A shallow history is indicated by a small circle containing an "H". It applies to the state that contains it.

Let's look at the example. Let's say we have this state machine with *StateE being currently active:

wiki_features_history_shallow_01

After E1 transition active state will become StateD:

wiki_features_history_shallow_02

Since we are using shallow history type, HSM will remember Parent2 as a history target for Parent1:

wiki_features_history_shallow_03

Since Parent2 has substates entry transition will be automatically executed and StateC will become active:

wiki_features_history_shallow_04

Deep history

Deep history pseudostate represents the most recent active configuration of the composite state that directly contains this pseudostate (e.g., the state configuration that was active when the composite state was last exited). A composite state can have at most one deep history element.

Deep history is indicated by a small circle containing an "H*". It applies to the state that contains it.

Let's look at the example. We have exactly same state machine, but now history type is set to "deep":

wiki_features_history_deep_01

While moving to StateD, HSM will save *StateE as a history target for Parent1:

wiki_features_history_deep_02

So after E2 transition to history state, our HSM will look exactly same as it's initial version:

wiki_features_history_deep_01

Timers

Timers are used to initiate transition logic from within HSM without any additional code. Some common examples of timers usage are:

  • cancel ongoing operation due to timeout;
  • repeating operation after delay.

Usage

To use a timer in your HSM you first need to register it using this API:

void registerTimer(const TimerID_t timerID, const HsmEventEnum event);
  • timerID - can be any value, but must be unique withing this instance of HSM;
  • event - event which should be triggered when timer expires.

Interacting with timers is part of State actions so registerStateAction() API should be used. You can start, stop or restart any of the registered timers.

Working with Variant values

Due to C++11 not having std::variant type, HSMCPP comes with it's own implementation of Variant container. It supports all basic types and some variations of STD containers.

Supported types

  • BYTE_1 (int8_t)
  • BYTE_2 (int16_t)
  • BYTE_4 (int32_t)
  • BYTE_8 (int64_t)
  • UBYTE_1 (uint8_t)
  • UBYTE_2 (uint16_t)
  • UBYTE_4 (uint32_t)
  • UBYTE_8 (uint64_t)
  • DOUBLE (double)
  • BOOL (bool)
  • STRING (std::string)
  • BYTEARRAY (std::vector)
  • LIST (std::list)
  • VECTOR (std::vector)
  • DICTIONARY (std::map<Variant, Variant>)
  • PAIR (std::pair<Variant, Variant>)

Working with Variant type

To create a Variant from a basic type:

Variant v1(7);
Variant v2 = Variant::make(7);

To get value our of Variant container you can use one of the toXXXXX() functions or value():

Variant v1("abc");
std::string s1 = v1.toString();
std::string s2 = *(v1.value<std::string>());

The difference between these two approaches is that toXXXXX() functions also try to convert internal value to requested type while value() returns a pointer to internal data.

  • toXXXXX()
    • pros
      • easy to use
      • automatically converts types (if possible)
    • cons
      • slower
      • some types are not available (for example only toInt64() and toUInt64() for numeric values)
  • value()
    • pros
      • fast
    • cons
      • unsafe
      • users are responsible to make sure they are using correct data type