Sinelabore Homepage

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 -l js or -l mjs (both generate the same content, only file extension differs).

Example command for a model file oven.xml:

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

By supporting JavaScript generation, Sinelabore lets users design state machines in UML and generate JS code directly. This is helpful for IoT device/server protocols and rapid GUI prototypes using HTML/JS.

How can you benefit from JS code generation?#

Creating business logic#

Create business logic as state machines on client or server side. Build state machine diagrams without manual state machine coding, enabling fast prototyping and requirement refinement. The built-in model checker keeps models consistent, and the simulator supports quick iteration.

Creating HMI prototypes for IoT or embedded devices#

Prototypes are often needed for quick feedback on design approaches or unclear requirements. JS is ideal here: it integrates naturally in HTML pages and supports rapid development of functional prototypes.

Microwave oven GUI simulation (video):

Download MP4 demo

The generated state machine handler is called from the HTML page at relevant points. For example, key presses can be forwarded like this:


<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 JavaScript action 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:

  • Code generation from flat or hierarchical models with or without regions (orthogonal parts of a state)
  • States and sub-states
  • Deep and flat hierarchy
  • Entry, exit, and action code
  • Choice pseudo-states
  • Transitions with event, guard, and action
  • Final states
  • Trace code generation with -Trace
  • UserData field for user-defined machine data

As in other backends, code at the beginning of the generated JS file can be specified using a UML class header section (for imports or local helpers). Pre-/post-action handler code can also be defined.

The generated code for the oven state machine looks as follows. The complete example is available on the Sinelabore GitHub.

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;
  }


}