Generating JavaScript code from UML state machine models

Sinelabore creates compact and clearly readable JavaScript code from UML state machine diagrams. It has zero dependencies, and is useful for frontend and backend application logic.

Sinelabore enables event-driven programming to handle complex logic in predictable, robust, and visual ways.

To generate JavaScript code, call the code generator with the command line flag -l js or -l mjs depending on the file extension wanted. The content of the generated file is the same in both cases.

Example command to generate the oven machine code from a model file called oven.xml:

java -cp “../../*” codegen.Main -l js -p ssc -o oven oven.xml

By supporting JS code generation, Sinelabore users can now design state machines in UML and generate JS code from them. Which is undoubtedly a great advantage when thinking about the development of protocols between IoT devices and servers, for example. Or the development of device prototypes where a functioning GUI user interface based on HTML/JS can be emulated very easily.

How can You benefit from JS code generation?

Creating business logic

Create your business logic as state machines on either the client or server side. Create state machine diagrams without manual coding, allowing rapid prototyping or requirements gathering without worrying about the details of state machine implementation. The built-in model checker ensures that your state machine is always correct. Simulate your design to test and iterate using the built-in simulator. Or create a rapid prototype (see below) to get early feedback from your team and customers on how things should work.

Creating HMI prototypes for IoT or embedded devices

Prototypes are often needed to get quick feedback on design approaches or unclear requirements. This is where JS can play an important role. Thanks to its integration into HTML pages and its simple syntax, it enables the rapid development of functional prototypes. Users can then “play” with them and give direct feedback. This is shown here for a microwave oven. The oven can be controlled via a simple HTML page (the state machine is shown below).

Microwave oven GUI simulation based on a website

The event hander of the generated state machine is called at the appropriate points in the HTML page e.g. to forward the key presses to the generated event handler the following code used:

<script type="module"> import { oven } from './oven.mjs'; import { TimerManager } from './TimerManager.mjs'; let myOven; // Declare my Oven in the outer scope const userData={ }; window.buttonDecClicked= function() { myOven.processEvent(myOven.events.evDec,userData) } window.buttonIncClicked=function() { myOven.processEvent(myOven.events.evInc,userData) } window.buttonOpenDoorClicked= function() { myOven.processEvent(myOven.events.evDoorOpen,userData) } window.buttonCloseDoorClicked=function() { myOven.processEvent(myOven.events.evDoorClosed,userData) } ... }

The machine below realizes the behaviour of the oven. It uses hierarchical states with deep history and actions in various places. Two JS helper classes are used to interact with the GUI and to have timers available.

The machine realizes this behaviour:

The light and the oven will only become active when a time has been set and the door is closed

If the door is opened in between, the oven will be switched off and the time will stop. After closing the door, the oven continues to run at the old time.

The time can be changed at any time

…

State Machine of a Microwave Oven with Java Script Promo Code

A customer can work with this machine and interactively test whether the device really works as intended. For example, should the light stay on after the cooking time has elapsed even though the oven has not yet been opened? Such things can only be determined if you can “play” with your model.

The Sinelabore Code Generator provides all the functions necessary to work with the machine. In addition to the event handler, these are auxiliary functions, for example to find out what state (or states) the machine is in and much more. The complete code is available below:

Features of the JavaScript State Machine Code Generator

The code generator supports the following features:

Code generation from flat or hierarchical state machine models with or without 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.

States and sub-states

Deep – and flat hierarchy

Entry, Exit and Action code of states

Choice pseudo-states

Transitions with event, guard and action

Final states

Trace code generation with command line parameter -Trace

UserData field for user defined variables to be used in the machine

As in the other backends too, code at the beginning of the generated JavaScript 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. It is also possible to define pre- and post-action code of the generated state handler (see section 'Introduction' in the manual).

The generated code looks as follows for the oven state machine. The complete example is available on the sinelabore github site.

Generated JavaScript code from the Oven State Machine

/* * (c) Sinelabore Software Tools GmbH, 2008 - 2024 * * All rights reserved. Reproduction, modification, * use or disclosure to third parties without express * authority is forbidden. */ /* Command line options: -l mjs -p ssc -o oven oven.xml */ /* This file is generated from oven.xml - do not edit manually */ /* Generated on: Wed Jan 17 19:50:24 CET 2024 / Version 6.2.1B13753*/ import { TimerManager } from './TimerManager.mjs'; import { DisplayInterface } from './DisplayInterface.mjs'; export function oven() { this.init = false; this.instId = 0; // State machine state data this.stateVars = { stateVar: null, stateVarMainRegion : null, stateVarCookingRegion : null, }; // States of the state machine this.states = Object.freeze({ none: 0, Super: 1, Completed: 2, Cooking: 3, CookingPause: 4, Idle: 5, TimeNotShown: 6, TimeShown: 7, }) // Events the state machine can process this.events = Object.freeze({ none: 0, evTimeout: 1, evDec: 2, evDoorOpen: 3, evDoorClosed: 4, evInc: 5, evPwr: 6, evBlink: 7, }) // Initialize state machine this.initialize = function(){ this.init = true; // Set state vars to default states this.stateVars.stateVar = this.states.Super /* set init state of top state */ this.stateVars.stateVarMainRegion = this.states.Idle; /* set init state of MainRegion */ this.stateVars.stateVarCookingRegion = this.states.TimeShown; /* set init state of CookingRegion */ this.tid = document.timerMgr.createTimer(0, this.events.evTimeout); this.tidBlink = document.timerMgr.createTimer(500, this.events.evBlink); this.display = new DisplayInterface(); console.log("Oven Off"); } this.isInSuper = function() { return (this.stateVars.stateVar === this.states.Super);} this.isInTimeNotShown = function() { return (this.stateVars.stateVarCookingRegion === this.states.TimeNotShown) && this.isInCooking();} this.isInTimeShown = function() { return (this.stateVars.stateVarCookingRegion === this.states.TimeShown) && this.isInCooking();} this.isInCompleted = function() { return (this.stateVars.stateVarMainRegion === this.states.Completed) && this.isInSuper();} this.isInCooking = function() { return (this.stateVars.stateVarMainRegion === this.states.Cooking) && this.isInSuper();} this.isInCookingPause = function() { return (this.stateVars.stateVarMainRegion === this.states.CookingPause) && this.isInSuper();} this.isInIdle = function() { return (this.stateVars.stateVarMainRegion === this.states.Idle) && this.isInSuper();} //Return the state name string based on the state value this.stateToString = function(stateValue) { const value = Object.keys(this.states).find(key => this.states[key] === stateValue); return value || `UnknownState(${stateValue})`; } //Return the event name string based on the event value this.eventToString = function(eventValue) { const value = Object.keys(this.events).find(key => this.events[key] === eventValue); return value || `UnknownEvent(${eventValue})`; } // Return of a map with the states in which the state machine is currently in this.innermostActiveStates = function() { var statesMap = new Map(); if ( this.isInCompleted() ) {statesMap.set(this.states.Completed,"Completed");} if ( this.isInTimeShown() ) {statesMap.set(this.states.TimeShown,"TimeShown");} if ( this.isInTimeNotShown() ) {statesMap.set(this.states.TimeNotShown,"TimeNotShown");} if ( this.isInCookingPause() ) {statesMap.set(this.states.CookingPause,"CookingPause");} if ( this.isInIdle() ) {statesMap.set(this.states.Idle,"Idle");} return statesMap; } // State machine event handler this.processEvent = function(msg, userData) { var evConsumed=0; if (this.init === false) { this.initialize() } // Copy stateVar to ensure regions have same view on machine this.stateVarsCopy = Object.assign({}, this.stateVars); // Action code /* Action - sample */ console.log(this.eventToString(msg)); switch (this.stateVars.stateVar) { case this.states.Super: /* calling region code */ evConsumed |= this.processEventMainRegion(msg, userData); /* Check if event was already processed */ if(evConsumed===0){ if (msg === this.events.evDec) { /* Transition from Super to Super*/ /* Exit code for regions in state Super*/ if(this.stateVars.stateVarMainRegion == this.states.Cooking){ }else { /* Intentionally left blank */ }; /* Action code for transition */ document.timerMgr.decTimer(this.tid); this.display.setTT(document.timerMgr.getPreset(this.tid)); /* Entry code for regions in state Super*/ console.log("Oven Off"); /* Default in entry chain */ this.stateVarsCopy.stateVarMainRegion = this.states.Idle;/* Default in entry chain */ /* adjust state variables */ this.stateVarsCopy.stateVar = this.states.Super; } else if (msg === this.events.evInc) { /* Transition from Super to Super*/ /* Exit code for regions in state Super*/ if(this.stateVars.stateVarMainRegion == this.states.Cooking){ }else { /* Intentionally left blank */ }; /* Action code for transition */ document.timerMgr.incTimer(this.tid); this.display.setTT(document.timerMgr.getPreset(this.tid)); /* Entry code for regions in state Super*/ console.log("Oven Off"); /* Default in entry chain */ this.stateVarsCopy.stateVarMainRegion = this.states.Idle;/* Default in entry chain */ /* adjust state variables */ this.stateVarsCopy.stateVar = this.states.Super; } else if (msg === this.events.evPwr) { /* Transition from Super to Super*/ /* Exit code for regions in state Super*/ if(this.stateVars.stateVarMainRegion == this.states.Cooking){ }else { /* Intentionally left blank */ }; /* Action code for transition */ console.log("Set Power"); /* Entry code for regions in state Super*/ console.log("Oven Off"); /* Default in entry chain */ this.stateVarsCopy.stateVarMainRegion = this.states.Idle;/* Default in entry chain */ /* adjust state variables */ this.stateVarsCopy.stateVar = this.states.Super; } else { /* Intentionally left blank*/ } /*end of event selection*/ } break; /* end of case Super */ default: /* Intentionally left blank*/ break; } /* end switch stateVar_root*/ // Post Action Code /* Post-Action - sample */ this.stateVars = Object.assign({}, this.stateVarsCopy); return evConsumed } // Region code for region CookingRegion this.processEventCookingRegion = function(msg, userData) { var evConsumed = 0; switch (this.stateVars.stateVarCookingRegion) { case this.states.TimeShown: if (msg === this.events.evBlink) { /* Transition from TimeShown to TimeNotShown*/ evConsumed=1; /* Action code for transition */ this.display.setTT(""); /* adjust state variables */ this.stateVarsCopy.stateVarCookingRegion = this.states.TimeNotShown; } else { /* Intentionally left blank*/ } /*end of event selection*/ break; /* end of case TimeShown */ case this.states.TimeNotShown: if (msg === this.events.evBlink) { /* Transition from TimeNotShown to TimeShown*/ evConsumed=1; /* Action code for transition */ this.display.setTT(document.timerMgr.getPreset(this.tid)); /* adjust state variables */ this.stateVarsCopy.stateVarCookingRegion = this.states.TimeShown; } else { /* Intentionally left blank*/ } /*end of event selection*/ break; /* end of case TimeNotShown */ default: /* Intentionally left blank*/ break; } /* end switch stateVar_root*/ return evConsumed; } // Region code for region MainRegion this.processEventMainRegion = function(msg, userData) { var evConsumed = 0; var eventConsumedCookingRegion = 0 switch (this.stateVars.stateVarMainRegion) { case this.states.Completed: if (msg === this.events.evDoorOpen) { /* Transition from Completed to Idle*/ evConsumed=1; /* Action code for transition */ document.getElementById("myImage").src = "images/oven_open_off.png"; /* OnEntry code of state Idle*/ console.log("Oven Off"); /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.Idle; } else { /* Intentionally left blank*/ } /*end of event selection*/ break; /* end of case Completed */ case this.states.Cooking: /* calling region code */ evConsumed |= this.processEventCookingRegion(msg, userData); /* Check if event was already processed */ if(evConsumed===0){ if (msg === this.events.evDoorOpen) { /* Transition from Cooking to CookingPause*/ evConsumed=1; /* Exit code for regions in state Cooking*/ /* Action code for transition */ console.log("Oven Off"); document.timerMgr.pauseTimer(this.tid); this.display.updateImage("images/oven_open_off.png"); /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.CookingPause; } else if (msg === this.events.evTimeout) { /* Transition from Cooking to Completed*/ evConsumed=1; /* Exit code for regions in state Cooking*/ /* Action code for transition */ console.log("Oven Off"); document.timerMgr.stopTimer(this.tid); document.timerMgr.stopTimer(this.tidBlink); this.display.updateImage("images/oven_closed_off.png"); /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.Completed; } else { /* Intentionally left blank*/ } /*end of event selection*/ } break; /* end of case Cooking */ case this.states.CookingPause: if (msg === this.events.evDoorClosed) { /* Transition from CookingPause to Cooking*/ evConsumed=1; /* Action code for transition */ document.timerMgr.continueTimer(this.tid); this.display.updateImage("images/oven_closed_on.png"); /* Entry code for regions in state Cooking*/ /* Default in entry chain */ this.stateVarsCopy.stateVarCookingRegion = this.states.TimeShown;/* Default in entry chain */ /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.Cooking; } else { /* Intentionally left blank*/ } /*end of event selection*/ break; /* end of case CookingPause */ case this.states.Idle: if (msg === this.events.evDoorClosed) { if (document.timerMgr.getPreset(this.tid)) { /* Transition from Idle to Cooking*/ evConsumed=1; /* Action code for transition */ document.timerMgr.startTimer(this.tid,false); document.timerMgr.startTimer(this.tidBlink,true); this.display.updateImage("images/oven_closed_on.png"); /* Entry code for regions in state Cooking*/ /* Default in entry chain */ this.stateVarsCopy.stateVarCookingRegion = this.states.TimeShown;/* Default in entry chain */ /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.Cooking; } else { /* Transition from Idle to Idle*/ evConsumed=1; /* Action code for transition */ this.display.updateImage("images/oven_closed_off.png"); /* OnEntry code of state Idle*/ console.log("Oven Off"); /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.Idle; } /*end of event selection*/ } else if (msg === this.events.evDoorOpen) { /* Transition from Idle to Idle*/ evConsumed=1; /* Action code for transition */ this.display.updateImage("images/oven_open_off.png"); /* OnEntry code of state Idle*/ console.log("Oven Off"); /* adjust state variables */ this.stateVarsCopy.stateVarMainRegion = this.states.Idle; } else { /* Intentionally left blank*/ } /*end of event selection*/ break; /* end of case Idle */ default: /* Intentionally left blank*/ break; } /* end switch stateVar_root*/ return evConsumed; } }