Skip to content

Commit 52182ca

Browse files
Merge branch 'release-1.11.0'. Refs #325.
2 parents a95c429 + 8304271 commit 52182ca

File tree

30 files changed

+544
-33
lines changed

30 files changed

+544
-33
lines changed

ogma-cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Revision history for ogma-cli
22

3+
## [1.11.0] - 2025-11-21
4+
5+
* Version bump 1.11.0 (#325).
6+
* Add missing dependency to installation instructions in README (#314).
7+
* Add example demonstrating the basics of diagram backend (#323).
8+
39
## [1.10.0] - 2025-09-21
410

511
* Version bump 1.10.0 (#310).

ogma-cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ being used.)
6868
On Debian or Ubuntu Linux, these dependencies can be installed with:
6969

7070
```sh
71-
$ apt-get install ghc cabal-install libbz2-dev libexpat-dev
71+
$ apt-get install ghc cabal-install libz-dev libbz2-dev libexpat-dev
7272
```
7373

7474
On Mac, they can be installed with:
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
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

Comments
 (0)