Code Generation - igor-krechetov/hsmcpp GitHub Wiki

Overview

In general it's totally fine to define HSM structure manually in code. But in real projects we often have to deal with:

  • state machine complexity (understanding states logic from code can becomes almost impossible even with as little as 10 different states);
  • synchronizing implementation and state diagrams for documentation;
  • copy-paste mistakes.

To help deal with these issues hsmcpp library comes with scxml2gen utility. It uses state machines defined in SCXML format and allows to:

  • generate C++ code;
  • generate PlantUML state diagrams.

SCXML

From Wikipedia:

SCXML stands for State Chart XML: State Machine Notation for Control Abstraction. It is an XML-based markup language that provides a generic state-machine-based execution environment based on Harel statecharts.

SCXML is able to describe complex finite state machines. For example, it is possible to describe notations such as sub-states, parallel states, synchronization, or concurrency, in SCXML.

SCXML format specification can be found on W3 official website.

Limitations and requirements for SCXML files

SCXML format was designed to describe both structure and implementation inside a single file. But since we are using it only to define structure a lot of SCXML features will be ignored.

Supported SCXML tags and attributes

  • <scxml>
    • initial
    • xmlns
  • <state>
  • <transition>
    • event
    • cond
    • target
  • <parallel>
  • <initial>
  • <final>
    • id
  • <history>
  • <onentry>
  • <onexit>
  • <invoke>
    • srcexpr
  • <script>
  • <xi:include> (see Examples)

Any other tags and attributes will be ignored during parsing.

Specifying callbacks

There are 5 types of callbacks that could be defined for HSM:

  • on state changed:
<state id="state_1">
    <invoke srcexpr="onState1"/>
</state>
  • on state entering:
<state id="state_1">
    <onentry>
        <script>onEnteringState1</script>
    </onentry>
</state>
  • on state exiting:
<state id="state_1">
    <onexit>
        <script>onExitingState1</script>
    </onexit>
</state>
  • on transition:
<state id="state_1">
    <transition event="NEXT_STATE" target="state_2">
        <script>onNextStateTransition</script>
    </transition>
</state>
  • on condition check:
<state id="state_1">
    <transition event="NEXT_STATE" cond="checkNextStateTransition" target="state_2"/>
</state>

Callback names should comply with C++ identifier naming rules.

SCXML editors

There doesn't seem to be too many available free SCXML editors. Here are the ones worth mentioning:

  • Qt Creator.

    • The easiest one to start with, though personally, I feel like arranging transitions is a bit sloppy. I hope they will add "snap-to-grid" feature in future releases.
    • Note: using parallel element in version 4.14.2 on Ubuntu crashes editor when saving file. This happens after editing state machine for awhile and doesn't seem to occur if only a couple of modifications were made. Hopefully will get fixed. Editing HSM in Qt Creator
  • scxmlgui

    • Simple editor written in Java. Requires a bit more manual writing than Qt Creator, but does the job.
    • compiled binary can be found here

Editing HSM in scxmlgui

Using Qt Creator

Basics of how to define a state machine in Qt Creator can be checked here:

Now let's go through working with hsmcpp specific features.

Specifying callbacks in editor

There are 4 types of callbacks (see States and Transitions for details):

  • state entering
  • state changed
  • state exiting
  • transition

They are specified as a C++ callback name which will be generated in HSM class.

State changed callback can be specified in state->Invoke->srcexp or state->Invoke->src: State callback

State entering callback can be specified in state->onentry->script->Content: State entering callback

State exiting callback can be specified in state->onexit->script->Content: State exiting callback

Transition callback can be specified in transition->script->Content: State exiting callback

Working with timers and actions

State actions in HSM provide following operations:

  • start timer
  • stop timer
  • restart timer
  • trigger transition on timer
  • trigger a regular transition

Actions can be specified in:

  • state->onentry
  • state->onexit

Timer start action

Command format is:

  • start_timer(<timer_id>, < interval in milliseconds >, <is singleshot: true | false>)
  • stop_timer(<timer_id>)
  • restart_timer(<timer_id>)
  • transition(<event_id>, {arg1, arg2, ...})
    • Note: arguments are optional. at the moment only strings and numbers are supported

