-
Notifications
You must be signed in to change notification settings - Fork 7
/
Documentation.dox
185 lines (148 loc) · 8.99 KB
/
Documentation.dox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
For a primer on UML state machines, look [here][1].
#### Using tsm
Create the state machine by first creating a context. The context is a structure that holds the states, events, actions and guards of the state machine. The context is a template parameter to the `Hsm` class. The `Hsm` class is a Mixin class that adds state machine like functionality to the context. In essence, it takes a context and converts it into a state machine.
```
```
Wrap the state machine around an execution policy. Currently, there are several options.
a. A state machine that executes in the context of the parent thread. This means that the client sending events to the state machine drives the event processing as well. Typically, this will be the 'main' thread of your application driving event processing. Note that several other threads can hold references to the state machine instance and queue up events (see 'd' below).
```cpp
SingleThreadedHsm<SwitchContext> sm;
```
b. A state machine that processes incomming events in its own thread. i.e. Events will be immediately consumed as long as the state machine is not busy. Clients 'fire and forget' an event and desire no control over event processing.
```
ThreadedExecutionPolicy<GarageDoorHsm> sm;
```
c. Send events to the state machine by using sendEvent method provided by the policy.
```
sm.sendEvent(sm.doorOpen);
```
d. If the state machine is running in parent thread context, invoke the `step` method to process the first event in the event queue. As opposed to the `SingleThreadedExecutionPolicy`, the `ThreadedExecutionPolicy` will immediately process an event on completion of prior event processing.
[1]: https://en.wikipedia.org/wiki/UML_state_machine
### Architecture
#### Policy based design
Classes have been partitioned across policies so they can be mixed and matched for code reuse (in an ideal world)! The current architecture supports state machines with the following characterestics.
```
a. Hierarchical
b. Asynchronous
c. Parallel/Orthogonal
```
Other Policy classes *can* be implemented for distributed event processing. The existing mechanism can also be extended to incorporate custom behavior such as writing state transitions to disk. For e.g. see `struct ThreadedExecWithObserver` in `ThreadedExecutionPolicy.h`
Clients need to include tsm.h file.
#### Hsm
Place holder for your own application specific sate machine definition.
#### Hierarchical StateMachine
The `make_hsm_t` meta-function is used to recursively examine a state transition table and convert all sub-contexts into state machines. Every state machine is a `Hsm` class that is a mixin of the context.
#### Policy Classes
Policy classes like `ThreadedExecutionPolicy` and `SingleThreadedExecutionPolicy` are mixins that operate on wrapped `Hsm`s. Clients will typically interact with a Policy class at the bottom of the inheritance hierarcy. By convention, these Policy classes also provide a `send_event` method as a public interface to the state machine. The `SingleThreadedExecutionPolicy` class also provides a `step` method for clients to initiate event processing. If the state machine is a `ClockedHsm`, a `tick` method is provided which will provide a reference to a `ClockTickEvent`.
The `ThreadedExecutionPolicy` processes events in its own thread. The processing of events is single threaded within all Hsms. So when a Hsm is started using a call to `start()`, the `StateMachine` will block waiting for `next_event` to return an event. The main advantage is that the only external interface to the Hsm can be the set of Events. Any "client" can asynchronously send an event to the state machine long as they have a reference to it. As soon as the Hsm is done with its processing, it will pick up the next event in the queue and process it. This can be seen in the test/*.cpp files.
#### Advantages
* Integration - A lot of effort went into integration using CMake. This should make it easier for modern C++ development.
* Unit Tests - Althought the framework could use a lot more unit tests, most of the code (98%) has been tested for 'happy path' cases.
* Architecture - Hopefully, the ability to add your own policy classes makes it adaptable to your applications.
#### Putting it all together
Create your own context definitions. The context hierarchy, its states (and sub-contexts if any), events, actions guards and transitions are all specified and defined in the definition. The relationships between Hsms.
Then choose a policy class for your state machine. Complete your state machine type by wrapping the policy class around it. The unit test provided below is illustrative.
```cpp
namespace TrafficLight {
struct LightContext {
struct G1 {
bool handle(LightContext&, ClockTickEvent& t) {
if (t.ticks_ >= 30) {
// exit action
t.ticks_ = 0;
return true;
}
return false;
}
};
struct Y1 {
bool handle(LightContext&, ClockTickEvent& t) {
if (t.ticks_ >= 5) {
// exit action
t.ticks_ = 0;
return true;
}
return false;
}
void entry(LightContext& t) { t.walk_pressed_ = false; }
};
struct G2 {
bool handle(LightContext& l, ClockTickEvent& t) {
if (t.ticks_ >= 60 || (l.walk_pressed_ && t.ticks_ >= 30)) {
// exit action
t.ticks_ = 0;
return true;
}
return false;
}
};
struct Y2 {
bool handle(LightContext&, ClockTickEvent& t) {
if (t.ticks_ >= 5) {
// exit action
t.ticks_ = 0;
return true;
}
return false;
}
};
bool walk_pressed_{};
using transitions = std::tuple<ClockedTransition<G1, Y1>,
ClockedTransition<Y1, G2>,
ClockedTransition<G2, Y2>,
ClockedTransition<Y2, G1>>;
};
// Emergency override trait. G1 and G2 will transition every five ticks. If
// walk_pressed_ is true, the light will transition to Y2 after 30 ticks. This
// will be part of a hierarchical state machine
struct EmergencyOverrideContext {
struct BaseHandle {
bool handle(EmergencyOverrideContext&, ClockTickEvent& t) {
if (t.ticks_ >= 5) {
// exit action
t.ticks_ = 0;
return true;
}
return false;
}
};
struct G1 : BaseHandle {};
struct Y1 : BaseHandle {};
struct G2 : BaseHandle {};
struct Y2 : BaseHandle {};
bool walk_pressed_{};
using transitions = std::tuple<ClockedTransition<G1, Y1>,
ClockedTransition<Y1, G2>,
ClockedTransition<G2, Y2>,
ClockedTransition<Y2, G1>>;
};
// Parent HSM - TrafficLight
struct TrafficLightHsmContext {
// Events
struct EmergencySwitchOn {};
struct EmergencySwitchOff {};
using transitions = std::tuple<
Transition<LightContext, EmergencySwitchOn, EmergencyOverrideContext>,
Transition<EmergencyOverrideContext, EmergencySwitchOff, LightContext>>;
};
}
// Test StateMachine ThreadedExecutionPolicy
TEST_CASE("Test ThreadedExecutionPolicy") {
// apply policy
using TrafficLightHsm =
ThreadedExecutionPolicy<TrafficLight::TrafficLightHsmContext>;
using LightHsm = make_hsm_t<TrafficLight::LightContext>;
using EmergencyOverrideHsm =
make_hsm_t<TrafficLight::EmergencyOverrideContext>;
TrafficLightHsm hsm;
hsm.start();
hsm.send_event(TrafficLight::TrafficLightHsmContext::EmergencySwitchOn());
std::this_thread::sleep_for(std::chrono::milliseconds(5));
REQUIRE(std::holds_alternative<EmergencyOverrideHsm*>(hsm.current_state_));
auto current_hsm = std::get<EmergencyOverrideHsm*>(hsm.current_state_);
REQUIRE(std::holds_alternative<TrafficLight::EmergencyOverrideContext::G1*>(
current_hsm->current_state_));
}
```
Here, the `TrafficLightHsm` is a hierarchical state machine. The `LightHsm` and `EmergencyOverrideHsm` are the leaf state machines. The `TrafficLightHsm` is a parent state machine that can switch between the `LightHsm` and `EmergencyOverrideHsm` state machines. The `EmergencyOverrideHsm` is a state machine that can override the `LightHsm` state machine. The `EmergencyOverrideHsm` is a "leaf" state machine that can switch between the `G1`, `Y1`, `G2` and `Y2` states. `LightHsm` is another "leaf" state machine that can switch between the same `G1`, `Y1`, `G2` and `Y2` states. If the main `LightHsm` needs to switch over to emergency mode, an `EmergencySwitchOn` event is sent to the `TrafficLightHsm`. The `TrafficLightHsm` will then switch over to the `EmergencyOverrideHsm` state machine.
The ThreadedExecutionPolicy is used to process events in a separate thread. If the ticks map to seconds, a PeriodicExecutionPolicy can be used to generate ticks every second. See `tsm.h` for an example.