Generating GO code
Sinelabore creates compact and clearly readable GO code from UML state machine diagrams.
To generate GO code, call the code generator with the command line flag -l go
.
GO has some very convincing new features compared to C. A highlight are go routines
with the select
and the channel concept
. This allows to write very elegant threading code without the need to use pthreads or other more complex methods. Some other interesting features are the availability of maps and slices, generics, the concept of embedding, tickers and timers to name some others. All of these features were used in the oven state machine example (see code here on Github) to create a simple but still powerful command line simulation.
The example oven state machine is driven by an active object that handles events from multiple sources using the GO select and channel feature. The React()
function below is a go routine
which processes keyboard events (line 6) and timer events (line 10) with a resolution provided by a Ticker. The state machine handler is then called with the respective event defined in the state machine diagram.
func (smBase *Reactor) React(c chan OvenEvent, quit chan bool) { smBase.Ticker = time.NewTicker(time.Millisecond * 100) var err error = nil for { select { case event := <-c: // receive events _, err = smBase.ProcessEvent(event) case <-quit: // receive quit event return // Quit Co-routine case <-smBase.Ticker.C: // timer tick to check if there are any timeouts vals := smBase.Timers.Tick() for i := 0; i < len(vals); i++ { if evt, ok := vals[i].Event.(OvenEvent); ok { _, err = smBase.ProcessEvent(evt) } } } if err != nil { debugPrint(fmt.Sprintf("internal error in sm %s\n", err)) return } } }
In the generated code, state hierarchies are mapped to nested switch/case code. Event handling code is mapped to if/else-if/else structures. There are several configuration/command line options to adjust the generated code: Configuration file options:
BaseClassMachine
: GO supports the language concept of embedding of structs to express a more seamless composition of types. With the ‘BaseClassMachine’ parameter the parent structure (embedding) Type can be specified. The nice thing in GO is the possibility to access structure members or functions of the embedding structure in the generated state machine code. This feature is used in the oven example to provide access to a Timer service and to store some data (timer IDs).
// Demonstrates how to embedded the statemachine type into another type // and use data/functions from this type e.g. as guards ... in the statemachine. type Reactor struct { id uint8 idBlink uint8 Ticker *time.Ticker Timers *CountdownTimers Statemachine *Oven
Namespace
: The specified value is used as package name of the generated code.ValidationCall
: If set to ‘yes’ a function is added to the generated code that allows to check if a state transition is allowed or simply to log state changes. It is expected that the function is implemented on the embedding data type.
Command line options:
-Trace
: Generates trace the taken transition which includes the triggering event as well as the guard. It is expected that the function is implemented on the embedding data type.
-l GO
: Instructs the code generator to generate GO code.
As in the other backends too, code at the beginning of the generated GO file can be specified with the ‘header’ linked to the class in a UML diagram. Use this to define your imports or other local code. As well as pre- and post-action code of the generated state handler (see section 'Introduction' in the manual).
The code generator supports code generation from models with regions. A region is an orthogonal part of a state. Regions allow to express parallelism within a state. A state can have two or more regions. Region contains states and transitions.
The example state diagram of a microwave oven state machine built with regions is shown below. Imagine how much time can be saved by generating 100% of the code instead of manually writing it. And even worse how much wasted time it would be to maintain it over time when new states/events are added. The oven uses the generic countdown_counter to schedule timers. This example uses two timers. One cyclic timer is used to flash an LED if the oven is running. The other one is used for the cooking time set by the user (single-shot).
Get an impression about the powerful generation here: https://github.com/sinelabore/examples/tree/master/OS_neutral/microwave_go
Below the console trace output of the oven simulation program is shown with the following user interaction: First set the cooking time to 2 seconds (pressing ++
) which sends 2x EvInc
. Then start cooking by closing the door (press c
) which sends EvDoorClosed
. Then pause the cooking by opening the door (pressing o
) which sends EvDoorOpen
and then close again the door (pressing c
) which sends EvDoorClosed
. In-between you can see the timer events of the blinking LED and finally the cooking timeout event.
Question: what keys do you have to press to cook again? Hint: you have to open the door again before you can set the cooking timer again. Look into the state diagram and all should be clear!
===== Microwave oven simulation ===== Use the following keys: ===== o,O: Open the door ===== c,C: Close the door ===== + increase cooking time ===== - decrese cooking time ===== q,Q: quit ===== ===== Example sequence: +++++c o c +++ c ===== 2023/11/20 21:15:08 Oven off 2023/11/20 21:15:09 Change state from Super to Super 2023/11/20 21:15:09 Oven off 2023/11/20 21:15:09 Trace msg: EvInc 2023/11/20 21:15:10 Change state from Super to Super 2023/11/20 21:15:10 Oven off 2023/11/20 21:15:10 Trace msg: EvInc 2023/11/20 21:15:13 Change state from Idle to Cooking 2023/11/20 21:15:13 Oven on LED on 2023/11/20 21:15:13 Trace msg: EvDoorClosed[smBase.Timers.GetPresetInSec(smBase.id)>0] 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=2000) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=500) 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1900) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=400) 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1800) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=300) 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1700) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=200) 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1600) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=100) 2023/11/20 21:15:13 Change state from LedOn to LedOff LED off 2023/11/20 21:15:13 Trace msg: EvTimeoutBlink 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1500) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=500) 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1400) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=400) 2023/11/20 21:15:13 Timer 1 running but not expired (Cnt=1300) 2023/11/20 21:15:13 Timer 2 running but not expired (Cnt=300) 2023/11/20 21:15:13 Change state from Cooking to CookingPause LED Off 2023/11/20 21:15:13 Oven off 2023/11/20 21:15:13 Trace msg: EvDoorOpen 2023/11/20 21:15:14 Change state from CookingPause to Cooking 2023/11/20 21:15:14 Oven on LED on 2023/11/20 21:15:14 Trace msg: EvDoorClosed 2023/11/20 21:15:14 Timer 1 running but not expired (Cnt=1200) 2023/11/20 21:15:14 Timer 2 running but not expired (Cnt=500) 2023/11/20 21:15:14 Timer 1 running but not expired (Cnt=1100) 2023/11/20 21:15:14 Timer 2 running but not expired (Cnt=400) 2023/11/20 21:15:14 Timer 1 running but not expired (Cnt=1000) 2023/11/20 21:15:14 Timer 2 running but not expired (Cnt=300) 2023/11/20 21:15:14 Timer 1 running but not expired (Cnt=900) 2023/11/20 21:15:14 Timer 2 running but not expired (Cnt=200) 2023/11/20 21:15:14 Timer 1 running but not expired (Cnt=800) 2023/11/20 21:15:14 Timer 2 running but not expired (Cnt=100) 2023/11/20 21:15:14 Change state from LedOn to LedOff LED off 2023/11/20 21:15:14 Trace msg: EvTimeoutBlink 2023/11/20 21:15:14 Timer 1 running but not expired (Cnt=700) 2023/11/20 21:15:14 Timer 2 running but not expired (Cnt=500) 2023/11/20 21:15:15 Timer 1 running but not expired (Cnt=600) 2023/11/20 21:15:15 Timer 2 running but not expired (Cnt=400) 2023/11/20 21:15:15 Timer 1 running but not expired (Cnt=500) 2023/11/20 21:15:15 Timer 2 running but not expired (Cnt=300) 2023/11/20 21:15:15 Timer 1 running but not expired (Cnt=400) 2023/11/20 21:15:15 Timer 2 running but not expired (Cnt=200) 2023/11/20 21:15:15 Timer 1 running but not expired (Cnt=300) 2023/11/20 21:15:15 Timer 2 running but not expired (Cnt=100) 2023/11/20 21:15:15 Change state from LedOff to LedOn LED on 2023/11/20 21:15:15 Trace msg: EvTimeoutBlink 2023/11/20 21:15:15 Timer 1 running but not expired (Cnt=200) 2023/11/20 21:15:15 Timer 2 running but not expired (Cnt=500) 2023/11/20 21:15:15 Timer 1 running but not expired (Cnt=100) 2023/11/20 21:15:15 Timer 2 running but not expired (Cnt=400) 2023/11/20 21:15:15 Change state from Cooking to Completed LED Off 2023/11/20 21:15:15 Oven off 2023/11/20 21:15:15 Trace msg: EvTimeout
For further information, please refer to the corresponding manual chapter on GO.