|
|
State machines are a powerful tool used to model the behavior of various systems.
|
|
|
They are composed of finite number of state states, and can be in only one state at a time (Finite State Machines), or in multiple, nested states in the same moment (Hierarchical State Machines).
|
|
|
State machines can _transition_ from one state to the other and perform additional task in response to an _event_.
|
|
|
|
|
|
## Code Implementation
|
|
|
|
|
|
In the OBSW, an FSM is a class that implements the `FSM` interface. This interface defines an *ActiveObject*(i.e. an object that runs a thread) with its own *SynchronizedQueue*(event queue). The AO thread continuously checks the event queue: when a new event is received, the FSM will execute a particular function (*StateHandler*) which encodes the current state of the FSM. At the end of the StateHandler, the FSM can either undergo a *transition* (changes the current state handler) or remain in the same state.
|
|
|
|
|
|
Examples of the implementation of the state machines can be found in the examples folder in the skyward-boardcore repository:
|
|
|
`skyward-boarcore/src/entrypoints/examples/`.
|
|
|
Link: [Here](https://git.skywarder.eu/r2a/skyward-boardcore/tree/master/src/entrypoints/examples).
|
|
|
|
|
|
## Rules & Best Practices
|
|
|
These are rules & best practices referring to the particular implementation of state machines use in skyward-boardcore. These practices are aimed at improving readability and at avoiding common causes of error when writing state machines. Se also the [Events Cheatsheet](Events-Cheatsheet).
|
|
|
|
|
|
|
|
|
### General rules
|
|
|
- **Always** handle `EV_ENTRY`, `EV_EXIT`, (`EV_INIT` only for hierarchical state machines) and the default case, even if you don't have anything to do in them.
|
|
|
- **DO NOT** perform transitions when handling `EV_EXIT`.
|
|
|
This is conceptually wrong: when a state machine is handling `EV_EXIT`, it means it is already performing a transition.
|
|
|
- When present, calls to `transition(...)` **must** be the last action performed by the state function.
|
|
|
`transition()` synchronously calls the current state function with `EV_EXIT`, and then the following state function with `EV_ENTRY`. Tasks executed after the call to `transition()` would be performed in a state that has already *exited*.
|
|
|
For example, if we call, from a state *S1*, a transition to state *S2* immediately followed by a printf, the order of execution would be:
|
|
|
1. (S1) transition S1 --> S2
|
|
|
2. (S1) EXIT
|
|
|
3. (S2) ENTRY
|
|
|
4. (S1) printf("I'm still in State S1!") // Not legal, since S1 has exited
|
|
|
|
|
|
One exception to this rule would be performing actions not pertinent to the state of the state machine, for example logging and displaying other information.
|
|
|
|
|
|
### Hierarchical State Machines rules
|
|
|
In addition to the general best practices, these apply only to Hierarchical State Machines.
|
|
|
1. **DO NOT** handle *EV_EMPTY*.
|
|
|
EV_EMPTY is used internally to navigate the hierarchy of the state machine during transitions.
|
|
|
2. "Initial" transition to a sub-state **MUST** be performed inside the `EV_INIT` case, and not in `EV_ENTRY`.
|
|
|
If handled in `EV_ENTRY` instead, every "transiting" entry (when we are transitioning into a child of the considered state) will cause an additional transition, causing undefined behavior.
|
|
|
|
|
|
3. **ALWAYS** return `HANDLED` when handling `EV_EXIT`.
|
|
|
Not doing so will cause the state machine to enter a infinite loop when performing a transition. I assure you this is *NOT* what you want.
|
|
|
|
|
|
### Other best practices
|
|
|
- Use braces around each *case* statement
|
|
|
- Avoid performing actions outside the switch statement in a state function.
|
|
|
Again, an exception are actions not related to the state machine operation such as logging.
|
|
|
- State functions should public, mainly to allow testing of the state machine.
|
|
|
- State names should begin with "state_" followed by the name of the state. while "state_" should be all lower case with a trailing underscore, the rest of the name must use CamelCaseNotation.
|
|
|
For example: `void state_CuttingDrogue(...)`
|
|
|
|
|
|
## Patterns
|
|
|
TBW
|
|
|
|
|
|
## Testing
|
|
|
|
|
|
### Checking the current state
|
|
|
Both finite state machines and hierarchical state machines provide a method to check the current state matches the one we provide:
|
|
|
```cpp
|
|
|
bool testState(void (T::*test_state)(const Event&))
|
|
|
```
|
|
|
for finite state machines.
|
|
|
```cpp
|
|
|
bool testState(State (T::*test_state)(const Event&))
|
|
|
```
|
|
|
for hierarchical state machines.
|
|
|
The single argument expects a function pointer to the state function: If the current state matches the provided state, the function will return true, else will return false.
|
|
|
For example, if we want to check if *S1* is the "first" state of the "FSMExample" state machine, we write:
|
|
|
```cpp
|
|
|
FSMExample fsm; // Our state machine object
|
|
|
if( fsm.testState(&FSMExample::state_S1) )
|
|
|
{
|
|
|
printf("S1 is the first state!\n");
|
|
|
}else
|
|
|
{
|
|
|
printf("S1 is NOT the first state!\n");
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### Synchronous testing
|
|
|
Testing state machines is quite problematic due to their asynchronous nature.
|
|
|
For simple tests, such as checking if the correct transition occurs after posting a specific event, we can work around the asynchronous side of the state machines and test transitions in a synchronous way. For this to be possible we need to have access to the `handleEvent(...)` method of the EventHandler class (from which state machines inherit), in order process events immediately, without adding them to the event queue.
|
|
|
To gain access to this function, which is at *protected* access level, write
|
|
|
`#define protected public` before including the state machine header in your test source.
|
|
|
DO NOT define this in an header or anything else than a test source file. We do not want this define to end up in production code!
|
|
|
Once we've done this, there are a few helper methods to let us check transition correctness with ease. Those methods are defined in the header `state_machine_test_helper.h`, and are:
|
|
|
```cpp
|
|
|
template <class FSM_type>
|
|
|
bool testFSMTransition(FSM_type& fsm, const Event& ev, void (FSM_type::*expected_state)(const Event&))
|
|
|
```
|
|
|
```cpp
|
|
|
template <class HSM_type>
|
|
|
bool testHSMTransition(HSM_type& hsm, const Event& ev, State (HSM_type::*expected_state)(const Event&))
|
|
|
```
|
|
|
The former is used with Finite State Machines, the latter with Hierarchical State Machines.
|
|
|
From now on, we will use FSMs as example, and thus the method `testFSMTransition`, but everything is mostly the same for HSMs.
|
|
|
|
|
|
This function takes 3 arguments and 1 template argument. The template argument doesn't need to be specified, as it will be deduced automatically by the compiler.
|
|
|
The 3 fuction arguments are:
|
|
|
1. *fsm*: A reference to the State Machine object you want to test
|
|
|
2. *ev*: An event that will be posted to the state machine provided by argument 1
|
|
|
3. *expected_state*: Function pointer to the state that the state machine should find itself after handling the event.
|
|
|
|
|
|
For example, if we want to check that a state machines will move from the current state to the state *S2* after receiving *EV_A*, we will write (using the Catch framework, see [Testing](https://git.skywarder.eu/r2a/skyward-boardcore/wikis/Testing)):
|
|
|
```cpp
|
|
|
FSMExample fsm; // Our state machine object
|
|
|
REQUIRE( testFSMTransition(fsm, Event{EV_A}, &FSMExample::state_S2) );
|
|
|
```
|
|
|
|
|
|
This test will pass if the state machine is in state *S2* after receiving the event, and will fail if the state machine is in any other state, and even if no transition has occured.
|
|
|
|
|
|
As a final note, remember to NOT call fsm.start() when you are doing synchronous testing, as it will start the state machine thread and can cause unexpected and not repeatable results!
|
|
|
|
|
|
### Asynchronous testing
|
|
|
Synchronous testing is not able to test the state machine - event broker interaction, and will not allow to test things like dalayed events and others. Two similar methods to the one described in the previous paragraph are provided, but this time these methods will post the event to the state machine's event queue, wait for it to process it asynchronously and only then check if a transition has occured.
|
|
|
These two methods are defined in the same header, and are:
|
|
|
|
|
|
```cpp
|
|
|
template <class FSM_type>
|
|
|
bool testFSMAsyncTransition(FSM_type& fsm, const Event& ev, uint8_t topic,
|
|
|
void (FSM_type::*expected_state)(const Event&),
|
|
|
EventBroker& broker = *sEventBroker)vent&),
|
|
|
EventBroker& broker = *sEventBroker)
|
|
|
```
|
|
|
|
|
|
```cpp
|
|
|
template <class HSM_type>
|
|
|
bool testHSMAsyncTransition(HSM_type& hsm, const Event& ev, uint8_t topic,
|
|
|
State (HSM_type::*expected_state)(const Event&),
|
|
|
EventBroker& broker = *sEventBroker)
|
|
|
```
|
|
|
|
|
|
Compared to the ones described before, they take 2 additional arguments:
|
|
|
- *topic*: The topic to post the event on
|
|
|
- *broker*: The event broker to use to post the events. Defaults to the global event broker
|
|
|
|
|
|
### Checking if an event has been posted
|
|
|
State machines interact with the rest of the software mostly by means of events. If we want to check if an event has been posted by a state machines (or anything else, really), we can use the `EventCounter` class.
|
|
|
This class can subscribe to one or multiple topics and count how many times any event is posted to that topic.
|
|
|
The main methods to use are:
|
|
|
|
|
|
```cpp
|
|
|
unsigned int getCount(const Event& ev);
|
|
|
unsigned int getCount(uint8_t ev_sig);
|
|
|
```
|
|
|
These will check how many times a specified event has been posted to the topics the event counter is subscribed to.
|
|
|
|
|
|
```cpp
|
|
|
unsigned int getTotalCount()
|
|
|
```
|
|
|
Will check how many events the event counter has received in total.
|
|
|
|
|
|
Example:
|
|
|
|
|
|
```cpp
|
|
|
EventCounter counter{*sEventBroker}; //Create an event counter that listens to events posted on the global event broker
|
|
|
counter.subscribe(TOPIC_T1); // Subscribe the counter to TOPIC_T1. It will now count all the events posted on this topic
|
|
|
|
|
|
printf("%d", counter.getTotalCount()); // 0
|
|
|
|
|
|
sEventBroker->post(Event{EV_A}, TOPIC_T1);
|
|
|
|
|
|
printf("%d", counter.getTotalCount()); // 1
|
|
|
printf("%d", counter.getCount(EV_A)); // 1
|
|
|
|
|
|
sEventBroker->post(Event{EV_B}, TOPIC_T1);
|
|
|
|
|
|
printf("%d", counter.getTotalCount()); // 2
|
|
|
printf("%d", counter.getCount(EV_A)); // 1
|
|
|
printf("%d", counter.getCount(Event{EV_B})); // 1
|
|
|
```
|
|
|
|
|
|
### Examples:
|
|
|
An example of test for state machines can be found in skyward-boardcore at:
|
|
|
`src/tests/examples/example-test-fsm.cpp`
|
|
|
|
|
|
|
|
|
## Links
|
|
|
Practical UML Statecharts in C/C++, 2nd Edition: https://www.state-machine.com/psicc2/ |
|
|
\ No newline at end of file |