oosmos
Version:
A Hierarchical State Machine Class
314 lines (251 loc) • 11.4 kB
Markdown
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](https://oosmos.com/OOSMOSjs)
- 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 **O**bject **O**riented **S**tate **M**achine **O**perating **S**ystem. OOSMOS is a small footprint C/C++ state machine operating system for the industrial IOT space. (See [www.oosmos.com](http://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](http://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.
```javascript
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();
```
This is the basic layout of an `OOSMOS` state machine, explained below.
```javascript
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();
```
0. 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.*
0. `ENTER` and `EXIT` are optional. They are only supplied when there is something to do on entry or exit from the state.
0. If a timeout is desired for the state, it must be set up in the special `ENTER` event using the `SetTimeoutSeconds()` API.
0. If state names or event names contain special characters, they must be enclosed in quotes. (Standard JavaScript object property rules.)
|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.<br><br>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.<br><br>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`.<br><br>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.<br><br>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.<br><br>Must be called from within an event function. Any supplied arguments are passed to the `ENTER` function of the target state.|
|API|Description|
|---|---|
|`Alert(Message)`|When running under `Node.js`, executes a `console.log()` call.<br><br>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)`.<br><br>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.<br><br>The next three arguments apply only when running under a browser.<br><br>Required `DebugID` specifies the `id` of a `<div>` element into which debug lines will be written.<br><br>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.*<br><br>If `true`, optional `ScrollIntoView` causes each new debug line to come into focus.|
|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.<br><br>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);
[ ]: (() => void) | any;
DOTPATH?: string;
}
export interface iComposite {
DEFAULT?: 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;
}
```
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.
```javascript
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();
```
Note that we can establish `jQuery` event handlers in the `ENTER` event and then `unbind` them in the corresponding `EXIT` event.
```javascript
.
.
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:
```javascript
.
.
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();
}
};
},
.
.
```
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
```
From the top level directory, type `npm start`. Note: It depends on `Python`.
OOSMOS