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

Back to keep the state machine topic alive. In the meantime I did some reading about JS, node.js and the context areas for variables. This seems very special and not found in any other language. Its fundamental to understand the scope of node, flow and global to do anything with NodeRed. Graphical programming was invented with the intention to be easy, but Node Red is not. Without taking any look below the bonnet, I always asked: Why is programming Node Red that complicated?

Regarding the state machine I am going to limit my modifications to the traffic light example for the first. Its a simple functionality with behaviour known by everybody without explanations. Nice visualisations possible.

One minor thing I like to change next, is the injection node using a toggle switch. Its unwise to use it in any state machine example for education purpose as it is a hidden state machine itself using 2 states on and off. Instead, starting the flow should be done with unconditional injection node what always start the statemachine. According to the design rules of statemachines, it is required to introduce a 5th state to the traffic lights what is "off" if such functionality is required. Therefore, the transistion to one of the On-States should be triggered by another injection node instead of the timer. Hopefully I am able to make that soon.

Sorry, probably not relevant to finite state machines, which I have not used in Node-red...

I suggest that the one reason you find Node-red hard to grasp is may be that you are stuck in a mindset where a program acts sequentially upon some stored variables.

Node-red is a flow based language where data, in the form of messages, flows past the blocks of code (either functions or built-in nodes) which can modify the message and pass it on, reroute it or drop it entirely.

When I first looked at Node-red it was a familiar concept because I had previously learned SQL, where conceptually you SELECT some data by chucking the entire data stream through the code and the query reaches in as it passes and grabs the glittering fish that match your requirements.
I wish I could recall where I read that analogy because It's not so helpful when I try and explain it!

I have also envisaged a message as a railroad train and the nodes as stations. Everywhere the train stops people can get on or off and the train passes on along it's route. When the next train arrives, the platform is bare. There is no evidence left of the preceding train.
It's also possible for two trains to join at a station, or to split into two smaller trains with different destinations.

Node-red does offer context variables eg a count of passengers on the last train so that you can tell if this one is more crowded, but the best use of the language minimises use of these variables.

Another "mistake" new users make is to try and code everything in function nodes. It's understandable but I think the learning curve is easier to scale if you do as much as possible with the other kinds of node.

Its not only a mindset, its a specific class of application problems to solve. According to UML(2) (Booch, Jacobsen, Rumbaugh), the graphical logo shows a 3-dimensional cube what should illustrate the view from 3 directions to any application problem.

Database and message passing show WHAT is done, state machines show WHEN is something done and the class diagram shows WHO is doing something. To describe any unknown problem from only one view is most times incomplete and requires severe practice for humans.

Probably to recognize the number of statemachines, the number of their states and and transititions and/or the required variables to describe a problem is the only creative part of software design. Thats possibly one reason why the AI approach before failed that bad. Once we have a correct state diagramm or class diagram or whatever, the resulting code can be generated semi-automatically for the implementation in whatever language we want. If humans cannot describe the problem of what they want, AI might fail with the implementation or produces unusable results. Only good to recognize: Thats not what we want.

If you never used statemachines, you are probably cought in database related problems where the state modeling is trivial. For me, the state view is helpfull for the moment as the control applications typically done by PLC have most time trivial data flows.

"Context" is actually very important in most modern languages, especially scripting ones. Node-RED leans into that concept and translates it to the visual paradigm.

In other languages, you will have "local" and "global" variables and will depend on context (e.g. browser, server, module, or function, etc). Node-RED is not really any different.

Understanding your current scope is vital to all programming. Node-RED is no different.

As jbudd says, it feels like you are stuck in other worlds. Node-RED is certainly DIFFERENT but is not difficult. The concept of handing off data between nodes linked with wires doesn't really get much simpler.

More importantly, if you are getting caught up on context variables, you may well be approaching things incorrectly. The idea with a visual, flow-based programming tool is to use the data flow (message flow in Node-RED parlance) to pass information between the processing nodes. Context variables should be an exception not the rule.

State machines are a very specific programming tool and would be considered by most to be fairly advanced. They are one of those things that aren't really hard but can be difficult to get your head around to begin with (rather like long division!). You certainly don't need a FSM to deal with your original problem, it is merely an elegant solution.

Not never, just not in Node-red

Very likely!