... | ... | @@ -4,7 +4,7 @@ State machines can _transition_ from one state to the other and perform addition |
|
|
|
|
|
## 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.
|
|
|
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/`.
|
... | ... | @@ -19,8 +19,8 @@ These are rules & best practices referring to the particular implementation of s |
|
|
- **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:
|
|
|
`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
|
... | ... | @@ -30,16 +30,16 @@ These are rules & best practices referring to the particular implementation of s |
|
|
|
|
|
### Hierarchical State Machines rules
|
|
|
In addition to the general best practices, these apply only to Hierarchical State Machines.
|
|
|
1. **DO NOT** handle *EV_EMPTY*.
|
|
|
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.
|
|
|
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
|
|
|
- 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 be public, mainly to allow testing of the state machine.
|
... | ... | @@ -63,7 +63,7 @@ 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:
|
|
|
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) )
|
... | ... | @@ -78,7 +78,7 @@ if( fsm.testState(&FSMExample::state_S1) ) |
|
|
### 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
|
|
|
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:
|
... | ... | @@ -95,19 +95,19 @@ From now on, we will use FSMs as example, and thus the method `testFSMTransition |
|
|
|
|
|
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.
|
|
|
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)):
|
|
|
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.
|
|
|
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!
|
|
|
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.
|
... | ... | @@ -129,8 +129,8 @@ bool testHSMAsyncTransition(HSM_type& hsm, const Event& ev, uint8_t topic, |
|
|
```
|
|
|
|
|
|
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
|
|
|
- _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](Event-Utils) class.
|
... | ... | @@ -172,6 +172,5 @@ printf("%d", counter.getCount(Event{EV_B})); // 1 |
|
|
An example of test for state machines can be found in skyward-boardcore at:
|
|
|
`src/tests/examples/`.
|
|
|
|
|
|
|
|
|
## Links
|
|
|
Practical UML Statecharts in C/C++, 2nd Edition: https://www.state-machine.com/psicc2/ |
|
|
\ No newline at end of file |