Wanted: Simple state machine examples

When working with JavaScript in a full IDE such as VS Code, you can get all of this without needing to go full TypeScript.

Here is an example from my uibuilder nodes. I have a simple typedefs.js file and I reference that where needed. This is just the entry for the core contents of a Node-RED node in the runtime.

/** runtimeNode
 * @typedef {object} runtimeNode Local copy of the node instance config + other info
 * @property {Function} send Send a Node-RED msg to an output port
 * @property {Function} done Dummy done Function for pre-Node-RED 1.0 servers
 * @property {Function} context get/set context data. Also .flow and .global contexts
 * @property {Function} on Event listeners for the node instance ('input', 'close')
 * @property {Function} removeListener Event handling
 * @property {Function} log General log output, Does not show in the Editor's debug panel
 * @property {Function} warn Warning log output, also logs to the Editor's debug panel
 * @property {Function} error Error log output, also logs to the Editor's debug panel
 * @property {Function} trace Trace level log output
 * @property {Function} debug Debug level log output
 * @property {Function} status Show a status message under the node in the Editor
 * @property {object=} credentials Optional secured credentials
 * @property {string=} name Internal.
 * @property {string=} id Internal. uid of node instance.
 * @property {string=} type Internal. Type of node instance.
 * @property {string=} z Internal. uid of ???
 * @property {string=} g Internal. uid of ???
 * @property {[Array<string>]=} wires Internal. Array of Array of strings. The wires attached to this node instance (uid's)
 * @property {number=} _wireCount Count of connected wires
 * @property {string=} _wire ID of connected wire
 * @property {[Array<Function>]=} _closeCallbacks ??
 * @property {[Array<Function>]=} _inputCallback Input callback fn
 * @property {[Array<Function>]=} _inputCallbacks ??
 * @property {number=} _expectedDoneCount ??
 * @property {Flow=} _flow Full definition of this node's containing flow
 * @property {*=} _alias ??
 */

Similarly, using JSDoc headers for your own functions and variables gives VS Code everything it needs to help you.

Although the code editor in Node-RED uses Monaco which is the web version of VS Code, I don't think we can supply it with typedefs from an external file (though I may be wrong, I've not really investigated). It does have some core definitions in the function node anyway.

I'm pretty busy at the moment as it's exam-session time here in the UK, so not much spare time - so can I ask if have you a specific application for an FSM or is your interest more academic?

1 Like

Many thanks for your examples. My interests with FSM is practical, not academic. Assume all, I already have background knowledge with FSM for SW and HW design. I dont take care for Moore and Mealy machines this moment. Unfortunately, I do not have any experience with NodeRed nor JS. As mentioned in first post of this thread, my application is a energy storage system (ESS) what I build up myself last year using components from the dutch company Victron Energy. This system comes with a small Raspi - style controller what runs Venus-OS what is the Victron Energy flavour of Linux. At this system I found a preconfigured NodeRed what allows control over the complete installation. For this reason I decided to find node red useful and take a closer look on how it works.

Up on today, I already realized a very simple remote control for the DNO to throttle down the grid setpoint using GPIO inputs by NodeRed. This feature is not available with the system by default. Currently there are more tasks like balancing the solar production with the battery state of charge what I do by hand up on today. Its annoying to keep always an eye to the battery SOC. I plan to couple several of the systems to a private microgrid where the manual tasks grow on doing similar works than the redispatch engineers do for the transmission grids and connected powerhouse plants. NodeRed seems the ideal candidate to realize this functionallities as it does not need further hardware, can access low level IO, protocolls like MQTT and high level functionalities like reading from a weather forecast API in the Internet. There is already a chapter in the Victron user forum for Node Red but they do not support any applications beside the defaults. There are also example applications suplied by users of Victron systems here but their functionalities can be very diffrent. Luckily each Nodered user is able to realize its own opinions on what the ESs system should do.

While playing around with your FSM examples, I noticed the event driven approach of NodeRED is very diffrent thinking to other barebone or realtime uC systems. FSM are assumed for concurrent parallel execution and generates the events itself by transititions. On the other hand, NodeRead has the synchrous approach of message passing and seems hardly using asynchronous actions. Probably I have to open an separate thread if next question becomes more specific what cannot be solved by further reading and experiments. FSM was the question as it is something simply needed in the toolbox for basic operations to proceed with the ideas in the project.

That isn't really an accurate picture.

Lots of things in Node-RED (and the underlying Node.js and even more underlying v8 JS engine) happen asynchronously.

Node.js (and JavaScript in general) works primarily on a single threaded loop. So async actions are really slower activities releasing the loop for additional tasks while waiting for a response. Slow filing system activities being a prime example.

And, of course, you need to remember that node.js is also bound to the server OS. So it is definitely NOT a real-time service.

