Hello, yasmine!

"Hello, yasmine!" is yasmine's variation of "Hello, world!".

It is a very simple state machine that enters the state 'Waiting' on start-up. In the state 'Waiting' the output "waiting" is printed. When receiving a 'Hello event' a transition is taken that leads to the state 'Replying'. In the state 'Replying' the output "Hello, yasmine!" is printed on the screen. By an automatic transition the state machine returns to the state "Waiting" and is ready for further "Hello event"s.

The following diagram shows this state machine:

images/download/attachments/2523144/hello_yasmine.png

Walk-through

In this section the source code for the simple example 'Hello, yasmine!' state machine will be explained.

You can use this as a starting point for understanding how to use the yasmine C++ state machine framework library and what its basic concepts are.

Includes

#include <iostream>
#include "yasmine.hpp"

In the following table the bold header files names are originating from the yasmine library. The other ones are C++ standard library header files.

Header file

Content

<iostream>

console output

yasmine.hpp

collection of headers that are needed for building and running the state machine

Global variable and type aliases

const sxy::event_id HELLO_EVENT = 1;
typedef sxe::SX_UNIQUE_PTR< sxy::sync_state_machine > state_machine_uptr;

In this example, the event id is defined as a global variable, 'HELLO_EVENT' and it has the value 1. 'event_id' is a type alias for int values. Event IDs have to start at 1 as 0 is used for a completion event.

Then we create a type alias 'state_machine_uptr' for a unique pointer, using the SX_UNIQUE_PTR macro, to a state machine object.

Functions

In this example, five functions are defined:

  • reply

  • wait

  • setup_state_machine

  • check_state_machine_for_defects

  • main

reply

The function "reply" takes no input parameter. The function displays a message to console.

void reply()
{
std::cout << "Hello, yasmine!" << std::endl;
}

wait

This function takes no input parameter. The function displays a message to console.

void wait()
{
std::cout << "waiting" << std::endl;
}

setup_state_machine

The function 'setup_state_machine' takes a single input parameter (const std::string& _name) that is the name of the state machine that will be created. It returns a unique pointer to the newly created state machine.

state_machine_uptr state_machine = SX_MAKE_UNIQUE< sxy::sync_state_machine >( _name );

Construct an object of type state_machine and store it via a unique pointer.

sxy::composite_state& root_state = state_machine->get_root_state();
sxy::region& main_region = root_state.add_region( "main region" );

In the first line above, we get a reference to the root state of the state machine. We need this reference to be able to add regions. In these regions we can add other elements of the state machine.

In the second line, we used the add_region member function of the root state (the root state of the state machine is a composite state) creates a region with the given name (in this case 'main region') and add the region to the root state. The function will return a reference to the newly created region and thus it will be possible to add new states and pseudostates to the region.

add_region( _region_name )

Parameter name

Explanation

_name

The name of the region.

sxy::initial_pseudostate& initial_pseudostate = main_region.add_initial_pseudostate( "initial" );
sxy::simple_state& simple_state_waiting = main_region.add_simple_state( "waiting", Y_BEHAVIOR_FUNCTION2( wait ) );
sxy::simple_state& simple_state_replying = main_region.add_simple_state( "replying", Y_BEHAVIOR_FUNCTION2( reply ) );

The next step is to create the states and the pseudostates of the state machine. Elements will be created using add-functions that correspond to the type that is needed.

add_initial_pseudostate will create an initial pseudostate object with the given name and will add it to the region. It will return a reference to the newly added initial pseudostate. The reference will be needed later.

add_initial_pseudostate( _initial_state_name )

Parameter name

Explanation

_name

The name of the initial pseudostate.

add_simple_state will create a simple state with the given name, given 'entry', 'exit' and 'do' activities, if there is any. In the case of "Hello, yasmine!" we just use the 'do' activity, so the other two functors for behaviors will be nulls (It is possible not to write them any more because are default defined as empty functors for behaviors). Then the simple state is added to the region whose add-function was called. In this case, the simple state 'Waiting' will be added to the 'l_main_region' and will execute the 'wait' behavior. The function will return a reference to the newly created simple state. We store the reference for later use.

The second use of the add_simple_state creates the 'Replying' simple state that will execute the 'reply' behavior.

Both 'wait' and 'reply' are created using the macro Y_BEHAVIOR_FUNCTION2 that takes a free function name as parameter and will create a wrapper functor (lambda function).
In case of "Hello, yasmine!", the behaviors will just display some messages in the console.

add_simple_state( _name, _behavior, _entry_behavior, _exit_behavior )

Parameter name

Explanation

_name

The name of the simple state.

_behavior

The 'do' behavior of the simple state.

_entry_behavior

The 'entry' behavior of the simple state.

_exit_behavior

The 'exit' behavior of the simple state.

state_machine->add_transition( HELLO_EVENT, simple_state_waiting, simple_state_replying );
state_machine->add_transition( sxy::Y_COMPLETION_EVENT_ID, initial_pseudostate, simple_state_waiting );
state_machine->add_transition( sxy::Y_COMPLETION_EVENT_ID, simple_state_replying, simple_state_waiting );

Now it is time to create the transitions using the overloaded add_transition member function of the state machine. This will create and automatically add the transition to the state machine.

add_transition( _event_id, _source, _target, _kind, _guard, _behavior )

