UNPKG

oosmos

Version:

A Hierarchical State Machine Class

314 lines (251 loc) 11.4 kB
## 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](https://oosmos.com/OOSMOSjs) ### 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 **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. ![](http://www.oosmos.com/github/README.md/simple_timeout.svg) _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(); ``` ### State Machine Structure Overview 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(); ``` ### State Machine Structure Rules 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 ### 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.<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.| ### Support APIs |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 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.<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); [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. ```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(); ``` ### jQuery Example (fragments) 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(); } }; }, . . ``` ## 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`.