In terms on Node-RED itself, while we talk about "passing messages", this is really not what actually happens in a flow. In truth, the reason Node-RED can be as efficient as it is is because of the way JavaScript mostly passes variables by reference. As such, Node-RED's "messages" are simply a memory location reference passed between different functions. And this is why, very occasionally, people get caught out by an actual async action downstream impacting a node upstream and is why, if you put 2 wires out of a node, Node-RED will automatically do a clone of the message (which is relatively slow).

1 Like

Just to affirm that, while the fsm is waiting for the next event, any other messages in the flows will get serviced. There can be thousands of messages all active apparently at once.

You have a separate typedefs file. That's better than nothing, still feels like a walmart version of typescript haha.

But it means that:

  1. I don't have to write and maintain 2 files for every .js file like with TypeScript
  2. I don't have to learn what is virtually another language (more than enough with js, html and css thanks!)
  3. I can use JSDoc style notations everywhere, both in my js files (which is where I want things documented anyway) and in the single typedefs (which I only use for things I might need documenting across multiple js files anyway).

1 & 2 are the killers for me and why I don't ever want to learn TypeScript. Might be fine for large-scale development with a big team but it is just unnecessary overhead for a single dev mostly working on fairly simple, small things.

AKA…. Walmart tools are perfectly fine for most DIY users.

1 Like

Just had a few spare minutes to adapt the Door Lock System to use enum-style coding for events, states, outputs and ouput_signals. Also brought out the four outputs to separate ports.