Parameter name

Explanation

_event_id

Event ID for which the transition is fired.

_source

Reference to the source vertex of the transition.

_target

Reference to the target vertex of the transition

_kind

Transition kind. This will be an enum value form transition_kind.

_guard

A functor (lambda function) that will be used as a guard of the transition

_behavior

A functor (lambda function) that will be used as a behavior of the transition.

In the case of "Hello, yasmine!", there are three transitions. One from the initial state to the 'Waiting' state (from now 'transition 1'), one from the 'Waiting' state to the 'Replying' state (this one has a trigger; from now 'transition 2') and one from the 'Replying' state to the 'Waiting' state (from now 'transition 3').

Transitions 'transition 1' and 'transition 3' will be executed automatically, so the event ID is Y_COMPLETION_EVENT_ID.

All three transitions have the kind EXTERNAL, have no guards and no behaviors. This are default valued, so we don't need to add them as parameters to the add_transition member function.

Transition 'transition 1' has the initial pseudostate 'l_initial_pseudostate' as source and the simple state 'l_simple_state_waiting' as target.

Transition 'transition 2' has the simple state 'l_simple_state_waiting' as source and the simple state 'l_simple_state_replying' as a target. It will be enabled for the event 'HELLO_EVENT'.

Transition 'transition 3' has the simple state 'l_simple_state_replying' as source and the simple state 'l_simple_state_waiting' as a target.

return ( sxe::move( state_machine ) );

The function 'setup_state_machine' will return the unique pointer to the newly created state machine by moving the unique pointer.

check_state_machine_for_defects

bool check_state_machine_for_defects( const sxy::sync_state_machine& _state_machine )

The function 'check_state_machine_for_defects' is taking a const reference to the state machine as an input parameter.

sxy::state_machine_defects defects;

Inside the function a state_machine_defects object is created. It will store all (potential) defects of the state machine.

const bool state_machine_has_no_defects = _state_machine.check( defects );
if( !state_machine_has_no_defects )
{
sxy::write_defects_to_log( defects );
}
return( state_machine_has_defects );

Now the check function is called with the defects object as parameter. This will fill the defects object with all the defects that are found when analysing the state machine. It will also return true if there are defects and false otherwise.

For detailed information about the check function, refer to the dedicated page Checking for defects.

If there are defects, the defects will be dumped in the log (in this case in the console because 'cout_logger' is used) by calling the function 'write_defects_to_log'.

The boolean result value of the check will be returned by the function 'check_state_machine_for_defects'.

main

The main function is (of course) the entry point of the application. In this function the log manager will be instantiated, started and stopped. The state machine is created, checked, and ran here as well.

int error_code = 0;
 
hermes::log_manager_template<hermes::std_timestamp_policy>& log_manager = hermes::log_manager::get_instance();
log_manager.set_log_level( hermes::log_level::LL_FATAL );
log_manager.add_logger( SX_MAKE_UNIQUE< hermes::cout_logger >() );
log_manager.run();
sxy::version::log_version();

The error code is set to zero, that means the application is started with the premise that there are no errors.

Next, in this block of code we get a reference to the log manager (a singleton). Then the log level of the logger is set to 'FATAL' and a console logger is added. The log level 'FATAL' will log fatal errors and nothing else. After these settings were made, the log manager is started. For a detailed discussion of logging refer to its documentation.

const state_machine_uptr hello_yasmine_state_machine = setup_state_machine( "hello yasmine state machine" );

After this, the state machine is created by calling the function 'setup_state_machine'.

if( check_state_machine_for_defects( *hello_yasmine_state_machine ) )
{
hello_yasmine_state_machine->run();
try
{
hello_yasmine_state_machine->fire_event( sxy::event_impl::create( HELLO_EVENT ) );
hello_yasmine_state_machine->fire_event( sxy::event_impl::create( HELLO_EVENT ) );
hello_yasmine_state_machine->fire_event( sxy::event_impl::create( HELLO_EVENT ) );
hello_yasmine_state_machine->halt();
}
catch( const std::exception& exception )
{
SX_LOG( hermes::log_level::LL_FATAL, "Unhandled exception: '%'.", exception.what() );
error_code = 1;
}
catch( ... )
{
SX_LOG( hermes::log_level::LL_FATAL, "Unknown exception!" );
error_code = 2;
}
}
else
{
error_code = 3;
}

The state machine is checked for defects by calling the function 'check_state_machine_for_defects'. If there is a defect, it will be logged in the check function and the error code will be set to 3.

If there are no defects, the state machine is started and 3 events are created by calling the static member function create (belongs to event_impl class) and then they are fired.

The methods run and fire_event return false, if a terminate pseudostate was reached. In this case, the state machine design contains no terminate pseudostate, so we don't care about the return values. After all events were fired, the state machine is stopped by calling the halt method.

With a try/catch we handle possible exceptions. Also, in the catch block, we log the exception.

log_manager.halt_and_join();
return ( error_code );

The log manager is stopped and because it's working in another thread, it needs to be joined. We make this in a single step using halt_and_join method.

And finally, the main function will return the error code. The error code will be 0, if no error occurred.

The resulting output will be:

waiting
Hello, yasmine!
waiting
Hello, yasmine!
waiting
Hello, yasmine!
waiting

Source code

The source code can be found on GitHub.