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.

1 Like

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!

In the meantime, I crashed my system several times. Seems the node.js runtime is easily screwed up by any kinds of loops and even memory leaks or stack errors as I certainly tried erreonous parameter passings.

Last crash all my state machine modifications were unfortunately lost after I got NodeRed up again. As my system controls above described ESS, I first have to setup a new NodeRed system for playing around with new experiments.

About my recent learning curve, NodeRed offers plenty of possibilities using data flows. The underlying JS is a more "normal" language but its sometimes hard or impossible to escape from the node. NodeRed inventors simply dont want to have this as they offer other possibilities to do the job. Mainly the programmers view must not take care too much about the JIT implementations and performance results - its simply not like other realtime systems.

As a consequence for state machine programming I failed up on now, doing a asynchronous time system in the style I want to have. That point I noticed, the traffic light example is not a true state machine but a Petri Net: The timer blocks the flow while time elapses. In case of state machines, running time is a state and elapse of time is a event equal to a signal on any input port. Petri Nets can be very annoying with lock up situations why its use is more academic than practical. Equal to state machines, the code for a design can be generated automatically for any language. If the Petri Net is limited for the use of exactly one Token, its very similar to the state machine (but not equal). The PetriNet token represents the data flow of NodeRed and I am going to do certainly more experiments with that.

Not quite sure what you are getting at here?

Node-RED is absolutely NOT a "real time" system, not even close. It is not meant to be either. If you need that, you need to start with a real time OS and use languages suited to real time programming.

Node-RED is a general purpose visual programming environment. It is great for simple ETL, prototyping and non-realtime interfaces. And for easy to programme data-driven UI's.

For such things, it is incredibly easy to use and rock solid in use.

But like any other visual tool, once you start to push hard at the edges, if you don't understand the underlying platforms and their strengths and weaknesses, you WILL get caught out.

You mention 2 areas where Node-RED is comparatively weak.

Loops - mostly we try to avoid any looping in Node-RED, many workflows don't need them at all, in other cases (e.g. UIBUILDER's control outputs feeding back to a cache node), node authors will hopefully have made provision (which I did :grinning_face:). If not then, of course, the flow author must do so. Not really sure this is actually a weakness though. More a consequence of visual-style programming.

Memory leaks and stack exhaustion are areas where Node.js itself is a little weak in my view. They are hard to track down and debug. By definition, they are often caused by missing something non-obvious in the programming. Some tools do now exist to help but they seem to be pretty advanced usage so not everyone will be able to use them. Not helped by the fact that, being a high-level language, the use of memory in general and the stack explicitly is not something everyone learns about, it can be quite obscure.

But, bottom line is that Node-RED uses Node.js which is certainly not real-time and runs on non-realtime OS's. It should not be compared to low-level languages let alone realtime systems.

If plugin is installed, discourse supports mermaid.js rendering, which means you can make FSM diagrams from simple text markup - so I am going to test it.

Up from V3.1, NodRed also knows Mermaid, so you can describe your FSM and it will draw the diagram for visual documentation. Here are the traffic lights by @dynamicdave

stateDiagram-v2
    [*] --> red
    red --> |time| red_yellow
    red_yellow --> green
    green --> yellow
    yellow --> red

Edit: Obviously the plugin is not enabled in this forum by the admins. Here is a screendump on how the FSM diagram appears inside my NodeRed flow documentation tab.

Here are some modifications to @dynamicdave 's traffic lights.

  1. If you click to the info tab (below deploy button), a description of the flow appears what contains a Mermaid.js generated state diagram to illustrate the traffic lights functionality

  2. The FSM is always active. Switch off might be implemented later using additional off state

  3. Added a pedestrian lights to show the diffrence between Moore and Mealy outputs

  4. State information is passed as a parameter to the Moore output decoder.
    (Not using the flow scope GlobalState)

  5. Explicit function for initialisation starts the FSM in defined state with defined outputs

  6. Init and comment Nodes contain further hints in their description tab for general FSM design