[{"id":"49fe1f86ac046e65","type":"function","z":"bda7a9600da379e9","name":"FSM with enum-style for events and outputs","func":"// =========================\n// === ENUM DEFINITIONS ===\n// =========================\nconst States = Object.freeze({\n    LOCKED: \"LOCKED\",\n    UNLOCKED: \"UNLOCKED\",\n    ALARMED: \"ALARMED\"\n});\n\nconst Events = Object.freeze({\n    ENTER_CODE: \"ENTER_CODE\",\n    LOCK: \"LOCK\",\n    FORCE_ENTRY: \"FORCE_ENTRY\",\n    RESET_ALARM: \"RESET_ALARM\"\n});\n\nconst Outputs = Object.freeze({\n    LOCKED_MSG: \"Door is now LOCKED\",\n    UNLOCKED_MSG: \"Door is now UNLOCKED\",\n    ALARMED_MSG: \"ALARM! Forced entry detected\",\n    NO_CHANGE: \"No transition\"\n});\n\n// ===============================\n// === OUTPUT VALUES PER STATE ===\n// ===============================\nconst OutputSignals = Object.freeze({\n    [States.LOCKED]: Object.freeze({ door_lock: 1, solenoid: 0, led: \"red\" }),\n    [States.UNLOCKED]: Object.freeze({ door_lock: 0, solenoid: 1, led: \"green\" }),\n    [States.ALARMED]: Object.freeze({ door_lock: 1, solenoid: 0, led: \"flashing_red\" })\n});\n\n// ===============================\n// === TRANSITION TABLE FORMAT ===\n// ===============================\nconst transitions = {\n    [States.LOCKED]: {\n        [Events.ENTER_CODE]: (msg) =>\n            msg.code === correctCode\n                ? { nextState: States.UNLOCKED, output: Outputs.UNLOCKED_MSG }\n                : { nextState: States.LOCKED, output: Outputs.NO_CHANGE },\n        [Events.FORCE_ENTRY]: () =>\n            ({ nextState: States.ALARMED, output: Outputs.ALARMED_MSG })\n    },\n\n    [States.UNLOCKED]: {\n        [Events.LOCK]: () =>\n            ({ nextState: States.LOCKED, output: Outputs.LOCKED_MSG }),\n        [Events.FORCE_ENTRY]: () =>\n            ({ nextState: States.ALARMED, output: Outputs.ALARMED_MSG })\n    },\n\n    [States.ALARMED]: {\n        [Events.RESET_ALARM]: () =>\n            ({ nextState: States.LOCKED, output: Outputs.LOCKED_MSG })\n    }\n};\n\n// =====================================\n// === GET CURRENT STATE FROM CONTEXT ===\n// =====================================\nlet currentState = context.get(\"state\") || States.LOCKED;\n\n// ==========================\n// === EXTRACT EVENT INPUT ===\n// ==========================\nlet event = msg.payload;\nif (typeof event === \"object\") {\n    event = event.event;\n    msg.code = msg.payload.code;\n}\n\n// Simulated correct code\nconst correctCode = \"1234\";\n\n// =====================================\n// === FSM TRANSITION LOGIC ===\n// =====================================\nlet result = transitions[currentState]?.[event]?.(msg);\nif (result) {\n    currentState = result.nextState;\n} else {\n    result = {\n        nextState: currentState,\n        output: Outputs.NO_CHANGE\n    };\n}\n\n// Save state and update status\ncontext.set(\"state\", currentState);\nnode.status({ text: currentState });\n\n// ============================\n// === CREATE OUTPUT MESSAGES ===\n// ============================\nconst signal = OutputSignals[currentState];\n\nconst msg1 = { payload: signal.door_lock };     // door_lock output\nconst msg2 = { payload: signal.solenoid };      // solenoid output\nconst msg3 = { payload: signal.led };           // LED output\nconst msg4 = {                                   // state_info output\n    payload: {\n        state: currentState,\n        message: result.output\n    }\n};\n\nreturn [msg1, msg2, msg3, msg4];\n\n","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":670,"y":1020,"wires":[["b3cde1bcdc1090bf"],["cb7209dad7722f12"],["dbc7bacb1b54e030"],["34427a6c26214076"]]},{"id":"b3cde1bcdc1090bf","type":"debug","z":"bda7a9600da379e9","name":"door_lock","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":940,"y":960,"wires":[]},{"id":"cb7209dad7722f12","type":"debug","z":"bda7a9600da379e9","name":"solenoid","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":940,"y":1000,"wires":[]},{"id":"dbc7bacb1b54e030","type":"debug","z":"bda7a9600da379e9","name":"led","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":1040,"wires":[]},{"id":"e3e12730f4902d43","type":"inject","z":"bda7a9600da379e9","name":"Initally set door to LOCKED state","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"LOCKED","payloadType":"str","x":300,"y":880,"wires":[["49fe1f86ac046e65"]]},{"id":"34427a6c26214076","type":"debug","z":"bda7a9600da379e9","name":"system message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":970,"y":1080,"wires":[]},{"id":"92473381988995f2","type":"comment","z":"bda7a9600da379e9","name":"FSM with enum-style coding for states, events, outputs and output signals.","info":"","x":760,"y":920,"wires":[]},{"id":"560d450b17d9cccb","type":"group","z":"bda7a9600da379e9","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["a732bf62d25fc6d5","8f99d0e505064c3c","b18e34c862e100f3","ff39c2b3479cf250","d2bce1b0083cfed4","d6507a2d52ddfdcf"],"x":154,"y":919,"w":272,"h":302},{"id":"a732bf62d25fc6d5","type":"inject","z":"bda7a9600da379e9","g":"560d450b17d9cccb","name":"UNLOCK correct code","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"event\":\"ENTER_CODE\",\"code\":\"1234\"}","payloadType":"json","x":300,"y":1000,"wires":[["49fe1f86ac046e65"]]},{"id":"8f99d0e505064c3c","type":"inject","z":"bda7a9600da379e9","g":"560d450b17d9cccb","name":"LOCK","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"LOCK","payloadType":"str","x":250,"y":1080,"wires":[["49fe1f86ac046e65"]]},{"id":"b18e34c862e100f3","type":"inject","z":"bda7a9600da379e9","g":"560d450b17d9cccb","name":"RESET_ALARM","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"RESET_ALARM","payloadType":"str","x":280,"y":1180,"wires":[["49fe1f86ac046e65"]]},{"id":"ff39c2b3479cf250","type":"comment","z":"bda7a9600da379e9","g":"560d450b17d9cccb","name":"Simulated inputs","info":"","x":260,"y":960,"wires":[]},{"id":"d2bce1b0083cfed4","type":"inject","z":"bda7a9600da379e9","g":"560d450b17d9cccb","name":"FORCE_ENTRY","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"FORCE_ENTRY","payloadType":"str","x":280,"y":1140,"wires":[["49fe1f86ac046e65"]]},{"id":"d6507a2d52ddfdcf","type":"inject","z":"bda7a9600da379e9","g":"560d450b17d9cccb","name":"UNLOCK incorrect code","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"event\":\"ENTER_CODE\",\"code\":\"1233\"}","payloadType":"json","x":300,"y":1040,"wires":[["49fe1f86ac046e65"]]}]

Note: The nested objects have been made fully immutable using Object.freeze().

Output Port Label Example Payload
Output 1 door_lock 1 or 0
Output 2 solenoid 1 or 0
Output 3 LED red, green, flashing_red
Output 4 state_info { state: LOCKED, message: Door is now LOCKED }

Note: The script will accept a command as msg.payload string or an object as examples below...

Action Payload Format When Allowed
Unlock Door { event: ENTER_CODE, code: 1234 } When state == LOCKED
Lock Door LOCK or { event: LOCK } When state == UNLOCKED
Force Entry FORCE_ENTRY or { event: FORCE_ENTRY } When state == LOCKED or UNLOCKED
Reset Alarm RESET_ALARM or { event: RESET_ALARM } When state == ALARMED
2 Likes