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:
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).
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 scopeconst 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
…
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.
/*
* (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';exportfunction oven(){this.init=false;this.instId=0;// State machine state datathis.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 processthis.events=Object.freeze({
none:0,
evTimeout:1,
evDec:2,
evDoorOpen:3,
evDoorClosed:4,
evInc:5,
evPwr:6,
evBlink:7,})// Initialize state machinethis.initialize=function(){this.init=true;// Set state vars to default statesthis.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 valuethis.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 valuethis.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 inthis.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 handlerthis.processEvent=function(msg, userData){var evConsumed=0;if(this.init===false){this.initialize()}// Copy stateVar to ensure regions have same view on machinethis.stateVarsCopy=Object.assign({},this.stateVars);// Action code /* Action - sample */
console.log(this.eventToString(msg));switch(this.stateVars.stateVar){casethis.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;}elseif(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;}elseif(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 CookingRegionthis.processEventCookingRegion=function(msg, userData){var evConsumed =0;switch(this.stateVars.stateVarCookingRegion){casethis.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 */casethis.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 MainRegionthis.processEventMainRegion=function(msg, userData){var evConsumed =0;var eventConsumedCookingRegion =0switch(this.stateVars.stateVarMainRegion){casethis.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 */casethis.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;}elseif(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 */casethis.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 */casethis.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*/}elseif(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;}}