A Hierarchical State Machine Framework for JavaScript and TypeScript
JavaScript TypeScript HTML Python
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
js
ts
.gitignore
LICENSE.md
README.md
package.json

README.md

OOSMOS.js -- A Hierarchical State Machine Class.

OOSMOS for JavaScript is an open source, easy-to-use hierarchical state machine class. The principle source code is written in the single TypeScript source file OOSMOS.ts.

Live Demo: OOSMOS.js

Features

  • Very readable encapsulated state machine structure.
  • Simple. Less than 300 lines of code of post-transpiled JavaScript.
  • Runs in a browser (uses webpack) or Node.js.
  • Supports arbitrarily deep hierarchical state machines.
  • Supports state local variables. (Ideal for caching jQuery elements.)
  • Simple API: Only 5 principal APIs.
  • Can run multiple state machines concurrently.
  • Events can pass arguments into the state entry function via transitions.

Note: OOSMOS stands for Object Oriented State Machine Operating System. OOSMOS is a small footprint C/C++ state machine operating system for the industrial IOT space. (See www.oosmos.com.) This JavaScript implementation borrows from the Object-Oriented and State Machine elements of OOSMOS but the Operating System elements are supported by the JavaScript runtime.

Example

We'll use OOSMOS.ts to implement a simple state machine that toggles between states A and B with timeouts as represented in this state chart.

Note that we use UMLet to draw state machines. (www.umlet.com). State machines are easy to draw, can export as .SVG files, and the tool is open source.

The entire implementation is below. In lines 3 through 27, we create a state machine called TimeoutDemo. State A starts on line 6 and state B starts on line 16 and look very similar. State A implements an ENTER event that establishes a timeout of 4 seconds. When the timeout expires, the TIMEOUT function will be executed which transitions to state B. State B essentially does the same thing as state A except that it times out in 1 second.

Line 31 starts the state machine.

In lines 8 and 18 we use the OOSMOS Print() API to display progress.

1  import { StateMachine } from '../OOSMOS';
2
3  class TimeoutTest extends StateMachine {
4    constructor() {
5      super({ DEFAULT: 'A',
6        A: {
7          ENTER: function() {
8            this.Print("In state A");
9            this.SetTimeoutSeconds(4);
10         },
11         TIMEOUT: function() {
12           this.Transition('B');
13         },
14       },
15
16       B: {
17         ENTER: function() {
18           this.Print("In state B");
19           this.SetTimeoutSeconds(1);
20         },
21         TIMEOUT: function() {
22           this.Transition('A');
23         },
24       },
25     });
26   }
27 }
28
29 const pTimeoutTest = new TimeoutTest();
30 pTimeoutTest.SetDebug(true);
31 pTimeoutTest.Start();

State Machine Structure Overview

This is the basic layout of an OOSMOS state machine, explained below.

import { StateMachine } from '../OOSMOS';

class TimeoutTest extends StateMachine {
  constructor() {
    super({ DEFAULT: 'StateName',
	  StateName: {
	    ENTER: function() {
	      this.SetTimeoutSeconds(4);
	    },
	    EXIT: function() {
	    },
	    TIMEOUT: function() {
	    },
	    EventName: function() {
	      this.Transition('StateName');
	    },
	    'Event-String': function() {
	    },
	  },
	  StateName: {
	    EventName: function() {
	    },
	    EventName: function() {
	    },
	    COMPOSITE: { DEFAULT: 'StateName',
	      StateName: {
	        EventName: function() {
	        }
	      },
	    },
	  },
    });
  }     
});

const pTimeoutTest = new TimeoutTest();
pTimeoutTest.Start();

State Machine Structure Rules

  1. The DEFAULT state indicator is only required when there is more than one state in the COMPOSITE. Note that the object passed to the top level OOSMOS function is a COMPOSITE object.
  2. ENTER and EXIT are optional. They are only supplied when there is something to do on entry or exit from the state.
  3. If a timeout is desired for the state, it must be set up in the special ENTER event using the SetTimeoutSeconds() API.
  4. If state names or event names contain special characters, they must be enclosed in quotes. (Standard JavaScript object property rules.)

API

Principal APIs

API Description
Event('Event', [, arguments...]) Generates an event. If the current state does not handle the event, it is propagated up the hierarchy until a state does handle it. If no active state handles the event, then the event has no effect.

Any supplied arguments are passed to the appropriate event handler function.
IsIn('dot.qualified.statename') Returns true if the specified dot-qualified state is active.

For example, if state A.AA.AAA is the current leaf state, then IsIn of 'A', 'A.AA' and 'A.AA.AAA' will all return true.

Note that if your state machine is not hierarchical, i.e., it does not use the COMPOSITE keyword, then none of your state names will contain a "dot" character.
SetTimeoutSeconds(Seconds) Establish a timeout for this state.

Multiple, nested timeouts can all be active at once.
Start() Starts the state machine.
Transition('dot.qualified.statename' [, arguments...]) Transitions from one state to another.

Must be called from within an event function. Any supplied arguments are passed to the ENTER function of the target state.

Support APIs