For the moment I am not able to get the timer system to the style what I want. The timer blocks the FSM loop what is not in accordance with FSM desgin. It is also not in accordance with Node Reds message passing approach as the timers return value is always msg.payload = undefined.

What I expect is any return value like a value for remaining time, at least TRUE for running time and FALSE for elapsed time. Obviosly this requires a seperate timer flow to escape Node Reds synchronous timer node. This case, the FSM node could pass its local state infomation to itself back in the FSM loop. Doing so, probably allows to drop the use of flow scoped GlobalState variable.

[{"id":"be3ab1ef.3f0a68","type":"tab","label":"Traffic Lights with events and times","disabled":false,"info":"This is a state machine example for Node Red. Graphical FSM  representation is done using Mermaid code what is supported up from NodeRed V3.1. To be recognized, the mermaid code needs to be included in three tilde characters followed by the keyword mermaid. This tilde character is only available at the key beside backspace and can be accessed by pressing the shift key only. \n\nTo get a good FSM design, never use composite states, nor fork and join. \n\n```mermaid\nstateDiagram-v2\n    [*] --> red\n    red --> red_yellow : time\n    red_yellow --> green : time\n    green --> yellow : time\n    yellow --> red : time\n```\n","env":[]},{"id":"1404d113b2d5ca0f","type":"group","z":"be3ab1ef.3f0a68","name":"Cars trffic lights output viusalisation","style":{"label":true},"nodes":["743e732.c12228c","778d9f53.8e5f7","c8a94827.acb49","1ef063a1275b6a52"],"x":874,"y":679,"w":312,"h":262},{"id":"23fca6ab8e2d47fc","type":"group","z":"be3ab1ef.3f0a68","name":"Pedestrians traffic lights output visualisation","style":{"label":true},"nodes":["8fcd17210933c672","0c4c0544b7bd6e2d","9d2a9a85ab1c0aca"],"x":874,"y":399,"w":312,"h":202},{"id":"743e732.c12228c","type":"function","z":"be3ab1ef.3f0a68","g":"1404d113b2d5ca0f","name":"Cars RED light","func":"if ((msg.payload === \"red\") || (msg.payload === \"red_yellow\"))\n   {msg.payload = 1;\n    node.status({fill:\"red\",shape:\"dot\",text:\"Red ON\"});\n   }\n   \nelse\n   {msg.payload = 0;\n    node.status({});\n   }\nreturn;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":980,"y":780,"wires":[[]]},{"id":"778d9f53.8e5f7","type":"function","z":"be3ab1ef.3f0a68","g":"1404d113b2d5ca0f","name":"Cars YELLOW light","func":"if ((msg.payload === \"yellow\") || (msg.payload === \"red_yellow\"))\n   {msg.payload = 1;\n   node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});}\n\nreturn;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":840,"wires":[[]]},{"id":"c8a94827.acb49","type":"function","z":"be3ab1ef.3f0a68","g":"1404d113b2d5ca0f","name":"Cars GREEN light","func":"if (msg.payload ===\"green\")\n   {msg.payload = 1;\n   node.status({fill:\"green\",shape:\"dot\",text:\"Green ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":900,"wires":[[]]},{"id":"28cc6bcb.920094","type":"function","z":"be3ab1ef.3f0a68","name":"Traffic Lights State Machine","func":"// FSM implemented by switch / case \n\nvar LocalState = flow.get(\"GlobalState\");\nvar pRed;\nvar pGrn;\n\nswitch (LocalState)\n    {\n        case 0:                             // state red\n            msg3 = {payload:\"red_yellow\"};  // pass state info for Moore outputs\n            pRed = {payload:\"on\"};\n            pGrn = {payload:\"off\"};         // swich Pedestrians Green on\n            msg4 = {delay:1000};            // start red_yellow time\n            LocalState = 1;                 // next state red_yellow \n            break;\n            \n        case 1:                          // red_yellow\n            LocalState = 2;              // next state green\n            msg3 = {payload:\"green\"};\n            msg4 = {delay:3000};         // start green time\n            break;\n        \n        case 2:                         // state green\n            LocalState = 3;             // next state yellow\n            msg3 = {payload:\"yellow\"};  \n            msg4 = {delay:1000};        // start yellow time\n            break;\n            \n        case 3:                          // yellow\n            LocalState = 0;              // red time\n            msg3 = {payload:\"red\"};\n            pRed = {payload:\"off\"};     // switch Pedestrians Green off\n            pGrn = {payload:\"on\"};      // swich Pedestrians Green on\n            msg4 = {delay:5000};\n            break;\n    }\n\nflow.set(\"GlobalState\", LocalState);                // save state information\nnode.status({text:\"State = \" + LocalState});        // display state information\nreturn [pRed,pGrn,msg3,msg4];\n","outputs":4,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":580,"wires":[["8fcd17210933c672"],["0c4c0544b7bd6e2d"],["743e732.c12228c","778d9f53.8e5f7","c8a94827.acb49"],["82ab245c.6aa488"]]},{"id":"82ab245c.6aa488","type":"delay","z":"be3ab1ef.3f0a68","name":"Timer","pauseType":"delayv","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":450,"y":600,"wires":[["28cc6bcb.920094"]]},{"id":"43e78726a5c704c9","type":"inject","z":"be3ab1ef.3f0a68","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":310,"y":540,"wires":[["9f03fd51549f6c27"]]},{"id":"9f03fd51549f6c27","type":"function","z":"be3ab1ef.3f0a68","name":"Init","func":"var LocalState = flow.get(\"GlobalState\");\nvar pRed;\n\nnode.send( {delay:5000} );               \n// first red_yellow time\nLocalState = 1;\nflow.set(\"GlobalState\", LocalState);\npRed = {payload:\"on\"};\nreturn [pRed,msg];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":540,"wires":[["8fcd17210933c672"],["28cc6bcb.920094"]],"info":"Every FSM needs to be initialized at power up. This requires to set the state information what is depicted by a solid black start point in the Mermaid FSM diagram. \n\nFor the first state entry, Mealy Outputs needs to be initialized as well while Moore outputs rely on the state information iteself. Therefore this init funktion uses two outputs. One for the state machine what passes the initial state information, another for each output what passes the first event for the output signal.\n"},{"id":"8fcd17210933c672","type":"function","z":"be3ab1ef.3f0a68","g":"23fca6ab8e2d47fc","name":"Pedestrians RED","func":"if (msg.payload ===\"on\")\n   {msg.payload = 1;\n   node.status({fill:\"red\",shape:\"dot\",text:\"Red ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":500,"wires":[[]]},{"id":"0c4c0544b7bd6e2d","type":"function","z":"be3ab1ef.3f0a68","g":"23fca6ab8e2d47fc","name":"Pedestrians GREEN","func":"if (msg.payload ===\"on\")\n   {msg.payload = 1;\n   node.status({fill:\"green\",shape:\"dot\",text:\"Green ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1000,"y":560,"wires":[[]]},{"id":"9d2a9a85ab1c0aca","type":"comment","z":"be3ab1ef.3f0a68","g":"23fca6ab8e2d47fc","name":"This is a Mealy FSM architecture","info":"The Mealy architecture should be always prefered for software FSM designs to keep the number of required states essential. Mealy output behaviour is independend from state information and might be combined with other combinatorical signals. This allows to keep the number of states essential.  **Mealy outputs connect to the transition events instead of states. **\n\nFirst step for system design is to determine the number and names of input and output signals of the project. For any given outputs, there is the next step to try the the signal generation by using a boolean equation from inputs. \n\nIf not possible, the problem is sequential for what we have to introduce a state. It is the job of experienced humans to reqcognize the correct number of FSM and its states and give them approprate names.\n","x":1030,"y":440,"wires":[]},{"id":"1ef063a1275b6a52","type":"comment","z":"be3ab1ef.3f0a68","g":"1404d113b2d5ca0f","name":"This is a Morre FSM architecture","info":"Outputs for Moore FSM always depend from the state information. The Moore style output decoder is included only for information. Personally I always prefere the Mealy style for modeling.\n","x":1030,"y":720,"wires":[]}]
1 Like

Finally I have a timer system for FSM in the style I wanted. To escape from NodeReds synchronous timer system, attached example shows two flows in one tab. The upper flow contains a simple blinker FSM with only two states: On and Off. The state indicator toggles between 0 and 1, yellow blinker node should visualize a yellow blinking dot below.

The lower flow is a auxiliary timer loop used by the FSM. The Async FSM Timer Node always show a blue dot with 1 what indicates running time. This is, because the yellow blinker is always active. One state counts the off-time, the other state counts the on-time. Occasionally we see a small flicker on the blue dot. This happens sometimes for one cycle only, where time is elapsed and Node Red is doing the handshake over the flow-scoped BlinkTimer Variable before starting the timer to count the next time again.

Recent insights of this experiment

  1. Node Reds execution Flow times shows reasonable fast cycles what are typically anything between 1mSec and 10mSec. Any worst cases for possible jitter not evaluated. Assume all, realtime performance is better than expected or required for all typical Node Red applications.

  2. Inside a function node, any Javascript loop using do/while or non terminating for-loop will block anything inside the Node Red tab. Obviosly Node Red uses a cooperative multitask what will switch between each of the nodes. As a consequence, Node Red does not allow any graphical link to its own node. To escape from this limitation, the example shows inserted DummyFunctions inside both loops. The example contains two diffrent and asynchronous loops, both working contemporary.

  3. There are probably many other solutions more simple with Node Red to make a blinking dot. This one follows the state machine design typically used for realtime applictions.

More details to explore

  1. replace global state variable
    Following the message passing architecture of Node Red, I like to replace the flow global state variable by a local variable passed from and to the FSM node as a parameter on stack. This should be feasable, as the timer is no more blocking the data flow and DummyFunct might pass out what is coming in without any changes.

  2. replace global timer variable
    Accidently I stumbled over the BigTimer - Scargill's Tech Blog project but got mad of all the possiblities without taking a look under its bonnet. There are too many possibilites not really mandatory for any problem, but nothing similar what I used in this example for the FSM times. To remove the global timer variables and even replace the timer flow of this example with a self written timer node could be feasable. From FSM view, this only requires to function calls.

One to start the timer: SetTimer (Timevariable);
One to check if time is elapsed: TestTimer (Timervariable);

A (Timervariable) parameter will be required for each contemporary running time. The timer loop used in this example is required for each active FSM. It could be eliminated by only one what derives the time from system time UTC time stamp. Examples and suggestions welcome.

[{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"2bf1285a50be6ae1","type":"group","z":"aacb7c81f89be079","name":"Auxilliary timer loop to escape from NodeReds synchronous timer desgin","style":{"label":true},"nodes":["11f4989c0dba9de8","1ff5cd5581ca6ad5","bf02810cef9dfd9b","7882b3c7fcbb8b55","407daca816bddd54","57c0bd75ad81b2ae","e1dd832363b3052d"],"x":214,"y":639,"w":992,"h":142},{"id":"24f2582fc405a8d1","type":"group","z":"aacb7c81f89be079","name":"Simple blink FSM with On and Off State","style":{"label":true},"nodes":["7334cf2d08ec61be","83b08ea7f44c0d4b","12d6c1b1507be065","8b99ba6916511706","b3c9ca3be64d4c23","fb360390aafa7be4"],"x":214,"y":439,"w":832,"h":162},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"Async FSM Timer","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":650,"y":740,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"","props":[{"p":"delay","v":"0","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":310,"y":740,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"TestTimer","func":"var LocalTimer = flow.get(\"GlobalTimer\");       // get actual timer value\nvar msg1;\nvar msg2;\n\nif (LocalTimer == 0)\n    {\n      msg2 = {payload:\"stop\"};\n      return [null, msg2]\n    }\n\nelse\n    {\n      msg1 = {delay:LocalTimer};\n      return [msg1, null];\n    }\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1120,"y":740,"wires":[["11f4989c0dba9de8"],["e1dd832363b3052d"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"TimerInit","func":"var LocalTimer = flow.get(\"GlobalTimer\");   // flow socpe, non volatile\nLocalTimer = 0;                           // first time value\nflow.set(\"GlobalTimer\", LocalTimer);        // initialize global timer value\nmsg.delay = LocalTimer;                     // pass initial value to timer\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":740,"wires":[["11f4989c0dba9de8"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"TimeElapsed","func":"var LocalTimer = flow.get(\"GlobalTimer\");       // get actual timer value\nLocalTimer = 0;                                 // confirm elapsed time by handshake\nflow.set(\"GlobalTimer\", LocalTimer);            // save confirmed data \nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":680,"wires":[["bf02810cef9dfd9b"]]},{"id":"57c0bd75ad81b2ae","type":"comment","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"1=Timer Running, 0=Timer Stopped","info":"","x":640,"y":680,"wires":[]},{"id":"e1dd832363b3052d","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"DummyFunct","func":"return msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":740,"wires":[["bf02810cef9dfd9b"]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar LocalBTimer = flow.get(\"GlobalTimer\");       // get actual values\nvar LocalBState = flow.get(\"GlobalBState\");\nvar pYel;\n\nswitch (LocalBState)\n{\n  case 0:                             // state OFF\n    if (LocalBTimer == 0)\n    {\n      pYel = {payload:\"on\"};\n      LocalBTimer=543;\n      flow.set(\"GlobalTimer\" , LocalBTimer)\n      LocalBState = 1;                // next state ON\n    }  \n    break;\n    \n  case 1:                          // state ON\n    if (LocalBTimer == 0) \n    {\n      pYel = {payload:\"off\"};\n      LocalBTimer=567;\n      flow.set(\"GlobalTimer\" , LocalBTimer)\n      LocalBState = 0;             // next state OFF\n    }\n    break;\n}\n\nflow.set(\"GlobalBState\", LocalBState);              // save state information\n                                                    // do not save Globaltimer, poll only\nnode.status({text:\"State = \" + LocalBState});       // display state information\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":560,"wires":[["8b99ba6916511706"],["b3c9ca3be64d4c23"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":310,"y":500,"wires":[["12d6c1b1507be065"]]},{"id":"12d6c1b1507be065","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"Init","func":"var LocalBState = flow.get(\"GlobalBState\");\nvar pRed;\n\nLocalBState = 1;\nflow.set(\"GlobalBState\", LocalBState);\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":500,"wires":[["7334cf2d08ec61be"]],"info":"Every FSM needs to be initialized at power up. This requires to set the state information what is depicted by a solid black start point in the Mermaid FSM diagram. \n\nFor the first state entry, Mealy Outputs needs to be initialized as well while Moore outputs rely on the state information iteself. Therefore this init funktion uses two outputs. One for the state machine what passes the initial state information, another for each output what passes the first event for the output signal.\n"},{"id":"8b99ba6916511706","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"YellowBlinker","func":"if (msg.payload ===\"on\")\n   {msg.payload = 1;\n    node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":950,"y":540,"wires":[[]]},{"id":"b3c9ca3be64d4c23","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"DummyFunct","func":"return msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":560,"wires":[["7334cf2d08ec61be"]]},{"id":"fb360390aafa7be4","type":"comment","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"FSM use async timers, puls witdh 4:6","info":"","x":730,"y":480,"wires":[]}]

Sorry, I don't understand this. You can certainly loop a node's output back to its input - what am I missing?

Here are 2 ways to do this without needing an extra fn node:

One with a simple junction (it looks a little funky so I don't really like this. I prefer the 2nd with a link out mapped to a link in.

That is quite an old node now. While it still works fine, as you say, the options are rather over-complex. Most of us prefer to use the CRON+ node if we need complex timings.