|
| 1 | +# Generating applications from state machine diagrams |
| 2 | + |
| 3 | +This tutorial demonstrates how to generate applications from state machine |
| 4 | +diagrams using Ogma. Specifically, this page shows: |
| 5 | + |
| 6 | +- How to run Ogma and invoke the diagram backend. |
| 7 | +- How to specify the state machine diagrams that the generated application |
| 8 | + implements. |
| 9 | +- How to build and run the generated code. |
| 10 | + |
| 11 | +## Table of Contents |
| 12 | + |
| 13 | +- [Introduction](#introduction) |
| 14 | +- [Diagrams](#diagrams) |
| 15 | + - [States](#states) |
| 16 | + - [Transitions](#transitions) |
| 17 | +- [Generating the application](#generating-the-application) |
| 18 | +- [Building the application](#building-the-application) |
| 19 | +- [Running the application](#running-the-application) |
| 20 | + |
| 21 | +# Introduction |
| 22 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 23 | + |
| 24 | +Ogma is a tool that, among other features, is capable of generating robotics |
| 25 | +and flight applications. |
| 26 | + |
| 27 | +State machines are commonly used to capture the behavior of robots, aircraft |
| 28 | +and spacecraft in operation, which transition between different states or modes |
| 29 | +that dictate their behavior. For example, a robot may be stopped, being |
| 30 | +controlled by a human operator, navigating automatically, etc. A drone may be |
| 31 | +stopped, taking off, landing, being piloted by a human operator, automatically |
| 32 | +holding its position, navigating automatically using GPS, and so on. |
| 33 | + |
| 34 | +In such systems, it is useful to be able to determine if the system is |
| 35 | +transitioning between these states correctly (e.g., it never goes from flying |
| 36 | +to stopped without landing first), whether an intended state change should be |
| 37 | +allowed (e.g., it never attempts to go into automatic navigation without a GPS |
| 38 | +fix), or just to have an implementation of the expected transition logic that |
| 39 | +follows the state machine specification faithfully. |
| 40 | + |
| 41 | +Ogma can take a state machine diagram and generate a C program that implements |
| 42 | +any of the aforementioned use cases. For Ogma to be able to generate state |
| 43 | +machine implementations, users need to provide the following information: |
| 44 | + |
| 45 | +- A state machine diagram that describes valid states and transitions, in one |
| 46 | + of the supported formats. |
| 47 | + |
| 48 | +- An indication of the mode that the resulting code should operate in (e.g., |
| 49 | + checking the current state against an expectation, calculating the next |
| 50 | + state). |
| 51 | + |
| 52 | +An invocation of Ogma's diagram backend may look like the following: |
| 53 | + |
| 54 | +```sh |
| 55 | +$ ogma diagram --app-target-dir demo \ |
| 56 | + --mode calculate \ |
| 57 | + --file-name ogma-cli/examples/diagram-001-hello-ogma/diagram-copilot.dot \ |
| 58 | + --file-format dot \ |
| 59 | + --prop-format literal |
| 60 | +``` |
| 61 | + |
| 62 | +where: |
| 63 | + |
| 64 | +- `demo` is the directory where the application is produced. |
| 65 | + |
| 66 | +- `calculate` is the mode of operation of the generated code. |
| 67 | + |
| 68 | +- `diagram-copilot.dot` contains the state machine diagram that needs to be implemented. |
| 69 | + |
| 70 | +- `dot` describes the format of `diagram-copilot.dot`. |
| 71 | + |
| 72 | +- `literal` indicates the language used to describe the expressions in the transitions between states. |
| 73 | + |
| 74 | +File names are customizable, as are many aspects of the generated applications. |
| 75 | +In the following, we explore how to specify the diagrams, including the states |
| 76 | +and transitions, how to run Ogma, how to compile the resulting application, and |
| 77 | +how to check that it works. |
| 78 | + |
| 79 | +# Diagrams |
| 80 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 81 | + |
| 82 | +To illustrate how Ogma works, let us implement a state machine that transitions |
| 83 | +between three states depending on the value of an input variable. |
| 84 | + |
| 85 | +## States |
| 86 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 87 | + |
| 88 | +The first element to provide are the states that the resulting system can be in. |
| 89 | + |
| 90 | +In our example, that information is provided in an `diagram-copilot.dot` file, |
| 91 | +which, in our case, looks like the following (this does not include expressions |
| 92 | +or constraints for the transitions, which will described later): |
| 93 | + |
| 94 | +``` |
| 95 | +digraph g{ |
| 96 | + rankdir="LR"; |
| 97 | + edge[splines="curved"] |
| 98 | + 0 -> 1; |
| 99 | + 1 -> 0; |
| 100 | + 0 -> 2; |
| 101 | + 2 -> 0; |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +Currently, Ogma uses numbers to represent states. The system is initially in |
| 106 | +state `0`, then it may go either to `1` or `2`, from which it may come back to |
| 107 | +state `0`. An approximate visual representation of the state machine is below. |
| 108 | + |
| 109 | +```mermaid |
| 110 | +flowchart LR |
| 111 | + 0 --> 1 |
| 112 | + 1 --> 0 |
| 113 | + 0 --> 2 |
| 114 | + 2 --> 0 |
| 115 | +``` |
| 116 | + |
| 117 | +The above diagram is specified in `dot` or Graphviz format. Ogma also accepts |
| 118 | +diagrams specified in `mermaid`. |
| 119 | + |
| 120 | +This diagram is incomplete; in the following subsection, we describe how to |
| 121 | +indicate when the system should transition between states. |
| 122 | + |
| 123 | +## Transitions |
| 124 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 125 | + |
| 126 | +For Ogma to generate useful code to determine if or when the system should be |
| 127 | +in one state or another, we need to add constraints or expressions in the edges |
| 128 | +between states. We can do so by attaching, to each edge or arrow in the |
| 129 | +diagram, an expression that determines when the system should transition to |
| 130 | +each state. |
| 131 | + |
| 132 | +The following diagram adds expressions to each arrow, indicating that the state |
| 133 | +machine should transition from the initial state `0` to state `1` when an input |
| 134 | +variable `input` becomes greater than 180, and to state `2` when it smaller |
| 135 | +than 120. In either case, if the negation of that condition becomes true after |
| 136 | +transitioning, then the system transitions back to the initial state, but it |
| 137 | +does not transition directly between `1` and `2`. |
| 138 | + |
| 139 | +``` |
| 140 | +digraph g{ |
| 141 | + rankdir="LR"; |
| 142 | + edge[splines="curved"] |
| 143 | + 0 -> 1 [label = "input > 180"]; |
| 144 | + 1 -> 0 [label = "input <= 180"]; |
| 145 | + 0 -> 2 [label = "input < 120"]; |
| 146 | + 2 -> 0 [label = "input >= 120"]; |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +The visual representation of the diagram is given below: |
| 151 | + |
| 152 | +```mermaid |
| 153 | +flowchart LR |
| 154 | + 0 -- "input > 180" --> 1 |
| 155 | + 1 -- "input <= 180" --> 0 |
| 156 | +
|
| 157 | + 0 -- "input < 120" --> 2 |
| 158 | + 2 -- "input >= 120" --> 0 |
| 159 | +``` |
| 160 | + |
| 161 | +The above diagram is underspecified. Specifically, it does not state what would |
| 162 | +happen if, for example, we are in state `0` and the input is greater than 120 |
| 163 | +but lower than 180. Implicitly, we could assume that the state machine should |
| 164 | +not switch states, but Ogma assumes, by default, that all cases must be stated |
| 165 | +explicitly. If there is no transition to apply in a given state, Ogma assumes |
| 166 | +that the system is a faulty state. |
| 167 | + |
| 168 | +A way to make the diagram more precise is to add also transitions from each |
| 169 | +state to itself when none of the outgoing transitions apply: |
| 170 | + |
| 171 | +```dot |
| 172 | +digraph g{ |
| 173 | + rankdir="LR"; |
| 174 | + edge[splines="curved"] |
| 175 | + 0 -> 0 [label = "input >= 120 && input <= 180"]; |
| 176 | +
|
| 177 | + 0 -> 1 [label = "input > 180"]; |
| 178 | + 1 -> 0 [label = "input <= 180"]; |
| 179 | +
|
| 180 | + 1 -> 1 [label = "input > 180"]; |
| 181 | +
|
| 182 | + 0 -> 2 [label = "input < 120"]; |
| 183 | + 2 -> 0 [label = "input >= 120"]; |
| 184 | +
|
| 185 | + 2 -> 2 [label = "input < 120"]; |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +The above diagram is both fully specified and fully deterministic. |
| 190 | + |
| 191 | +# Generating the application |
| 192 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 193 | + |
| 194 | +To generate the application from the diagram above, we invoke `ogma` with the |
| 195 | +following arguments: |
| 196 | + |
| 197 | +``` |
| 198 | +$ ogma diagram --app-target-dir demo \ |
| 199 | + --prop-format literal \ |
| 200 | + --mode calculate \ |
| 201 | + --file-format dot \ |
| 202 | + --file-name ogma-cli/examples/diagram-001-hello-ogma/diagram-copilot.dot |
| 203 | +``` |
| 204 | + |
| 205 | +We specify the mode of operation `calculate`, which instructs Ogma to generate |
| 206 | +code that will implement the state machine. Other modes available are `check`, |
| 207 | +in which the code generated checks it the current state matches an expectation, |
| 208 | +and `safeguard`, in which the code reports the states that the system is |
| 209 | +allowed to transition to based on its current state and inputs. |
| 210 | + |
| 211 | +The call to `ogma` above creates a `demo` directory that contains a |
| 212 | +specification of the state machine in the diagram. |
| 213 | + |
| 214 | +# Building the application |
| 215 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 216 | + |
| 217 | +The generated application includes a |
| 218 | +[Copilot](https://github.com/Copilot-Language/Copilot) file that contains a |
| 219 | +formal encoding of a monitor for the state machine under |
| 220 | +`demo/copilot/Copilot.hs`. |
| 221 | + |
| 222 | +To build the application, we must first go tho the directory where the |
| 223 | +specification is located, and compile it to C99 code: |
| 224 | + |
| 225 | +```sh |
| 226 | +$ cd demo/Copilot/ |
| 227 | +$ runhaskell Copilot.hs |
| 228 | +``` |
| 229 | + |
| 230 | +The above command generates three files in the same directory: `monitor.c`, |
| 231 | +`monitor.h`, and `monitor_types.h`. The first contains the implementation of |
| 232 | +the state machine, the second contains the interface to the state machine, and |
| 233 | +the last one would normally contain any auxiliary type definitions, and is |
| 234 | +empty in this case. |
| 235 | + |
| 236 | +We can compile that C code with a standard C compiler as follows: |
| 237 | + |
| 238 | +```sh |
| 239 | +$ gcc -c monitor.c |
| 240 | +``` |
| 241 | + |
| 242 | +The code generated expects that state machine starts at the initial state `0`, |
| 243 | +that the current system input will be made available in a global variable |
| 244 | +called `input`, and that an existing function `handler` will exist and be able |
| 245 | +to handle notifications from the generated code with the new state. In `check` |
| 246 | +mode, the generated code also expects a variable `state` that will hold the |
| 247 | +system state, which the generated monitor compares against the internally |
| 248 | +calculated or expected state. The names `input` and `state` can be customized |
| 249 | +by the user via a combination of command-line flags and transition expressions. |
| 250 | +Run `ogma diagram --help` for more information. |
| 251 | + |
| 252 | +# Running the application |
| 253 | +<sup>[(Back to top)](#table-of-contents)</sup> |
| 254 | + |
| 255 | +The code generated by Ogma does not contain a `main` entry point for the |
| 256 | +program. To test the generated module, we write a C program in a file `main.c` |
| 257 | +defines different values for `input`, calls the state machine implementation to |
| 258 | +calculate the new expected state, and reports the results: |
| 259 | + |
| 260 | +``` |
| 261 | +#include <stdio.h> |
| 262 | +#include <stdint.h> |
| 263 | +#include <stdlib.h> |
| 264 | +
|
| 265 | +#include "monitor.h" |
| 266 | +
|
| 267 | +uint8_t input = 150; |
| 268 | +uint8_t state = 0; |
| 269 | +uint64_t iteration = 0; |
| 270 | +
|
| 271 | +/** |
| 272 | + * Function called by the state machine to communicate its expected state. |
| 273 | + * |
| 274 | + * @param handler_arg0 The new state of the state machine. |
| 275 | + * @param handler_arg1 The previous state of the state machine. |
| 276 | + * @param handler_arg2 The input received that caused the transition. |
| 277 | + */ |
| 278 | +void handler(uint8_t handler_arg0, uint8_t handler_arg1, uint8_t handler_arg2) |
| 279 | +{ |
| 280 | + printf("[Calculated] Previous state: %d, input: %d, new state: %d\n", |
| 281 | + handler_arg1, handler_arg2, handler_arg0); |
| 282 | +
|
| 283 | + iteration++; |
| 284 | +
|
| 285 | + state = handler_arg0; |
| 286 | +} |
| 287 | +
|
| 288 | +/** |
| 289 | + * Report the current, internally calculated input and state, and call the |
| 290 | + * state machine to calculate the new state. The function <code>step</code> |
| 291 | + * calls <code>handler</code> at each step. |
| 292 | + */ |
| 293 | +void next() |
| 294 | +{ |
| 295 | + printf("Executing step %ld: input %d, state %d\n", iteration, input, state); |
| 296 | + step(); |
| 297 | + printf("--------------------------------------\n"); |
| 298 | +} |
| 299 | +
|
| 300 | +void main (int argc, char **argv) |
| 301 | +{ |
| 302 | + // We step with initial state 0 and input 150. The machine should remain in |
| 303 | + // state 0. |
| 304 | + next(); |
| 305 | +
|
| 306 | + // We step again with input greater than 180. The machine should now |
| 307 | + // transition to state 1. |
| 308 | + input = 185; |
| 309 | + next(); |
| 310 | +
|
| 311 | + // We step again with no changes. The machine should remain in state 1. |
| 312 | + next(); |
| 313 | +
|
| 314 | + // We step again with input lower than or equal to 180. The machine should |
| 315 | + // now transition to state 0. |
| 316 | + input = 110; |
| 317 | + next(); |
| 318 | +
|
| 319 | + // We step again with no changes. The machine should transition to state 2. |
| 320 | + next(); |
| 321 | +
|
| 322 | + // We step again with no changes. The machine should remain in state 2. |
| 323 | + next(); |
| 324 | +
|
| 325 | + // We step again with an input greater than 180. The machine should now |
| 326 | + // transition to state 0. |
| 327 | + input = 185; |
| 328 | + next(); |
| 329 | +} |
| 330 | +``` |
| 331 | + |
| 332 | +We can compile and link all C files together as follows: |
| 333 | +``` |
| 334 | +$ gcc main.c monitor.c -o main |
| 335 | +``` |
| 336 | + |
| 337 | +If we launch the program, we can see how the state machine switches between |
| 338 | +states. At each step, the program first reports the state it is in and its |
| 339 | +current input, and then the generated code calls `handler`, which reports the |
| 340 | +state the machine was in, the input received, and the new state: |
| 341 | + |
| 342 | +``` |
| 343 | +Executing step 0: input 150, state 0 |
| 344 | +[Calculated] Previous state: 0, input: 150, new state: 0 |
| 345 | +-------------------------------------- |
| 346 | +Executing step 1: input 185, state 0 |
| 347 | +[Calculated] Previous state: 0, input: 185, new state: 1 |
| 348 | +-------------------------------------- |
| 349 | +Executing step 2: input 185, state 1 |
| 350 | +[Calculated] Previous state: 1, input: 185, new state: 1 |
| 351 | +-------------------------------------- |
| 352 | +Executing step 3: input 110, state 1 |
| 353 | +[Calculated] Previous state: 1, input: 110, new state: 0 |
| 354 | +-------------------------------------- |
| 355 | +Executing step 4: input 110, state 0 |
| 356 | +[Calculated] Previous state: 0, input: 110, new state: 2 |
| 357 | +-------------------------------------- |
| 358 | +Executing step 5: input 110, state 2 |
| 359 | +[Calculated] Previous state: 2, input: 110, new state: 2 |
| 360 | +-------------------------------------- |
| 361 | +Executing step 6: input 185, state 2 |
| 362 | +[Calculated] Previous state: 2, input: 185, new state: 0 |
| 363 | +-------------------------------------- |
| 364 | +``` |
| 365 | + |
| 366 | +The generated implementation for the state machine transitions between the |
| 367 | +different states as specified in the diagram above. With trivial changes, we |
| 368 | +can make the state machine compare its internally calculated state against the |
| 369 | +value of `state`, and only call handler if the two are different. |
| 370 | +Alternatively, we can also make the machine call `handler` at every step, |
| 371 | +indicating if transitioning to the different states is allowed from the current |
| 372 | +state and with the current `input`, for which we have to modify the signature |
| 373 | +of `handler` to accept a boolean parameter for each of the machine states. |
0 commit comments