API Description
Alert(Message) When running under Node.js, executes a console.log() call.

In a browser, executes a window.alert() call.
Assert(Condition, String) Calls the Alert(String) function if Condition is not met.
DebugPrint(String) Executes a Print(String) if debug mode is on. See SetDebug.
Print(String) When running under Node.js, executes a console.log(String).

When running in a browser, requires an ID of a <div> element into which to write the string, specified in the SetDebug call).
Restart Re-initializes the state machine as if run for the first time. Principally used for testing.
SetDebug(DebugMode [, DebugID [, MaxLines [, ScrollIntoView]]]) Pass a DebugMode of true to enable debug mode, false otherwise.

The next three arguments apply only when running under a browser.

Required DebugID specifies the id of a <div> element into which debug lines will be written.

Optional MaxLines specifies the maximum number of lines that are retained in the <div>. Note that most browsers slow down considerably due to inefficient scrolling if there are too many lines retained in the <div>. Defaults to 200.

If true, optional ScrollIntoView causes each new debug line to come into focus.

Reserved Names in a State Object

Reserved Event Name Description
ENTER The ENTER function is called as the state is entered.
EXIT The EXIT function is called as the state is exited.
TIMEOUT This event is triggered if a timeout that was established in the ENTER function occurred.
COMPOSITE Specified by you to indicate nested states (hierarchy).
DOTPATH Used internally by OOSMOS.

Reserved Names in a Composite Object

Reserved State Name Description
DEFAULT Specified by you inside of a COMPOSITE to indicate which of multiple states at the same level should be entered by default.

Not required if there is only one state in the composite.

OOSMOS.d.ts

(Private elements removed.)

export interface iState {
    ENTER?: () => void;
    EXIT?: () => void;
    TIMEOUT?: () => void;
    COMPOSITE?: iComposite | (() => iComposite);
    [EventString: string]: (() => void) | any;
    DOTPATH?: string;
}
export interface iComposite {
    DEFAULT?: string;
    [StateName: string]: iState | (() => iState) | any;
}
export declare class StateMachine {
    constructor(Composite: iComposite);
    Transition(To: string, ...Args: any[]): void;
    Start(): void;
    Restart(): void;
    IsIn(StateDotPath: string): boolean;
    Event(EventString: string, ...Args: any[]): void;
    SetTimeoutSeconds(TimeoutSeconds: number): void;
    DebugPrint(Message: string): void;
    SetDebug(DebugMode: boolean, DebugID?: string, MaxLinesOut?: number, ScrollIntoView?: boolean): void;
    Print(Message: string): void;
    Assert(Condition: boolean, Message: string): void;
    Alert(Message: string): void;
}

State Local Variables

Each state must (eventually) specify an object of events. See state A, below. It specifies events ENTER and TIMEOUT within an object. If you want to specify one or more variables that are local only to the state, you can instead specify a function that returns the required object of events. An example is state B below. We specify a function that declares state-local variables and then returns the object of events. All the variables declared form a closure with state B's object of events. This is very useful for, say, caching jQuery elements for the life of the state.

import { StateMachine } from '../OOSMOS';

class TimeoutDemo extends StateMachine {
  constructor() {
    super({ DEFAULT: 'A',
      A: {
        ENTER: () => {
          this.SetTimeoutSeconds(4);
        },
        TIMEOUT: function() {
          this.Transition('B');
        },
      },
      B: () => {
        var Timeouts = 0;

        return {
          ENTER: function() {
            this.SetTimeoutSeconds(1);
          },
          TIMEOUT: function() {
            Timeouts += 1;
            this.Transition('A');
          },
        };
      },
    });
  }
}

const pTimeoutDemo = new TimeoutDemo();
pTimeoutDemo.SetDebug(true);
pTimeoutDemo.Start();

jQuery Example (fragments)

Note that we can establish jQuery event handlers in the ENTER event and then unbind them in the corresponding EXIT event.

  .
  .
  Active: {
    ENTER: () => {
      $('#Active').show();

      $('#eStop').click(()     => { this.Transition('Idle');   });
      $('#eRestart').click(f() => { this.Transition('Active'); });
    },

    EXIT: () => {
      $('#eStop').unbind('click');
      $('#eRestart').unbind('click');

      $('#Active').hide();
    }
  },
  .
  .

Further, we can use state-local variables to cache the jQuery selectors, like this:

  .
  .
  Active: function() {
    var $Active   = $('#Active');
    var $eStop    = $('#eStop');
    var $eRestart = $('#eRestart');

    return {
      ENTER: () => {
        $Active.show();

        $eStop.click(()     => { this.Transition('Idle');   });
        $eRestart.click(f() => { this.Transition('Active'); });
      },

      EXIT: () => {
        $eStop.unbind('click');
        $eRestart.unbind('click');

        $Active.hide();
      }
    };
  },
  .
  .

Debug Output

A StateName being entered is represented like this:

--> StateName

A StateName being exited is represented like this:

    StateName -->

A default state being entered is indicated like this:

==> StateName

Building

From the top level directory, type npm start. Note: It depends on Python.