Using any of the above timer actions will result in availability of timer event. Name format: ON_TIMER_<timer_id> Timer transition

Conditional transitions

Condition for transitions can be specified in transition->cond: Conditional transition

Possible formats:

  • <callback_name> is false
  • <callback_name> is true
  • <callback_name> (same as "is true")

Conditional entry points

Hsmcpp library supports conditional entry points, but defining them through editor directly is not possible.

First, create your substates, define an entry point and add a single transition. Multiple entries

Now you need to save the file and open SCXML file in some text editor (Qt Creator doesn't allow editing SCXML files directly). Find your section. Original XML

Modify it to look like this (ignore qt:editorinfo tag). Add as many transitions as you need. Modified XML

After reloading SCXML file in Qt Creator your HSM should look like this. Conditional entry points

Using scxml2gen

Command line arguments

scxml2gen works in two modes:

  • C++ code generation
  • plantuml state diagram generation

To get a list of supported arguments run:

scxml2gen.py -h
usage: scxml2gen.py [-h] (-code | -plantuml) -scxml SCXML [-class_name CLASS_NAME] [-class_suffix CLASS_SUFFIX] [-template_hpp TEMPLATE_HPP] [-template_cpp TEMPLATE_CPP] [-dest_hpp DEST_HPP] [-dest_cpp DEST_CPP]
                    [-dest_dir DEST_DIR] [-out OUT]

State machine code/diagram generator

optional arguments:
  -h, --help            show this help message and exit
  -code                 generate C++ code based on hsmcpp library. Supported arguments: -class_name, -class_suffix, -template_hpp, -template_cpp, -dest_hpp, -dest_cpp, -dest_dir
  -plantuml             generate plantuml state diagram
  -scxml SCXML, -s SCXML
                        path to state machine in SCXML format
  -class_name CLASS_NAME, -c CLASS_NAME
                        class name used in generated code
  -class_suffix CLASS_SUFFIX, -cs CLASS_SUFFIX
                        suffix to append to class name (default: Base)
  -template_hpp TEMPLATE_HPP, -thpp TEMPLATE_HPP
                        path to HPP template file
  -template_cpp TEMPLATE_CPP, -tcpp TEMPLATE_CPP
                        path to CPP template file
  -dest_hpp DEST_HPP, -dhpp DEST_HPP
                        path to file in which to store generated HPP content (default: ClassSuffixBase.hpp)
  -dest_cpp DEST_CPP, -dcpp DEST_CPP
                        path to file in which to store generated CPP content (default: ClassSuffixBase.cpp)
  -dest_dir DEST_DIR, -d DEST_DIR
                        path to folder where to store generated files (ignored if -dest_hpp and -dest_cpp are provided)
  -out OUT, -o OUT      path for storing generated Plantuml file (only for -plantuml)

Code generation example

Let's look at the sample command to generate HSM from SCXML:

python3 ./tools/scxml2gen/scxml2gen.py -code -s ./examples/02_generated/02_generated.scxml -c SwitchHsm -thpp ./tools/scxml2gen/template.hpp -tcpp ./tools/scxml2gen/template.cpp -d ./

This will generate two files:

SwitchHsmBase.hpp

// Content of this file was generated

#ifndef __GEN_HSM_SWITCHHSMBASE__
#define __GEN_HSM_SWITCHHSMBASE__

#include <hsmcpp/hsm.hpp>

enum class SwitchHsmStates
{
    On,
    Off,
};

enum class SwitchHsmEvents
{
    SWITCH,
};

class SwitchHsmBase: public HierarchicalStateMachine<SwitchHsmStates, SwitchHsmEvents>
{
public:
    SwitchHsmBase();
    virtual ~SwitchHsmBase();

protected:
    void configureHsm();

// HSM state changed callbacks
protected:
    virtual void onOff(const VariantList_t& args) = 0;
    virtual void onOn(const VariantList_t& args) = 0;

// HSM state entering callbacks
protected:

// HSM state exiting callbacks
protected:

// HSM transition callbacks
protected:

// HSM transition condition callbacks
protected:
};

#endif // __GEN_HSM_SWITCHHSMBASE__

SwitchHsmBase.cpp

// Content of this file was generated

#include "SwitchHsmBase.hpp"

SwitchHsmBase::SwitchHsmBase()
    : HierarchicalStateMachine<SwitchHsmStates, SwitchHsmEvents>(SwitchHsmStates::On)
{
    configureHsm();
}

SwitchHsmBase::~SwitchHsmBase()
{}

void SwitchHsmBase::configureHsm()
{
    registerState<SwitchHsmBase>(SwitchHsmStates::On, this, &SwitchHsmBase::onOn, nullptr, nullptr);
    registerState<SwitchHsmBase>(SwitchHsmStates::Off, this, &SwitchHsmBase::onOff, nullptr, nullptr);


    registerTransition<SwitchHsmBase>(SwitchHsmStates::On, SwitchHsmStates::Off, SwitchHsmEvents::SWITCH, this, nullptr);
    registerTransition<SwitchHsmBase>(SwitchHsmStates::Off, SwitchHsmStates::On, SwitchHsmEvents::SWITCH, this, nullptr);
}

Using custom templates

scxml2gen comes with a predefined template for hpp and cpp files:

It it doesnt satisfy your project needs you can define your own. Currently scxml2gen supports following variables:

  • CLASS_NAME: name of generated class (constructed as class name + suffix provided as arguments)
  • ENUM_STATES: name of the enum containing HSM states (constructed as class name + "States")
  • ENUM_EVENTS: name of the enum containing HSM events (constructed as class name + "Events")
  • ENUM_TIMERS: name of the enum containing HSM timers (constructed as class name + "Timers")
  • ENUM_STATES_ITEM: (BLOCK item) list of state names
  • ENUM_EVENTS_ITEM: (BLOCK item) list of event names
  • ENUM_TIMERS_ITEM: (BLOCK item) list of timer names
  • INITIAL_STATE: name of the initial state
  • HSM_STATE_ACTIONS: declaration of HSM onStateChanged callbacks (each function is placed on a new line)
  • HSM_STATE_ENTERING_ACTIONS: declaration of HSM onEntering callbacks (each function is placed on a new line)
  • HSM_STATE_EXITING_ACTIONS: declaration of HSM onExit callbacks (each function is placed on a new line)
  • HSM_TRANSITION_ACTIONS: declaration of HSM transition callbacks (each function is placed on a new line)
  • HSM_TRANSITION_CONDITIONS: declaration of HSM condition callbacks (each function is placed on a new line)
  • HPP_FILE: hpp file name
  • REGISTER_STATES: code registering all HSM states
  • REGISTER_SUBSTATES: code registering all HSM substates
  • REGISTER_TRANSITIONS: code registering all HSM transitions
  • REGISTER_TIMERS: code registering all HSM timers
  • REGISTER_ACTIONS: code registering all HSM actions

Variables can be referenced in two ways:

  • @VARIABLE@: inserts variable as-is (for example: @CLASS_NAME@ => SwitchHsmBase)
  • %VARIABLE%: inserts variable in upper case (for example: %CLASS_NAME% => SWITCHHSMBASE)
// Content of this file was generated

#ifndef __GEN_HSM_%CLASS_NAME%__
#define __GEN_HSM_%CLASS_NAME%__

#include <hsmcpp/hsm.hpp>

enum class @ENUM_STATES@
{
    @ENUM_STATES_DEF@
};

...

Integrating code generation to project

Ideally, code generation should be integrated into a build process to prevent any need for copy-pasting. Example of how to do so can be found in /examples/02_generated.

To make invoking scxml2gen during build more convenient two CMake functions are provided:

  • function(generateHsm genTarget scxml className destDirectory outSrcVariableName)
    • Generates hpp and cpp file in destDirectory.
    • IN arguments
      • genTarget: new target name (used later for add_dependencies() call)
      • scxml: path to scxml file
      • className: class name to use when generating code (default suffix will be added)
      • destDirectory: path to directory where to save generated files
    • OUT arguments
      • outSrcVariableName: name of the variable where to store path to generated cpp file
  • function(generateHsmEx genTarget scxml className classSuffix templateHpp templateCpp destHpp destCpp)
    • Extended version of generateHsm which allows to provide custom template and destination files path
    • IN arguments
      • genTarget: new target name (used later for add_dependencies() call)
      • scxml: path to scxml file
      • className: class name to use when generating code (default suffix will be added)
      • classSuffix: suffix to append to class name when generating code
      • destDirectory: path to directory where to save generated files
      • templateHpp, templateCpp: path to HPP and CPP templates
      • destHpp, destCpp: destination path for generated HPP and CPP files

They will be automatically available to you when including root CMakeLists.txt file in your project.

Here is an example CMake script to build generate and build a simple HSM. Important points here are:

  • using add_dependencies(). generateHsm() just and a custom target, so without anyone depending on it nothing will be generated.
  • the last argument to generateHsm() must be a string with a variable name (not variable itself!).
  • adding generated cpp file to your executable.
set(BINARY_NAME_02 "02_generated")

# create folder for generated files
set(GEN_DIR ${CMAKE_BINARY_DIR}/gen)
file(MAKE_DIRECTORY ${GEN_DIR})

generateHsm(GEN_02_HSM ./02_generated.scxml "SwitchHsm" ${GEN_DIR} "GEN_OUT_SRC")

add_executable(${BINARY_NAME_02} 02_generated.cpp ${GEN_OUT_SRC})
add_dependencies(${BINARY_NAME_02} GEN_02_HSM)
target_include_directories(${BINARY_NAME_02}
    PRIVATE
        ${HSMCPP_STD_INCLUDE}
        ${CMAKE_BINARY_DIR}
)
target_link_libraries(${BINARY_NAME_02} PRIVATE ${HSMCPP_STD_LIB})

Implementation itself is very similar to a HelloWorld example, but now we don't need to manually register HSM structure.

Suggestion:

  • use override keyword for callbacks. This will save you a lot of effort if callback gets renamed/removed from HSM definition.
  • you can use protected inheritance for generated class (SwitchHsmBase) if you want to prevent clients (of SwitchHsm) to directly access HSM API.
  • you can generate your files anywhere, but doing it inside your build folder will prevent you from accidentally submitting them.
#include <chrono>
#include <thread>
#include <hsmcpp/HsmEventDispatcherSTD.hpp>
#include "gen/SwitchHsmBase.hpp"

using namespace std::chrono_literals;

class SwitchHsm: public SwitchHsmBase
{
public:
    virtual ~SwitchHsm(){}

// HSM state changed callbacks
protected:
    void onOff(const VariantList_t& args) override
    {
        printf("Off\n");
        std::this_thread::sleep_for(1000ms);
        transition(SwitchHsmEvents::SWITCH);
    }

    void onOn(const VariantList_t& args) override
    {
        printf("On\n");
        std::this_thread::sleep_for(1000ms);
        transition(SwitchHsmEvents::SWITCH);
    }
};

int main(const int argc, const char**argv)
{
    std::shared_ptr<hsmcpp::HsmEventDispatcherSTD> dispatcher = std::make_shared<hsmcpp::HsmEventDispatcherSTD>();
    SwitchHsm hsm;

    hsm.initialize(dispatcher);
    hsm.transition(SwitchHsmEvents::SWITCH);

    dispatcher->join();

    return 0;
}

Generating PlantUML diagrams

PlantUML is an amazing tool that allows creating a lot of different diagram types using text files. Since I couldn't find any way to automatically generate images based on SCXML or export them to PlantUML format I added additional functionality to scxml2gen application.

To generate a PlantUML file from SCXML simply call:

python3 ./tools/scxml2gen/scxml2gen.py -plantuml -s ./tests/scxml/multilevel.scxml -o ./multilevel.plantuml

You can also use CMake function generateHsmDiagram() to do it automatically during build. You can check example of its usage in /examples/04_history/CMakeLists.txt.

⚠️ **GitHub.com Fallback** ⚠️