In my quest to search or develop the 'perfect' state machine I have built the following class. With the class you can make a state machine object which comes with timing and transition methods. Being an embedded developer who hobbies a lot, this class is compliant to the HAL of the Arduino platform.
My aim was to code relative complex tasks while causing as few bugs as possible. That is why I tried to make a painfully clear syntax and an easy to use class. I'll start with the usage.
As part of my bug prevention, I start with simple sphere diagrams. They are used to generate the state machine skeleton. I already filled mine in and it demonstrates all methods of the class.
I recommend you start from the bottom where the state machine is to be found
// HEADER FILES
#include <Arduino.h>
#include "demoStatemachine.h"
#include "src/macros.h"
#include "src/stateMachineClass.h"
static StateMachine sm ; // static state machine object, you can have more than 1 state machine per source file
// but I rarely use more than 1 state machine in a source file
#define beginState testRepeat // by default this line is commented...
#ifndef beginState
#error beginState not defined // ...so a programmer cannot forget to set the begin state
#endif
// VARIABLES
uint8_t timeoutFlag ;
extern uint8 serialByte ; // global variable which contains the last received serial byte
// FUNCTIONS
extern void demoStatemachineInit(void)
{
sm.nextState( beginState, 0 ) ;
}
// STATE FUNCTIONS
StateFunction( testRepeat ) // = bool testRepeatF()
{
if( sm.entryState() ) // runs once upon entering this state
{
Serial.println( "entering test repeat ") ;
}
if( sm.onState() ) // keeps running until sm.exit() is called
{
if( sm.repeat( 5000 ) )
{
Serial.println ("I say helle every 5000ms" ) ;
}
if( serialByte == 'S') sm.exit() ;
}
if( sm.exitState() ) // runs once upon the last call to this state machine function
{
Serial.println( "leaving test repeat ") ;
}
return sm.endState() ; // is true when exitState has run, signales the state machine to pick the next state
}
StateFunction( testReboot )
{
if( sm.entryState() )
{
Serial.println( "entering test Reboot ") ;
}
if( sm.onState() )
{
if( serialByte == 'R' ) { sm.reboot( 500 ) ; } // lets entry state run again in 500ms
if( serialByte == 'S' ) { sm.exit() ; }
}
if( sm.exitState() )
{
Serial.println( "leaving test Reboot ") ;
}
return sm.endState() ;
}
StateFunction( testTimeout )
{
if( sm.entryState() )
{
Serial.println( "entering test Timeout ") ;
timeoutFlag = false ;
sm.setTimeout( 10000 ) ;
}
if( sm.onState() )
{
if( sm.timeout() )
{
timeoutFlag = true ;
sm.exit() ;
}
else if( serialByte == 'S' )
{
sm.exit() ;
}
}
if( sm.exitState() )
{
if( timeoutFlag ) Serial.println("10 seconds passed, clock beat you") ;
else Serial.println("you beat the clock") ;
}
return sm.endState() ;
}
#define entryState if( sm.entryState() ) // this usually on top of the file, cannot use both macro and normal style in 1 file
#define onState if( sm.onState() )
#define exitState if( sm.exitState() == 0 ) return 0 ; \
else
StateFunction( testIdleState )
{
entryState // if( sm.entryState() )
{
Serial.println( "entering test idle, press any key to stop program") ;
}
onState // if( sm.onState() )
{
if( serialByte == 'S')
{
sm.exit() ;
}
}
exitState // if( sm.exitState() == 0 ) return 0 ; else
{
Serial.println( "leaving the test idle state ") ;
return 1 ;
}
}
// STATE MACHINE
extern uint8_t demoStatemachine()
{
STATE_MACHINE_BEGIN( sm ) //if() sm.run() ) switch( sm.getState() ) { // the 'hidden' sm.run() is what handles the delay between 2 following states
State(testRepeat) { // corresponds 101 with diagram
sm.nextState( testReboot, 1000 ) ; } // entering a non-zero time here, will cause a non-blocking delay before testReboot is run
// this is my equivalent of the so-called off state
State(testReboot) {
sm.nextState( testTimeout, 2000 ) ; }
State(testTimeout) { // = break ; case testTimeout: if( testTimeoutF() )
if( timeoutFlag ) sm.nextState( testIdleState, 30000 ) ;
else sm.nextState( testRepeat, 0 ) ; }
State(testIdleState) {
sm.nextState( demoStatemachineIDLE, 0 ) ; } // this state does not actually exists
// states without outgoing arrows default to idle state
STATE_MACHINE_END( sm ) // break ; } return sm.getState() ;
}
For every Sphere there is a State()
.
For every arrow/transition there is a sm.nextState()
. All code concerning when and why a next state is picked, goes anywhere between State()
and nextState()
. This is opposed to the transition table.
A 'State' is actually a case label which performs a function call to the corresponding state + F.
So State( foo );
is actually break ; case foo: if( fooF() )
When foof()
returns 1, the state function is finished and a next state will be picked.
Similarly, StateFunction()
is also a macro StateFunction( foo )
becomes bool fooF()
. There are several reasons why I use macros for this. First it allows me to use the same enumerated name in both State()
and StateFunction()
(DRY, don't repeat yourself) Secondly a StateFunction
is IMO painfully clear syntax, it is a function which distinguishes itself from regular
functions by being part of a state machine.
The matching header file contains the enumerated states and two function prototypes. The reason why these state names are in the header file, is that other code may compare the current state of state machine with one of it's state names. Take note that the first state demoStatemachineIDLE
does not actually exist. A state which lacks an outgoing arrow or does not transition to another state (testIdleState
in this case) automatically defaults to this idle state.
enum demoStatemachineStates
{
demoStatemachineIDLE,
testRepeat,
testReboot,
testTimeout,
testIdleState
} ;
extern uint8_t demoStatemachine(void) ;
extern void demoStatemachineInit(void) ;
In this structure, you have the entryState
, onState
, exitState
and offState
. The offState
is not an actual stretch of code, after all one does not do anything in the offState
. If you enter a non-zero value in sm.nextState( bar, 500 ) ;
barF will be called 500ms later. It is this 500ms delay which I deem the offState
.
Working as embedded designer I wanted to be flexible. I can use Serial input, Wi-Fi input, sensor input and time input. That is why I favour this structure above lists with function points, events and state transitions tables. I keep code where it belongs.
To use the state machine you initialize it once, and you call the function
#include "src/macros.h" // just contains // #define DEBUG
#include "demoStatemachine.h"
uint8_t serialByte ;
void setup()
{
initIO() ;
Serial.begin( 115200 ) ;
demoStatemachineInit() ; // initialize state machine
}
void loop()
{
if( Serial.available() > 0 ) serialByte = Serial.read() ;
else serialByte = 0 ;
if( demoStatemachine() == demoStatemachineIDLE ) // you can use the returned state for anything, usefull for nested state machine
{
Serial.println("program ended");
bool PIGS_CAN_FLY = !false ;
while( PIGS_CAN_FLY == true ) {;} // loop forever
}
}
The state machine returns its current state. When it reaches the IDLE state, it will return 0. You can make use of this to let state machines communicate to each other or call state machines from other state machines.
By keeping the state machine on the bottom of the file I can also navigate incredibly fast to the state functions.
That leaves us with the state machine class itself. The header file
#include <Arduino.h>
#include "macros.h"
#define StateFunction( x ) bool x##F()
#ifndef DEBUG
#define State(x) break; case x: if(x##F())
#else
#define State(x) break; case x: if(sm.runOnce) Serial.println(#x); if(x##F()) // if debug is defined, all states are stringerized and printed when entry state is run
#endif
#define STATE_MACHINE_BEGIN(x) if( x.run() ) switch( x.getState() ) {
#define STATE_MACHINE_END(x) break ; } return x.getState() ;
class StateMachine {
public:
StateMachine() ;
void setState( uint8_t ) ;
uint8_t getState() ;
void nextState( uint8_t, uint32_t ) ;
uint8_t entryState() ;
uint8_t onState() ;
uint8_t exitState() ;
uint8_t run() ;
void setTimeout( uint32_t ) ;
uint8_t timeout() ;
void exit() ;
void reboot( uint32_t ) ;
uint8_t endState() ;
uint8_t repeat( uint32_t ) ;
#ifdef DEBUG
uint8_t runOnce ; // if debug is active, this must be public in order to print the state names
#endif
private:
#ifndef DEBUG
uint8_t runOnce ; // is usually private
#endif
uint8_t enabled ;
uint8_t exitFlag ;
uint32_t prevTime ;
uint32_t interval ;
uint8_t state;
uint8_t timeOutSet ;
} ;
The #ifdef DEBUG
directive is used to stringify the enumerated states with a different macro for State()
. If DEBUG
is defined, all enumerated states of all state machines will be printed to the monitor once when the entryState
is run. This allows one to verify the integrity of the state machine even before the actual coding.
The source file of the class
#include "stateMachineClass.h"
/**
* @brief creates an instance of the state machine class. Used to handle the flow
*/
StateMachine::StateMachine()
{
state = 0 ;
runOnce = true ;
exitFlag = false ;
}
/**
* @brief manually set a state
*
* @param The state
*
* @return N/A
*/
void StateMachine::setState( uint8_t _state )
{
state = _state ;
runOnce = true ;
exitFlag = false ;
}
/**
* @brief Lets the entry state to run again in a certain amount of time. Can be usefull from time to time
*
* @param delay delayed execution if desired
*
* @return N/A
*/
void StateMachine::reboot( uint32_t _interval )
{
runOnce = true ;
if( _interval )
{
enabled = 0 ;
prevTime = millis() ;
interval = _interval ;
}
}
/**
* @brief returns the current state variable
*
* @param N/A
*
* @return current state
*/
uint8_t StateMachine::getState()
{
return state ;
}
/**
* @brief function to handle the one time condition for the entry states
*
* @param N/A
*
* @return true or false
*/
uint8_t StateMachine::entryState()
{
uint8_t retVal = runOnce ;
runOnce = 0 ;
return retVal ;
}
/**
* @brief function to continously handle the on state, could be a macro
*
* @param N/A
*
* @return 1
*/
uint8_t StateMachine::onState()
{
return 1 ;
}
/**
* @brief function to handle the one time condition for the exit states
*
* @param N/A
*
* @return true or false
*/
uint8_t StateMachine::exitState()
{
return exitFlag ;
}
/**
* @brief Ensures the exit state of a state function will be executed
*
* @param N/A
*
* @return N/A
*/
void StateMachine::exit()
{
exitFlag = true ;
}
/**
* @brief sends signal to state machine if current state function is finished or not
*
* @param N/A
*
* @return exit flag
*/
uint8_t StateMachine::endState( )
{
return exitFlag ;
}
/**
* @brief sets a timestamp which can be used for timing within state functions
*
* @param timeout time in ms
*
* @return N/A
*/
void StateMachine::setTimeout( uint32_t time2run )
{
prevTime = millis() ;
interval = time2run ;
timeOutSet = true ;
}
/**
* @brief Monitors if the time in set by setTime() has passed
*
* @param N/A
*
* @return true or false
*/
uint8_t StateMachine::timeout()
{
if( (millis() - prevTime >= interval ) && timeOutSet == true )
{
timeOutSet = false ;
return 1 ;
}
return 0 ;
}
/**
* @brief Transisition from current state to the next state
*
* @param _state new state to execute
* @param _interval when to execute new state
*
* @return N/A
*/
void StateMachine::nextState( uint8_t _state, uint32_t _interval )
{
exitFlag = 0 ;
runOnce = 1 ;
if( _interval )
{
enabled = 0 ;
prevTime = millis() ;
interval = _interval ;
}
state = _state ;
}
/**
* @brief monitors the interval when there is an interval set between states
*
* @param N/A
*
* @return enabled flag
*/
uint8_t StateMachine::run()
{
if( enabled == 0 )
{
if( millis() - prevTime >= interval )
{
enabled = 1 ;
}
}
return enabled ;
}
/**
* @brief Called from within an if-statement, this method can be used for
* repeating parts of code with a certain interval
*
* @param N/A
*
* @return enabled flag
*/
uint8_t StateMachine::repeat( uint32_t _interval )
{
if( millis() - prevTime >= _interval )
{
prevTime = millis() ;
return true ;
}
return false ;
}
Generated code is compilable at start; the state machine will run even before coding a single word.
I am using this code extensively. I even added code-snippets to insert new state functions and states to save me the copy+paste trouble. So far it allows me to create code which sometimes even works on the first try. By keeping the code in the state functions as short as simple as possible, you get to prevent creating certain bugs. But I am guessing that this can be set for different state machine structures.
I am fully aware that most programmers are not particular fond of macros. I am asking for feedback; I am really interested in what others think of this code. But if your comment will only contain "cannot read through the macros" or something similar I ask you kindly not to comment at all.
EDIT: Real life example. For my work I program large machinery which use pneumatic cylinders. Not all cylinders come with sensors, so that is just a matter of setting an output and wait for half a second or so before taking the next step.
StateFunction( moveCylinder1 )
{
if( sm.entryState() )
{
digitalWrite( cylinder1, HIGH ) ;
}
if( sm.onState() )
{
// nothing to do really
sm.exit() ;
}
if( sm.exitState() )
{
// nothing to do really
}
return sm.endState() ;
}
...
State( moveCylinder1 ) {
sm.nextState( moveCylinder2, 500 ) ; } // just wait 500ms before moving the 2nd pneumatic cylinder.
Giving the simplicity of the matter, you could drop the "sub states" and just type this instead. I use code snippets to insert a complete function, but stripping it is possible and is part of the flexibility
StateFunction( moveCylinder1 )
{
digitalWrite( cylinder1, HIGH ) ;
return 1 ;
}
However, some cylinders do have sensors on one or both ends because they can get stuck. In following example, I want to set a timeout for the movement. If the sensor is not made on time, I want to reset the cylinder print a message, let the warning light blink and I want to be able to make a new attempt with user input. This structure allow me to do this all in a single state function.
StateFunction( moveCylinder1 )
{
if( sm.entryState() )
{
digitalWrite( cylinder1, HIGH ) ; // set cylinder out
sm.setTimeout( 1000 ) ; // set timeout
}
if( sm.onState() )
{
if( digitalRead( endSensor) ) sm.exit() ; // if sensor is made before timeout, state machine will continu
else if( sm.timeout() ) // if timeout occurs
{
digitalWrite( cylinder1, LOW ) ; // set cylinder back
blinkWarningLight = true ; // draw attention of machine operator
Serial.println( "WARNING, cylinder 1 took too long, press restart switch" ) ;
// sm.reboot( 1000 ) ; // you can automatically re attempt to set the cylinder again and make a few attempts
}
} // OR
if( resetButtonState == FALLING ) sm.reboot( 0 ) ; // you wait on user input, this resets the state by letting the entry state run again
if( sm.exitState() )
{
blinkWarningLight = false ; // clear the warning light
Serial.println( "EVERYTHING OK" ) ;
}
return sm.endState() ;
}
...
State( moveCylinder1 ) {
sm.nextState( moveCylinder2, 0 ) ; } // we know the cylinder is in position, so no need to wait