Skip to content

Commit b16cc69

Browse files
Merge branch 'develop-diagram-tutorial' into develop. Close #323.
**Description** To facilitate the adoption of Ogma, we want to have a collection of examples that demonstrate the most common use cases. A demonstration of how to use the diagram backend to generate a minimal state machine implementation would be a good starting point for users that want to generate monitors or applications from diagrams. **Type** - Feature: New manual page. **Additional context** None. **Requester** - Ivan Perez. **Method to check presence of bug** Not applicable (not a bug). **Expected result** A README, together with any necessary auxiliary files, is present in the `examples` directory. When the steps described in the `README` are followed, a simple state machine implementation is generated and demonstrated. The instructions are easy to follow for users not familiar with Ogma. **Solution implemented** Add under `examples/` a README page with a minimal example of a monitoring application. Demonstrate the elements of the example, how to invoke Ogma, how to compile the results, and how to run it. Provide any necessary auxiliary files in the same directory. **Further notes** None.
2 parents f236737 + 8b847c0 commit b16cc69

File tree

4 files changed

+458
-0
lines changed

4 files changed

+458
-0
lines changed

ogma-cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [1.X.Y] - 2025-11-21
44

55
* Add missing dependency to installation instructions in README (#314).
6+
* Add example demonstrating the basics of diagram backend (#323).
67

78
## [1.10.0] - 2025-09-21
89

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.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
digraph g{
2+
rankdir="LR";
3+
edge[splines="curved"]
4+
0 -> 0 [label = "input >= 120 && input <= 180"];
5+
6+
0 -> 1 [label = "input > 180"];
7+
1 -> 0 [label = "input <= 180"];
8+
9+
1 -> 1 [label = "input > 180"];
10+
11+
0 -> 2 [label = "input < 120"];
12+
2 -> 0 [label = "input >= 120"];
13+
14+
2 -> 2 [label = "input < 120"];
15+
}

0 commit comments

Comments
 (0)