Wanted: Simple state machine examples

Many thanks for the hint with the junction node. I was not aware for this element up on now. Whatever it is doing and for whatever it is intended, it replaces my previous used fake-dummy functions. In correct position, graphic is not looking funky. Only distance between the lines could be more than the line width itself. The U-turns are even touching. Distance between two link-lines should be same than distance from line to node.

The FSM state information now lives on the stack. No more use of the previous flow-global variable required.

[{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"aab3194343b5f7ad","type":"junction","z":"aacb7c81f89be079","x":540,"y":580,"wires":[["7334cf2d08ec61be"]]},{"id":"92a8498404719654","type":"junction","z":"aacb7c81f89be079","x":900,"y":740,"wires":[["bf02810cef9dfd9b"]]},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","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":590,"y":740,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"delay","v":"0","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":250,"y":740,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","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":1000,"y":740,"wires":[["11f4989c0dba9de8"],["92a8498404719654"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","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":400,"y":740,"wires":[["11f4989c0dba9de8"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","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":790,"y":740,"wires":[["bf02810cef9dfd9b"]]},{"id":"57c0bd75ad81b2ae","type":"comment","z":"aacb7c81f89be079","name":"1=Timer Running, 0=Timer Stopped","info":"","x":620,"y":680,"wires":[]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar Timer = flow.get(\"GlobalTimer\");        // get actual timer state\nvar state = msg.payload;                    // pop own state from stack\nvar pYel;\n\nswitch (state)\n{\n  case 0:                                   // state OFF\n    if (Timer == 0)                         // wait for off time elapsed\n    {\n      pYel = {payload:\"on\"};                // switch output for yellow light on\n      Timer=400;                            // start on time\n      flow.set(\"GlobalTimer\" , Timer)\n      state = 1;                            // next state ON\n    }  \n    break;\n    \n  case 1:                                   // state ON\n    if (Timer == 0)                         // wait for ON time elapsed\n    {\n      pYel = {payload:\"off\"};               // switch output for yellow light off\n      Timer=600;                            // start off time\n      flow.set(\"GlobalTimer\" , Timer)\n      state = 0;                            // next state OFF\n    }\n    break;\n}\n\nnode.status({text:\"State = \" + state});     // display state information\nmsg = {payload:state};                      // push state on stack until to next cycle\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":660,"y":580,"wires":[["8b99ba6916511706"],["aab3194343b5f7ad"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":250,"y":580,"wires":[["12d6c1b1507be065"]]},{"id":"12d6c1b1507be065","type":"function","z":"aacb7c81f89be079","name":"Init","func":"msg.payload = 0;            // start in state 0\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":580,"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","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":890,"y":580,"wires":[[]]},{"id":"fb360390aafa7be4","type":"comment","z":"aacb7c81f89be079","name":"FSM use async timers, puls witdh 4:6","info":"","x":610,"y":540,"wires":[]}]

Although I have reading all the postings, I've been busy with students to have contributed anything useful recently.

I did actually show a loop using Link-Out and Link-In nodes fairly early on in this thread.

I think you may have posted the wrong flow as it still using flow variables.

Old version possibly happens by use of clipboard. Here again

[{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"aab3194343b5f7ad","type":"junction","z":"aacb7c81f89be079","x":460,"y":200,"wires":[["7334cf2d08ec61be"]]},{"id":"92a8498404719654","type":"junction","z":"aacb7c81f89be079","x":900,"y":280,"wires":[["bf02810cef9dfd9b"]]},{"id":"94fa440b1594b51f","type":"junction","z":"aacb7c81f89be079","x":1060,"y":280,"wires":[["11f4989c0dba9de8"]]},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","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":570,"y":280,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":230,"y":280,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","name":"TestTimer","func":"\nif (flow.get(\"GlobalTimer\") == 0) return [null, {payload:0}];   // wait for time elapsed\nelse return [{delay:flow.get(\"GlobalTimer\")}, null];            // start next time\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":980,"y":280,"wires":[["94fa440b1594b51f"],["92a8498404719654"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","name":"TimerInit","func":"flow.set(\"GlobalTimer\", 0);                  // initialize global timer value\nmsg.delay = 0;                               // pass initial value to timer\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":280,"wires":[["11f4989c0dba9de8"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","name":"TimeElapsed","func":"flow.set(\"GlobalTimer\", 0);            // confirmed elapsed time to the FSM \nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":280,"wires":[["bf02810cef9dfd9b"]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","name":"Blink State Machine","func":"// Blinker FSM, input signal events and time events are treated equal \n// by the use of asynchronus running auxillary timer loop\n\nvar pYel;                                   // Yellow light output parameter port\n\nswitch (msg.payload)                        // pop own state information from stack\n{\n  case 0:                                   // state OFF\n    if (flow.get(\"GlobalTimer\") == 0)       // wait for off time elapsed\n    {\n      pYel = {payload:\"on\"};                // switch output for yellow light on\n      flow.set(\"GlobalTimer\", 400);         // start on time\n      msg = {payload:1};                    // next state ON\n    }  \n    break;\n    \n  case 1:                                   // state ON\n    if (flow.get(\"GlobalTimer\") == 0)       // wait for ON time elapsed\n    {\n      pYel = {payload:\"off\"};               // switch output for yellow light off\n      flow.set(\"GlobalTimer\", 600);         // start off time\n      msg = {payload:0};                    // next state OFF\n    }\n    break;\n}\n\nnode.status({text:\"State = \" + state});     // display state information\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":580,"y":200,"wires":[["8b99ba6916511706"],["aab3194343b5f7ad"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":230,"y":200,"wires":[["12d6c1b1507be065"]]},{"id":"12d6c1b1507be065","type":"function","z":"aacb7c81f89be079","name":"Init","func":"msg.payload = 0;            // start in state 0\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":200,"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","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":790,"y":200,"wires":[[]]}]

GlobalTimer is the only flow scope variable now. Further more local variables dropped to simplify the number of code lines.

Although deploy is always successfull, I am receiving this unexpected messages frequently from time to time without touching anything.

The problem was not a loop to other nodes but a loop to the own node where its coming from.

1 Like

There was a mistake in line 27 of the blink FSM node. Obviosly JS is not complaining about reading variables never written nor defined but the runtime does not know about on how to handle this. - Anything crashed - its no more blinking

Do you have 2 tabs with node-red (or another browser) open ? Then you get these conflicts.

Maybe accidently - I am going to take care for this. Going back to one of my earlier versions and come back later.

If you dont declare a variable, the Monaco editor will let you know.


And after closing, the editor will apply a badge
image

The runtime does not know how to handle what?
If the variable is not defined (either by const/let/var or old skool implicit declaration myvar="something"), then the function will throw a (catchable) error. This is standard JS behaviour. This will not crash node-red unless the error occurs in an async callback and is unhandled (e.g. not surrounded with try..catch)

This does not make sense unfortunately. Did your Node-RED crash? Did you look at the logs for a stack trace?


Critique and observations

NOTE, I have not followed this thread entirely so the please forgive anything below that has been discussed or previously acknowledged. Also, please note I mean no offence but rather hope to offer a quick review in the hope it helps :innocent:

Summary:

I am not keen on this approach. I often advise folk to avoid loops, dont use functions for everything, try to avoid sideeffects in your functions (aka pure).
When I loaded the last 2 flow exports they were pretty much everything I avoid. To even begin to understand what the flows were doing, I had to open and read the code of every function. That is kinda the opposite of what Node-RED and flow base programming attempts to achieve.

Details:

What I see in your flows is:

  • lots of function nodes (a bit anti pattern / hard to visually understand what is happening) (also function nodes being used where simple switch or change nodes would suffice)
  • lots of looping (not wise - easy to end up with multiple msgs traveling round the wires and easy to end up in a constant highspeed blocking loop)
  • (over) use of context. This can (will) lead to bugs when messages follow each other in quick succession (upstream operation can modify context before an inflight downstream operation reads context). Additionally, context is pretty much akin to using globals in any language. It is not recommended due to the many places of change that can occur leading to sideffects.

Closing:

I do understand the desire to make an FSM however in many cases, just drawing a visual self explaining flow to do the thing you want is often quicker and a more reliable approach. That said, I realise this is a simple demo and likely not your end goal (and its good learning too)

1 Like

Just to point out that while the editor(rightly) won't let you connect a node's output directly to it's input, it is perfectly happy if you use a junction or other node between and then ctrl-delete (delete and reconnect) the other node.

@jbudd very tricky, not anything intuitive but works perfect

Back with a older version. At least this blinks reliable.

[{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","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":470,"y":240,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"delay","v":"0","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":110,"y":240,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","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":880,"y":240,"wires":[["11f4989c0dba9de8"],["bf02810cef9dfd9b"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","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":260,"y":240,"wires":[["11f4989c0dba9de8","7cd464bea0f069f4"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","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":690,"y":240,"wires":[["bf02810cef9dfd9b"]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar Timer = flow.get(\"GlobalTimer\");        // get actual timer state\nvar state = msg.payload;                    // pop own state from stack\nvar pYel;\n\nswitch (state)\n{\n  case 0:                                   // state OFF\n    if (Timer == 0)                         // wait for off time elapsed\n    {\n      pYel = {payload:\"on\"};                // switch output for yellow light on\n      Timer=400;                            // start on time\n      flow.set(\"GlobalTimer\" , Timer)\n      state = 1;                            // next state ON\n    }  \n    break;\n    \n  case 1:                                   // state ON\n    if (Timer == 0)                         // wait for ON time elapsed\n    {\n      pYel = {payload:\"off\"};               // switch output for yellow light off\n      Timer=600;                            // start off time\n      flow.set(\"GlobalTimer\" , Timer)\n      state = 0;                            // next state OFF\n    }\n    break;\n}\n\nnode.status({text:\"State = \" + state});     // display state information\nmsg = {payload:state};                      // push state on stack until to next cycle\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":140,"wires":[["8b99ba6916511706"],["7334cf2d08ec61be"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":110,"y":140,"wires":[["12d6c1b1507be065","bae1cd30d2f42dc1"]]},{"id":"12d6c1b1507be065","type":"function","z":"aacb7c81f89be079","name":"FSM Init","func":"msg.payload = 0;            // start in state 0\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":140,"wires":[["7334cf2d08ec61be","2a9df74ced5babef"]],"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","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":690,"y":140,"wires":[[]]},{"id":"2a9df74ced5babef","type":"debug","z":"aacb7c81f89be079","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":440,"y":80,"wires":[]},{"id":"bae1cd30d2f42dc1","type":"debug","z":"aacb7c81f89be079","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":240,"y":80,"wires":[]},{"id":"7cd464bea0f069f4","type":"debug","z":"aacb7c81f89be079","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"delay","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":300,"wires":[]}]

The reason for the frequently alerts of version diffrence is not 2 open editor instances. Above flow is blinking reliable. Luckily I have a non productive test system where NodRed factory reset is no problem.

There are 3 debug nodes. With first deploy (after NodeRed factory reset) the debug output is like expected:

2-msg.payload boolean true
3-msg.delay number 0
1-msg.payload number 0

Yellow light starts blinking and everything seems ok. Simply moving the position of any node, gives the opportunity for a new deploy. With the deploy button Node red reports "successfully deployed". But there is no more debug output what I expect in this case of new initialisation. Possibly the JIT compile skips anything as it is already present from previous version. There is also no debug output if I change the FSM Init to start in state 1 what must not be optimized. Unfortunately, the debug output might follow minutes later without any user action accompanied by the version difference alert.

Edit: Also other simple flows do not work in parallel with the blink FSM. The reason seems, that the loops seems to steal all the computing time from the runtime what blocks everything after started once. Including proper deploy. Also deleting the complete flow takes a while until other flows work again.

The suspicious runtime problems seems solved by adding "brake" timers to slow down the loops. For both loops, my FSM and my aux timer loop I added a 100mS limit to the cycle time. Obviously NodeReds delay node respects the expectations of the runtime for cooperative multitask. Second tab is the table based FSM by @dynamicdave what works flawless contemporary without any changes. Also debug info now appears always like expected.

[{"id":"b5a1333e3bb6b1d0","type":"tab","label":"Statemachine Trafficlight","disabled":false,"info":"","env":[]},{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"106b9e284a8b6d25","type":"junction","z":"aacb7c81f89be079","x":640,"y":200,"wires":[["b84fef40d0752775"]]},{"id":"93814d1a6470edbc","type":"function","z":"b5a1333e3bb6b1d0","name":"Decode RED light","func":"var fsm_state = flow.get(\"state_counter\");\n\nif (fsm_state === 0 || fsm_state == 1)\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   }\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":60,"wires":[[]]},{"id":"db12c168fa131df2","type":"function","z":"b5a1333e3bb6b1d0","name":"Decode YELLOW light","func":"var fsm_state = flow.get(\"state_counter\");\n\nif (fsm_state == 1 || fsm_state == 3)\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 msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1000,"y":120,"wires":[[]]},{"id":"a162a116a95762d3","type":"function","z":"b5a1333e3bb6b1d0","name":"Decode GREEN light","func":"var fsm_state = flow.get(\"state_counter\");\n\nif (fsm_state == 2)\n   {msg.payload = 1;\n   node.status({fill:\"green\",shape:\"dot\",text:\"Green ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\n\nreturn msg;","outputs":1,"noerr":0,"x":1000,"y":180,"wires":[[]]},{"id":"1b98b5e06bd62acb","type":"inject","z":"b5a1333e3bb6b1d0","name":"Start & Stop Toggle","props":[{"p":"payload"},{"p":"delay","v":"1","vt":"num"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":190,"y":160,"wires":[["5213c067599bf3b1","958e1a7760c9bf69"]]},{"id":"5213c067599bf3b1","type":"function","z":"b5a1333e3bb6b1d0","name":"Start & Stop Indicator","func":"var status = flow.get(\"status\") || \"disable\";\n\nif (status == \"disable\") {\n    flow.set(\"status\", \"enable\");\n    node.status({text:\"Running\"});\n}\nelse {\n    flow.set(\"status\", \"disable\");\n     node.status({text:\"Stopped\"});\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":80,"wires":[[]]},{"id":"6c1d0d61a5b43642","type":"function","z":"b5a1333e3bb6b1d0","name":"EnableFSM","func":"var status = flow.get(\"status\") || \"disable\";\nif (status == \"enable\") {\n     node.status({text:\"Enabled\"});\n    return msg;\n}\nelse {\n     node.status({text:\"State = Disabled\"});\n     return null;\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":160,"wires":[["853d0f129c7a0e22"]]},{"id":"958e1a7760c9bf69","type":"delay","z":"b5a1333e3bb6b1d0","name":"FSM Timer","pauseType":"delayv","timeout":"1","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":410,"y":160,"wires":[["6c1d0d61a5b43642"]]},{"id":"853d0f129c7a0e22","type":"function","z":"b5a1333e3bb6b1d0","name":"FSM-v2","func":"// ****************\n// * State Enum   *\n// ****************\nconst STATES = {\n    RED: 0,\n    RED_AMBER: 1,\n    GREEN: 2,\n    AMBER: 3\n};\n\n// ****************\n// * State Config *\n// ****************\nconst STATE_TABLE = {\n    [STATES.RED]:       { delay: 1000,  lamps: [1, 0, 0], next: STATES.RED_AMBER },\n    [STATES.RED_AMBER]: { delay: 500,   lamps: [1, 1, 0], next: STATES.GREEN },\n    [STATES.GREEN]:     { delay: 1000,  lamps: [0, 0, 1], next: STATES.AMBER },\n    [STATES.AMBER]:     { delay: 1000,  lamps: [0, 1, 0], next: STATES.RED }\n};\n\n// ***************\n// * Get State   *\n// ***************\nlet state = flow.get(\"state_counter\") ?? STATES.RED;\nconst cfg = STATE_TABLE[state];\n\n// ***************\n// * Set Outputs *\n// ***************\nconst red    = { payload: cfg.lamps[0] };     // Output 1: red\nconst amber  = { payload: cfg.lamps[1] };     // Output 2: amber\nconst green  = { payload: cfg.lamps[2] };     // Output 3: green\nconst delayMsg = { delay: cfg.delay };        // Output 4: delay value ONLY in msg.delay\n\n// ***************\n// * Update State*\n// ***************\nflow.set(\"state_counter\", cfg.next);\n\n// ***************\n// * Show Status *\n// ***************\nnode.status({\n    fill: \"grey\",\n    shape: \"dot\",\n    text: `R:${cfg.lamps[0]} A:${cfg.lamps[1]} G:${cfg.lamps[2]} Delay:${cfg.delay}ms`\n});\n\n// ***************\n// * Return 4 outputs *\n// ***************\nreturn [red, amber, green, delayMsg];\n","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":160,"wires":[["93814d1a6470edbc"],["db12c168fa131df2"],["a162a116a95762d3"],["36dd8a118669a23f"]]},{"id":"36dd8a118669a23f","type":"link out","z":"b5a1333e3bb6b1d0","name":"OUT_msg_delay","mode":"link","links":["d7970e785518ca56","c4aa4ffe89814f17"],"x":915,"y":240,"wires":[]},{"id":"d7970e785518ca56","type":"link in","z":"b5a1333e3bb6b1d0","name":"IN_msg_delay","links":["36dd8a118669a23f"],"x":265,"y":240,"wires":[["958e1a7760c9bf69"]]},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","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":450,"y":340,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"delay","v":"0","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":110,"y":340,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","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":860,"y":340,"wires":[["11f4989c0dba9de8"],["d4379218167a7106"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","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":240,"y":340,"wires":[["11f4989c0dba9de8","7cd464bea0f069f4"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","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":670,"y":340,"wires":[["bf02810cef9dfd9b"]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar Timer = flow.get(\"GlobalTimer\");        // get actual timer state\nvar state = msg.payload;                    // pop own state from stack\nvar pYel;\n\nswitch (state)\n{\n  case 0:                                   // state OFF\n    if (Timer == 0)                         // wait for off time elapsed\n    {\n      pYel = {payload:\"on\"};                // switch output for yellow light on\n      Timer=400;                            // start on time\n      flow.set(\"GlobalTimer\" , Timer)\n      state = 1;                            // next state ON\n    }  \n    break;\n    \n  case 1:                                   // state ON\n    if (Timer == 0)                         // wait for ON time elapsed\n    {\n      pYel = {payload:\"off\"};               // switch output for yellow light off\n      Timer=600;                            // start off time\n      flow.set(\"GlobalTimer\" , Timer)\n      state = 0;                            // next state OFF\n    }\n    break;\n}\n\nnode.status({text:\"State = \" + state});     // display state information\nmsg = {payload:state};                      // push state on stack until to next cycle\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":140,"wires":[["8b99ba6916511706"],["106b9e284a8b6d25"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":110,"y":140,"wires":[["12d6c1b1507be065","bae1cd30d2f42dc1"]]},{"id":"12d6c1b1507be065","type":"function","z":"aacb7c81f89be079","name":"FSM Init","func":"msg.payload = 0;            // start in state 0\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":140,"wires":[["7334cf2d08ec61be","2a9df74ced5babef"]],"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","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":690,"y":140,"wires":[[]]},{"id":"2a9df74ced5babef","type":"debug","z":"aacb7c81f89be079","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":460,"y":80,"wires":[]},{"id":"bae1cd30d2f42dc1","type":"debug","z":"aacb7c81f89be079","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":280,"y":80,"wires":[]},{"id":"7cd464bea0f069f4","type":"debug","z":"aacb7c81f89be079","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"delay","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":280,"wires":[]},{"id":"b84fef40d0752775","type":"delay","z":"aacb7c81f89be079","name":"Speed Limit","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":250,"y":200,"wires":[["7334cf2d08ec61be"]]},{"id":"d4379218167a7106","type":"delay","z":"aacb7c81f89be079","name":"Speed Limit","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":870,"y":440,"wires":[["bf02810cef9dfd9b"]]}]

Here's one of my old projects (18-months ago) that uses a FSM to control my garage door.

Attached FSM works flawless with cycle time artifically reduced to 100mS by „SpeedLimit“ timers. The blue dot for async timer status is now clearly flickering for 100mS to 0, when handshake between timer and FSM happens. Before it was only visible occasionally and made many trouble blocking node.js runtime.

To satisfy the complaints of @Steve-Mcl, the FSMInit function node is replaced by a change node. Do not know on how to do the same for timer init as this needs to write into node- global variable. For the moment, the GlobalTimer is the only context variable in use. Maybe the timer loop can be replaced by a JS function based approach in future.

Local variables were dropped to reduce number of code lines.

Second tab contains the table based fsm traffic lights by @dynamicdave
I used for every change as a runtime health indicator. Version mismatch alerts never seen again.

The FSM code is now straightforward.

a- wait for anything. This can be events from inputs and/or time.
Event-time combinations were not possible before for the reason of blocking timer loop.
Now, time und input events are treated equal, no blocking loops for both..

b- switch anything. This can be actors at outputs or the start of a time

c- change state variable to next state. Next state can be diffrent and might depend from any input events.

The FSM toolbox seems now almost complete and I might going to to explore the 1000 other marvelous benefits of NodeRed. I am curious of wether forecast and sunrise/sunset events and of coarse to use a custom dashboard for my application.

To round up the FSM matter, I am going to port back my recent learnings into the case-based trafficlight example. To show the basic FSM possibilites, a pushbutton for pedestrians seems the ideal option. Red light for pedestrians will be very long. To reduce the waiting time, pedestrians might request green by pushing a button. This requires a state what polls two diffrent events contemporary: One is the pushbutton input, the other is the timer for red time.

[{"id":"b5a1333e3bb6b1d0","type":"tab","label":"Statemachine Trafficlight","disabled":false,"info":"","env":[]},{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"106b9e284a8b6d25","type":"junction","z":"aacb7c81f89be079","x":640,"y":180,"wires":[["b84fef40d0752775"]]},{"id":"93814d1a6470edbc","type":"function","z":"b5a1333e3bb6b1d0","name":"Decode RED light","func":"var fsm_state = flow.get(\"state_counter\");\n\nif (fsm_state === 0 || fsm_state == 1)\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   }\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":60,"wires":[[]]},{"id":"db12c168fa131df2","type":"function","z":"b5a1333e3bb6b1d0","name":"Decode YELLOW light","func":"var fsm_state = flow.get(\"state_counter\");\n\nif (fsm_state == 1 || fsm_state == 3)\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 msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1000,"y":120,"wires":[[]]},{"id":"a162a116a95762d3","type":"function","z":"b5a1333e3bb6b1d0","name":"Decode GREEN light","func":"var fsm_state = flow.get(\"state_counter\");\n\nif (fsm_state == 2)\n   {msg.payload = 1;\n   node.status({fill:\"green\",shape:\"dot\",text:\"Green ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\n\nreturn msg;","outputs":1,"noerr":0,"x":1000,"y":180,"wires":[[]]},{"id":"1b98b5e06bd62acb","type":"inject","z":"b5a1333e3bb6b1d0","name":"Start & Stop Toggle","props":[{"p":"payload"},{"p":"delay","v":"1","vt":"num"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":190,"y":160,"wires":[["5213c067599bf3b1","958e1a7760c9bf69"]]},{"id":"5213c067599bf3b1","type":"function","z":"b5a1333e3bb6b1d0","name":"Start & Stop Indicator","func":"var status = flow.get(\"status\") || \"disable\";\n\nif (status == \"disable\") {\n    flow.set(\"status\", \"enable\");\n    node.status({text:\"Running\"});\n}\nelse {\n    flow.set(\"status\", \"disable\");\n     node.status({text:\"Stopped\"});\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":80,"wires":[[]]},{"id":"6c1d0d61a5b43642","type":"function","z":"b5a1333e3bb6b1d0","name":"EnableFSM","func":"var status = flow.get(\"status\") || \"disable\";\nif (status == \"enable\") {\n     node.status({text:\"Enabled\"});\n    return msg;\n}\nelse {\n     node.status({text:\"State = Disabled\"});\n     return null;\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":160,"wires":[["853d0f129c7a0e22"]]},{"id":"958e1a7760c9bf69","type":"delay","z":"b5a1333e3bb6b1d0","name":"FSM Timer","pauseType":"delayv","timeout":"1","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":410,"y":160,"wires":[["6c1d0d61a5b43642"]]},{"id":"853d0f129c7a0e22","type":"function","z":"b5a1333e3bb6b1d0","name":"FSM-v2","func":"// ****************\n// * State Enum   *\n// ****************\nconst STATES = {\n    RED: 0,\n    RED_AMBER: 1,\n    GREEN: 2,\n    AMBER: 3\n};\n\n// ****************\n// * State Config *\n// ****************\nconst STATE_TABLE = {\n    [STATES.RED]:       { delay: 1000,  lamps: [1, 0, 0], next: STATES.RED_AMBER },\n    [STATES.RED_AMBER]: { delay: 500,   lamps: [1, 1, 0], next: STATES.GREEN },\n    [STATES.GREEN]:     { delay: 1000,  lamps: [0, 0, 1], next: STATES.AMBER },\n    [STATES.AMBER]:     { delay: 1000,  lamps: [0, 1, 0], next: STATES.RED }\n};\n\n// ***************\n// * Get State   *\n// ***************\nlet state = flow.get(\"state_counter\") ?? STATES.RED;\nconst cfg = STATE_TABLE[state];\n\n// ***************\n// * Set Outputs *\n// ***************\nconst red    = { payload: cfg.lamps[0] };     // Output 1: red\nconst amber  = { payload: cfg.lamps[1] };     // Output 2: amber\nconst green  = { payload: cfg.lamps[2] };     // Output 3: green\nconst delayMsg = { delay: cfg.delay };        // Output 4: delay value ONLY in msg.delay\n\n// ***************\n// * Update State*\n// ***************\nflow.set(\"state_counter\", cfg.next);\n\n// ***************\n// * Show Status *\n// ***************\nnode.status({\n    fill: \"grey\",\n    shape: \"dot\",\n    text: `R:${cfg.lamps[0]} A:${cfg.lamps[1]} G:${cfg.lamps[2]} Delay:${cfg.delay}ms`\n});\n\n// ***************\n// * Return 4 outputs *\n// ***************\nreturn [red, amber, green, delayMsg];\n","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":160,"wires":[["93814d1a6470edbc"],["db12c168fa131df2"],["a162a116a95762d3"],["36dd8a118669a23f"]]},{"id":"36dd8a118669a23f","type":"link out","z":"b5a1333e3bb6b1d0","name":"OUT_msg_delay","mode":"link","links":["d7970e785518ca56","c4aa4ffe89814f17"],"x":915,"y":240,"wires":[]},{"id":"d7970e785518ca56","type":"link in","z":"b5a1333e3bb6b1d0","name":"IN_msg_delay","links":["36dd8a118669a23f"],"x":265,"y":240,"wires":[["958e1a7760c9bf69"]]},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","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":430,"y":300,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":110,"y":300,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","name":"TestTimer","func":"if (flow.get(\"GlobalTimer\") == 0) return [{delay:\"stopped\"}, null] // wait for FSM\nelse return [null, {delay:flow.get(\"GlobalTimer\")}];               // start new time\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":300,"wires":[["d4379218167a7106"],["11f4989c0dba9de8"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","name":"TimerInit","func":"flow.set(\"GlobalTimer\", 0);        // initialize global timer value\nmsg.delay = 0;                     // pass initial value to timer\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":240,"y":300,"wires":[["11f4989c0dba9de8"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","name":"TimeElapsed","func":"flow.set(\"GlobalTimer\", 0);     // time elapsed, handshake for FSM \nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":300,"wires":[["bf02810cef9dfd9b"]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar pYel;                                   // parameter port for yellow output\n\nswitch (msg.payload)                        // pop state info as parameter from stack\n{ case 0:                                   // state OFF\n    if (flow.get(\"GlobalTimer\") == 0)       // wait for OFF time elapsed\n    { pYel = {payload:\"on\"};                // switch output for yellow light on\n      flow.set(\"GlobalTimer\", 400);         // start on time\n      msg.payload = 1;                      // next state ON\n    }  \n    break;\n\n  case 1:                                   // state ON\n    if (flow.get(\"GlobalTimer\") == 0)       // wait for ON time elapsed\n    { pYel = {payload:\"off\"};               // switch output for yellow light off\n      flow.set(\"GlobalTimer\" , 600)         // start off time\n      msg.payload = 0;                      // next state OFF\n    }\n    break;\n}\n\nnode.status({text:\"State = \" + msg.payload});     // display state information\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":140,"wires":[["8b99ba6916511706"],["106b9e284a8b6d25"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":110,"y":140,"wires":[["7dc2e27aea626843"]]},{"id":"8b99ba6916511706","type":"function","z":"aacb7c81f89be079","name":"YellowBlinker","func":"if (msg.payload ===\"on\") node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\nelse node.status({});\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":140,"wires":[[]]},{"id":"b84fef40d0752775","type":"delay","z":"aacb7c81f89be079","name":"Speed Limit","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":270,"y":180,"wires":[["7334cf2d08ec61be"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the FSM loop, timer resolution will be 100mS.\n"},{"id":"d4379218167a7106","type":"delay","z":"aacb7c81f89be079","name":"Speed Limit","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":950,"y":300,"wires":[["bf02810cef9dfd9b"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the timer loop, the maximum jitter will be 100mS.  "},{"id":"7dc2e27aea626843","type":"change","z":"aacb7c81f89be079","name":"FSM Init","rules":[{"t":"set","p":"payload","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":140,"wires":[["7334cf2d08ec61be"]]}]
1 Like

ahh hmmm, "Critique and observations" please! :rofl:

The flow design you are going for is the reason I wrote what I wrote. Take these comments:

And what I wrote a few hours earlier:

So I wasnt really "complaining" - just trying to steer you (and future readers)


So, forgetting for a moment this is about finite state machines, here is a 100% event based alternative to your most recent flows. Hopefully you can see it is more visually self explaining and hopefully gives you a few more tools in your endeavour to make the perfect FSM.

If you are interested, here is the demo flow (you will need node-red-contrib-cron-plus for this demo). Note also, it could also have been achieved with a function node and custom code using setTimeouts but I couldn't be bothered to write the logic and handle the edge cases when cron-plus can do it all for me (i'll leave it to you):

[{"id":"d5f1a7b5a23c9330","type":"function","z":"aacb7c81f89be079","name":"YellowBlinker","func":"if (msg.payload === \"on\" || msg.payload === 1) {\n    node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"})\n} else {\n    node.status({})\n}\nreturn msg\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":740,"wires":[["d72f625ab9c4af7a"]]},{"id":"01c6e574a44957d2","type":"inject","z":"aacb7c81f89be079","name":"FSM Init","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"1","payloadType":"num","x":120,"y":620,"wires":[["1702177de1479e39"]]},{"id":"1702177de1479e39","type":"switch","z":"aacb7c81f89be079","name":"payload == 0? \\n payload == 1?","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"num"},{"t":"eq","v":"1","vt":"num"}],"checkall":"true","repair":false,"outputs":2,"x":360,"y":620,"wires":[["a4fa2f3f3b61baef"],["418da2633199bddb"]]},{"id":"c7b0c0a708b23389","type":"link out","z":"aacb7c81f89be079","name":"link out 16","mode":"link","links":["055afafd32dfa98d"],"x":795,"y":600,"wires":[]},{"id":"a4fa2f3f3b61baef","type":"change","z":"aacb7c81f89be079","name":"Schedule 1 in 400ms","rules":[{"t":"set","p":"topic","pt":"msg","to":"traffic-light","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"1","tot":"num"},{"t":"set","p":"time","pt":"msg","to":"$millis() + 400","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":600,"wires":[["c7b0c0a708b23389"]]},{"id":"640145db8d169250","type":"link out","z":"aacb7c81f89be079","name":"link out 17","mode":"link","links":["055afafd32dfa98d"],"x":795,"y":640,"wires":[]},{"id":"418da2633199bddb","type":"change","z":"aacb7c81f89be079","name":"Schedule 0 in 600ms","rules":[{"t":"set","p":"topic","pt":"msg","to":"traffic-light","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"0","tot":"num"},{"t":"set","p":"time","pt":"msg","to":"$millis() + 600\t\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":640,"wires":[["640145db8d169250"]]},{"id":"0868153099c8ddb6","type":"link in","z":"aacb7c81f89be079","name":"scheduled event","links":["1d76fac6dd66d991"],"x":120,"y":740,"wires":[["72257b2cf1ac78c8","7f169edcee1c870d","8a44b65c23a423cd"]],"icon":"font-awesome/fa-bolt","l":true},{"id":"8cf6b141538bea22","type":"link in","z":"aacb7c81f89be079","name":"scheduled event","links":["1d76fac6dd66d991"],"x":120,"y":660,"wires":[["1702177de1479e39"]],"icon":"font-awesome/fa-bolt","l":true},{"id":"d72f625ab9c4af7a","type":"debug","z":"aacb7c81f89be079","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":850,"y":740,"wires":[]},{"id":"72257b2cf1ac78c8","type":"switch","z":"aacb7c81f89be079","name":"is traffic-light msg?","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"traffic-light","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":370,"y":740,"wires":[["d5f1a7b5a23c9330"]]},{"id":"7f169edcee1c870d","type":"switch","z":"aacb7c81f89be079","d":true,"name":"is something-else?","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"something-else","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":370,"y":800,"wires":[["321defd47e5b7de5"]]},{"id":"321defd47e5b7de5","type":"debug","z":"aacb7c81f89be079","d":true,"name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":850,"y":800,"wires":[]},{"id":"8a44b65c23a423cd","type":"debug","z":"aacb7c81f89be079","name":"msg counter.","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"","statusType":"counter","x":350,"y":880,"wires":[]},{"id":"81c43816b37471ea","type":"group","z":"aacb7c81f89be079","name":"Event Scheduler","style":{"label":true},"nodes":["64f9eca8e60b9404","1d76fac6dd66d991","055afafd32dfa98d","d00d2d62b0b8ecac"],"x":94,"y":459,"w":592,"h":82},{"id":"64f9eca8e60b9404","type":"cronplus","z":"aacb7c81f89be079","g":"81c43816b37471ea","name":"coordinator","outputField":"payload","timeZone":"","storeName":"","commandResponseMsgOutput":"fanOut","defaultLocation":"","defaultLocationType":"default","outputs":2,"options":[],"x":530,"y":500,"wires":[["1d76fac6dd66d991"],[]]},{"id":"1d76fac6dd66d991","type":"link out","z":"aacb7c81f89be079","g":"81c43816b37471ea","name":"link out 15","mode":"link","links":["0868153099c8ddb6","8cf6b141538bea22"],"x":645,"y":500,"wires":[],"icon":"font-awesome/fa-bolt"},{"id":"055afafd32dfa98d","type":"link in","z":"aacb7c81f89be079","g":"81c43816b37471ea","name":"schedule","links":["640145db8d169250","c7b0c0a708b23389"],"x":180,"y":500,"wires":[["d00d2d62b0b8ecac"]],"icon":"font-awesome/fa-clock-o","l":true},{"id":"d00d2d62b0b8ecac","type":"change","z":"aacb7c81f89be079","g":"81c43816b37471ea","name":"Prepare Schedule ","rules":[{"t":"set","p":"sendValue","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"{\"command\":\"add\",\"expressionType\":\"dates\",\"payloadType\":\"json\"}","tot":"json"},{"t":"set","p":"payload.name","pt":"msg","to":"topic ? topic : \"default-schedule\"","tot":"jsonata"},{"t":"set","p":"payload.expression","pt":"msg","to":"time","tot":"msg"},{"t":"set","p":"payload.payload","pt":"msg","to":"sendValue","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":500,"wires":[["64f9eca8e60b9404"]]},{"id":"892df1172a6e1d2b","type":"global-config","env":[],"modules":{"node-red-contrib-cron-plus":"2.1.0"}}]
1 Like

Analyzing your flow, this is no more a state machine but a parallel msg construction build around existing state machine. Anyway, it might be worth to take a closer look to the cron job as this might allow a better timer solution than existing auxilliary timer loop. Of coarse the number of msg counts in the FSM loop is much higher but this is the state machines view and not the NodeRed message view.

Almost everything in NodeRed is a hidden state machine what might contain one or more hidden or visible timers. A NodeRed timer node is not only one timer. The number of simultaneously runing times inside this timer node changes dynamically and depends from the timing of the incoming messages: If five messages arrive while the first is still delayed, the other four messages open another timer what allows them being delayed with the same time. For this reason, NodeRed needs to have a sophisticated runtime system what exactly works like a FSM and its associated timers.

For the case of FSM itself, this view is completely different. This is a trial to explain, why the FSM approach is not a waste of computing time. FSM deals with any number of inputs what might change at any time. Assume most simple input signal (e.g. a switch) is a one bit boolen information. According to its electrical line, the value true or false is always available to the software.

In comparision to this, there is the application. For applications, any specific input signal is not always important. Most time, only in a specific situation. Examine the traffic light pedestrian request button. There is no need to process this signal while the pedestrians have green anyway and even if its still not green but the first push is already confirmed by the pending indicator inside the pushbutton. There is also no need for the pedestrians to push the button while they have green or after they pushed once at red. Idiots to this regardless anyway and our control application has to deal with the situation.

Bigger systems (PLC or other realtime) might have thousands of binary signal inputs. Depending from the process to control, this input signals might change anytime with high frequency. Compare the input Signal of NodeReds inject node button. Its a hidden state machine using the states „pressed“ and „released“. If the state changes, a message will be issued therefore. Obviously every FSM has typical states where a specific input signal is out of interest. For this time, there is no need to receive a message for changing inputs and no need to generate this message. No need to scan the input port at all. With other words: A high number of flickering inputs would generate extreme amount of messages what are completely useless. This many useless messages exhaust the available computing time of any CPU like my single FSM loop did before. For this reason NodeRed is not a realtime system.

On the other hand it seems possible to use (or abuse) NodeRed as a relatime system with considerable speed results. Although its high speed, it is still not realtime as it is not deterministic while consumed computing time depends from signal input frequency and not from any (deterministic) FSM loop frequency.

My auxilliary timer loop for the FSM dismantles the sophisticated runtime hidden inside the NodeRed timer node. The result is a simple single timer using a context variable. From FSM view, any timer is a input signal exactly same than all other binary input signals. Although there were solutions using NodeReds join node to inject other input signals to the FSM loop, I going to stop this approach. Effectively, NodeReds hidden state machine needs to be dismantled first to generate new messages at a later time when they are required by the FSM and not only when signal changes. In case of FSM I am going to use a simple static flow-scoped context variable instead of a NodeRed message. Being aware of this explanations, its not a waste of computing time if the flow designers know what they do.

Attached is my latest traffic light FSM version. Cars have priority what makes a long pedestrian red time of 9 seconds. If a pedestrian appears in that time, he might press his request button to stop the cars prematurely. To give some immendiate feedback, the pushbutton is a illuminated model what indicates pending requests to its user.

The request button is a digital input. A preprocessing flow copies its signal to a flow context input mirrow. The FSM shows on how to poll any input signal equal to the timer signal.

[{"id":"8e2b8fd0f926c128","type":"tab","label":"Event based FSM","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":"aacb7c81f89be079","type":"tab","label":"Blink FSM","disabled":false,"info":"","env":[]},{"id":"9fe5fb9ec0c0b8a7","type":"group","z":"8e2b8fd0f926c128","name":"Cars traffic lights output viusalisation","style":{"label":true},"nodes":["7d74c23801848b0c","ed91409803b1598b","c6f26b4859613480","c80b8337ad33fa72"],"x":934,"y":439,"w":312,"h":262},{"id":"7a1dfe750951b882","type":"group","z":"8e2b8fd0f926c128","name":"Pedestrians traffic lights output visualisation","style":{"label":true},"nodes":["f29beac28fd80910","5e75e673a6b2c1b4","d913a633711cfeb3","094fc31f60cfbbb7"],"x":934,"y":139,"w":312,"h":282},{"id":"9d1731a61c2227ee","type":"group","z":"8e2b8fd0f926c128","name":"Same than timers, all HW input signals communicate with FSM using a flow scoped context variable","style":{"label":true},"nodes":["d589e831a5039c8d","96cd8e84e48efe34","486562621d485e60"],"x":94,"y":579,"w":692,"h":82},{"id":"c447cdfa25107a3d","type":"group","z":"8e2b8fd0f926c128","name":"Auxilliary timer loop to escape NodeReds synchronous msg flow design. Normally one timer per FSM is sufficient","style":{"label":true},"nodes":["bde8bee28d5bb264","85f024e2a41bebd6","4857cb7c9b528ce5","8a9bbbc89d6f0d5d","4bc2d01449a13ffa","320f9f9bbd242a6b"],"x":94,"y":739,"w":1072,"h":82},{"id":"106b9e284a8b6d25","type":"junction","z":"aacb7c81f89be079","x":640,"y":180,"wires":[["b84fef40d0752775"]]},{"id":"7d74c23801848b0c","type":"function","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"Cars RED light","func":"if ((msg.payload == 0) || (msg.payload == 1))\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":1040,"y":540,"wires":[[]]},{"id":"ed91409803b1598b","type":"function","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"Cars YELLOW light","func":"if ((msg.payload == 1) || (msg.payload == 3))\n   {msg.payload = 1;\n   node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});}\nreturn;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1050,"y":600,"wires":[[]]},{"id":"c6f26b4859613480","type":"function","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"Cars GREEN light","func":"if (msg.payload == 2)\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":1050,"y":660,"wires":[[]]},{"id":"c18b4c9bd8d583b2","type":"function","z":"8e2b8fd0f926c128","name":"Traffic Lights State Machine","func":"// FSM implemented by switch / case \n\nvar pRed;\nvar pGrn;\nvar pWit;\n\nswitch (msg.payload)\n{\n  case 0:                               // state red\n   if (flow.get(\"Timer\")== 0)           // wait for red time elapsed    \n      { pRed = {payload:\"on\"};          // switch pedestrian red off\n        pGrn = {payload:\"off\"};         // switch Pedestrians Green on\n        flow.set(\"Timer\", 1000);        // start red_yellow time\n        msg.payload = 1;                // next state red_yellow \n      }\n    break;\n            \n  case 1:                               // state  red_yellow\n    if (flow.get(\"Timer\")==0)           // wait for red_yellow time elapsed \n    {flow.set(\"Timer\",9000)             // start green time\n     msg.payload = 2;                   // next state green\n    }\n    break;\n            \n  case 2:                               // state green\n    if (flow.get(\"Button\") == 0)        // wait for pedestrians request button\n    {if (flow.get(\"Timer\")==0)          // wait for green time elapsed \n      {flow.set(\"Timer\", 1000);         // start yellow time\n       msg.payload = 3;                 // next state yellow\n      }\n    } \n    else\n    { pWit = {payload:\"on\"};            // confirm pedestrians request\n      flow.set(\"Timer\", 1000);          // start yellow time\n      msg.payload = 3;                  // next state yellow\n    }\n    break;\n    \n  case 3:                               // state yellow\n    if (flow.get(\"Timer\")==0)           // wait for yellow time elapsed \n    { pRed = {payload:\"off\"};           // switch Pedestrians Green off\n      pGrn = {payload:\"on\"};            // switch Pedestrians Green on\n      pWit = {payload:\"off\"};           // no more pedestrians request\n      flow.set(\"Timer\",4000);           // start red time\n      msg.payload = 0;                  // next state red\n    }\n    break;\n\n  case 4: {}                             // breakpoint state, does nothing\n    break;\n}\n\nnode.status({text:\"State = \" + msg.payload});   // display state information\nreturn [pRed,pGrn,pWit,msg];\n","outputs":4,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":380,"wires":[["f29beac28fd80910"],["5e75e673a6b2c1b4"],["094fc31f60cfbbb7"],["7d74c23801848b0c","ed91409803b1598b","c6f26b4859613480","546deffe9af46b93"]]},{"id":"e8c94c4ffb118d09","type":"inject","z":"8e2b8fd0f926c128","name":"Pon-Reset","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":190,"y":320,"wires":[["0a058b7e346b5361"]]},{"id":"0a058b7e346b5361","type":"function","z":"8e2b8fd0f926c128","name":"FSM Init","func":"var pRed;\n\npRed = {payload:\"on\"};\nmsg.payload = 0;\nreturn [pRed,msg]\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":320,"wires":[["f29beac28fd80910"],["c18b4c9bd8d583b2"]],"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":"f29beac28fd80910","type":"function","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","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":1050,"y":240,"wires":[[]]},{"id":"5e75e673a6b2c1b4","type":"function","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","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":1060,"y":300,"wires":[[]]},{"id":"d913a633711cfeb3","type":"comment","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","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":1090,"y":180,"wires":[]},{"id":"c80b8337ad33fa72","type":"comment","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"This is a Moore 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 prefere the Mealy style for FSM designs.\n","x":1090,"y":480,"wires":[]},{"id":"bde8bee28d5bb264","type":"delay","z":"8e2b8fd0f926c128","g":"c447cdfa25107a3d","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":510,"y":780,"wires":[["8a9bbbc89d6f0d5d"]]},{"id":"85f024e2a41bebd6","type":"inject","z":"8e2b8fd0f926c128","g":"c447cdfa25107a3d","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":190,"y":780,"wires":[["320f9f9bbd242a6b"]]},{"id":"4857cb7c9b528ce5","type":"function","z":"8e2b8fd0f926c128","g":"c447cdfa25107a3d","name":"TestTimer","func":"if (flow.get(\"Timer\") == 0) return [{delay:\"stopped\"}, null] // wait for FSM\nelse return [null, {delay:flow.get(\"Timer\")}];               // start new time\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":780,"wires":[["4bc2d01449a13ffa"],["bde8bee28d5bb264"]]},{"id":"8a9bbbc89d6f0d5d","type":"function","z":"8e2b8fd0f926c128","g":"c447cdfa25107a3d","name":"TimeElapsed","func":"flow.set(\"Timer\", 0);     // time elapsed, handshake for FSM \nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":780,"wires":[["4857cb7c9b528ce5"]]},{"id":"4bc2d01449a13ffa","type":"delay","z":"8e2b8fd0f926c128","g":"c447cdfa25107a3d","name":"Timer Resolution","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1050,"y":780,"wires":[["4857cb7c9b528ce5"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the timer loop, the maximum jitter will be 100mS.  "},{"id":"320f9f9bbd242a6b","type":"change","z":"8e2b8fd0f926c128","g":"c447cdfa25107a3d","name":"Timer Init","rules":[{"t":"set","p":"delay","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":780,"wires":[["bde8bee28d5bb264"]]},{"id":"546deffe9af46b93","type":"delay","z":"8e2b8fd0f926c128","name":"FSM cycle time","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":480,"y":400,"wires":[["c18b4c9bd8d583b2"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the FSM loop, timer resolution will be 100mS.\n"},{"id":"d589e831a5039c8d","type":"inject","z":"8e2b8fd0f926c128","g":"9d1731a61c2227ee","name":"Pedestrians Request Button","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"global","x":260,"y":620,"wires":[["96cd8e84e48efe34"]]},{"id":"96cd8e84e48efe34","type":"trigger","z":"8e2b8fd0f926c128","g":"9d1731a61c2227ee","name":"","op1":"","op2":"","op1type":"num","op2type":"num","duration":"200","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":500,"y":620,"wires":[["486562621d485e60"]]},{"id":"486562621d485e60","type":"function","z":"8e2b8fd0f926c128","g":"9d1731a61c2227ee","name":"Input Mirror","func":"if (msg.payload == 1) flow.set(\"Button\", 1); \nif (msg.payload == 0) flow.set(\"Button\", 0);\nreturn null;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":620,"wires":[[]]},{"id":"094fc31f60cfbbb7","type":"function","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","name":"Request Pending","func":"if (msg.payload ===\"on\")\n   {msg.payload = 1;\n   node.status({fill:\"white\",shape:\"dot\",text:\"Button iluminated\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1050,"y":380,"wires":[[]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar pYel;                                   // parameter port for yellow output\n\nswitch (msg.payload)                        // pop state info as parameter from stack\n{ case 0:                                   // state OFF\n    if (flow.get(\"GlobalTimer\") == 0)       // wait for OFF time elapsed\n    { pYel = {payload:\"on\"};                // switch output for yellow light on\n      flow.set(\"GlobalTimer\", 400);         // start on time\n      msg.payload = 1;                      // next state ON\n    }  \n    break;\n\n  case 1:                                   // state ON\n    if (flow.get(\"GlobalTimer\") == 0)       // wait for ON time elapsed\n    { pYel = {payload:\"off\"};               // switch output for yellow light off\n      flow.set(\"GlobalTimer\" , 600)         // start off time\n      msg.payload = 0;                      // next state OFF\n    }\n    break;\n}\n\nnode.status({text:\"State = \" + msg.payload});     // display state information\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":140,"wires":[["8b99ba6916511706"],["106b9e284a8b6d25"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":110,"y":140,"wires":[["7dc2e27aea626843"]]},{"id":"8b99ba6916511706","type":"function","z":"aacb7c81f89be079","name":"YellowBlinker","func":"if (msg.payload ===\"on\") node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\nelse node.status({});\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":140,"wires":[[]]},{"id":"b84fef40d0752775","type":"delay","z":"aacb7c81f89be079","name":"Speed Limit","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":270,"y":180,"wires":[["7334cf2d08ec61be"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the FSM loop, timer resolution will be 100mS.\n"},{"id":"7dc2e27aea626843","type":"change","z":"aacb7c81f89be079","name":"FSM Init","rules":[{"t":"set","p":"payload","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":140,"wires":[["7334cf2d08ec61be"]]},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","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":430,"y":300,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":110,"y":300,"wires":[["a8e06c1a18428622"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","name":"TestTimer","func":"if (flow.get(\"GlobalTimer\") == 0) return [{delay:\"stopped\"}, null] // wait for FSM\nelse return [null, {delay:flow.get(\"GlobalTimer\")}];               // start new time\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":300,"wires":[["d4379218167a7106"],["11f4989c0dba9de8"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","name":"TimeElapsed","func":"flow.set(\"GlobalTimer\", 0);     // time elapsed, handshake for FSM \nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":300,"wires":[["bf02810cef9dfd9b"]]},{"id":"d4379218167a7106","type":"delay","z":"aacb7c81f89be079","name":"Speed Limit","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":950,"y":300,"wires":[["bf02810cef9dfd9b"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the timer loop, the maximum jitter will be 100mS.  "},{"id":"a8e06c1a18428622","type":"change","z":"aacb7c81f89be079","name":"Timer Init","rules":[{"t":"set","p":"delay","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":240,"y":300,"wires":[["11f4989c0dba9de8"]]}]

Unfortunately, the timer loop has a major design flaw. While any time is running, the blocking NodeRed Timer node does not allow to start another time before the previous elapsed. As a consequence, pedestrians might request its green priority but waiting time is not really shortend. (This is what pedestrians always notice while waiting) . Therefore the auxilliary timer loop needs improvement. Obviously its possible and should be prefered to derive time from system UTC by using something like a cron job. Mainly, the FSM code already shows the equal processing of times and input signals what was not possible in other FSM examples of this thread.

Here is my final traffic light FSM.

Trying to improve the auxilliary timer loop, I failed with the graphical nodes becouse of racing conditions. To find out, if a time changes before previous time elapsed, I compared times of previous and new cycle. Indeed, the FSM starts the new time always faster after being elapsed, than a supervisor loop might detect the handshake intermediate state really reliable. Race and blocking conditions are typical problems if working with Petri nets instead FSM.

For this reason, the graphical time system was completely useless and is now replaced by only two Javascript lines what derive the UTC system time. Unfortunately getTime() is a method and not a function why I have to create a date object using the new constructor before.

let systic = new Date();        // create date object to generate system time
systic = systic.getTime();     // UTC to milliseconds for system clock 

My NodeRed already uses the 64 bit UTC, why the traffic light will also work reliable beyond the year 2038.

Status line of FSM node now also shows the timer beside the state information. For the traffic lights, the timer is always in use and shows the FSM timers decrementing in "realtime" flavour. For other FSM, using states what switch to input conditions only, the time might be indicated as elapsed (=negative sign).

The FSM node output ports do not depend any more from number of FSM physical signal outputs. The FSM is now reduced to only one output port regardless of the number of in and output ports and even timers. The loop cycling msg contain the topics for state, time and outputs. No more global context variables required for this. For the previous explained reason of efficiency, the input mirror is stored in global variables. So far, the FSM example is now simple and is doing all important things. It should be easy to understand and can be used as a template for other FSM related projects.

[{"id":"8e2b8fd0f926c128","type":"tab","label":"Event based FSM","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":"9fe5fb9ec0c0b8a7","type":"group","z":"8e2b8fd0f926c128","name":"Cars traffic lights output viusalisation","style":{"label":true},"nodes":["7d74c23801848b0c","ed91409803b1598b","c6f26b4859613480","c80b8337ad33fa72"],"x":714,"y":319,"w":312,"h":262},{"id":"7a1dfe750951b882","type":"group","z":"8e2b8fd0f926c128","name":"Pedestrians traffic lights output visualisation","style":{"label":true},"nodes":["f29beac28fd80910","5e75e673a6b2c1b4","d913a633711cfeb3","094fc31f60cfbbb7"],"x":714,"y":19,"w":312,"h":282},{"id":"7d74c23801848b0c","type":"function","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"Cars RED light","func":"if ((msg.state == 0) || (msg.state == 1))\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":820,"y":420,"wires":[[]]},{"id":"ed91409803b1598b","type":"function","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"Cars YELLOW light","func":"if ((msg.state == 1) || (msg.state == 3))\n   {msg.payload = 1;\n   node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});}\nreturn;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":480,"wires":[[]]},{"id":"c6f26b4859613480","type":"function","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"Cars GREEN light","func":"if (msg.state == 2)\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":830,"y":540,"wires":[[]]},{"id":"c18b4c9bd8d583b2","type":"function","z":"8e2b8fd0f926c128","name":"Traffic Lights State Machine","func":"// FSM implemented by switch / case using UTC system time for timers\n\nlet systic = new Date();        // create date object to generate system time\nsystic = systic.getTime();      // UTC to milliseconds for system clock \n\nswitch (msg.state)\n{\n  case 0:                               // state red\n   if (msg.timer - systic < 0)          // wait for OFF time elapsed\n      {\n        msg.pred = 1;                   // switch pedestrians red on\n        msg.pgrn = 0;                   // switch pedestirans green off\n        msg.timer = systic+1000;        // start red_yellow time\n        msg.state = 1;                  // next state red_yellow \n      }\n    break;\n\n  case 1:                               // state  red_yellow\n    if (msg.timer - systic < 0)         // wait for red_yellow time elapsed\n        {   \n            msg.timer = systic+6000;    // start green time\n            msg.state = 2;              // next state green\n        }\n    break;\n            \n  case 2:                               // state green\n    if (flow.get(\"Button\") == 0)        // wait for pedestrians request button\n    {\n        if (msg.timer - systic < 0)     // wait for greem time elapsed\n        {\n            msg.timer = systic+1000;    // start yellow time\n            msg.state = 3;              // next state yellow\n        }\n    } \n    else\n    { \n        msg.pwit = 1;                   // switch button illumination on\n        msg.timer = systic+1000;        // start yellow time\n        msg.state = 3;                  // next state yellow\n    }\n    break;\n    \n  case 3:                               // state yellow\n    if (msg.timer - systic < 0)         // wait for OFF time elapsed\n    {\n        msg.pred = 0;                   // switch pedestrinas red off\n        msg.pgrn = 1;                   // switch pedestrinas green on\n        msg.pwit = 0;                   // no more pedestrian requests pending\n        msg.timer = systic+4000;        // start red time\n        msg.state = 0;                  // next state red\n    }\n    break;\n}           \n\n// finally always display the most important FSM recourses  state + time\n\nnode.status({text:\"State = \" + msg.state + \",\\\nTimer = \" + (msg.timer-systic)});               \n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":260,"wires":[["f29beac28fd80910","5e75e673a6b2c1b4","094fc31f60cfbbb7","546deffe9af46b93","7d74c23801848b0c","ed91409803b1598b","c6f26b4859613480"]]},{"id":"e8c94c4ffb118d09","type":"inject","z":"8e2b8fd0f926c128","name":"Startup","props":[{"p":"state","v":"0","vt":"num"},{"p":"timer","v":"0","vt":"num"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":280,"y":200,"wires":[["c18b4c9bd8d583b2"]]},{"id":"f29beac28fd80910","type":"function","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","name":"Pedestrians RED","func":"if (msg.pred == 1)\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":830,"y":120,"wires":[[]]},{"id":"5e75e673a6b2c1b4","type":"function","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","name":"Pedestrians GREEN","func":"if (msg.pgrn == 1)\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":840,"y":180,"wires":[[]]},{"id":"d913a633711cfeb3","type":"comment","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","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":870,"y":60,"wires":[]},{"id":"c80b8337ad33fa72","type":"comment","z":"8e2b8fd0f926c128","g":"9fe5fb9ec0c0b8a7","name":"This is a Moore 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 prefere the Mealy style for FSM designs.\n","x":870,"y":360,"wires":[]},{"id":"546deffe9af46b93","type":"delay","z":"8e2b8fd0f926c128","name":"FSM cycle time","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":280,"y":260,"wires":[["c18b4c9bd8d583b2"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the FSM loop, timer resolution will be 100mS.\n"},{"id":"d589e831a5039c8d","type":"inject","z":"8e2b8fd0f926c128","name":"Pedestrians Request Button","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":160,"y":380,"wires":[["96cd8e84e48efe34"]]},{"id":"96cd8e84e48efe34","type":"trigger","z":"8e2b8fd0f926c128","name":"","op1":"","op2":"","op1type":"num","op2type":"num","duration":"200","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":400,"y":380,"wires":[["486562621d485e60"]]},{"id":"486562621d485e60","type":"function","z":"8e2b8fd0f926c128","name":"Input Mirror","func":"if (msg.payload == 1) flow.set(\"Button\", 1); \nif (msg.payload == 0) flow.set(\"Button\", 0);\nreturn null;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":380,"wires":[[]]},{"id":"094fc31f60cfbbb7","type":"function","z":"8e2b8fd0f926c128","g":"7a1dfe750951b882","name":"Request Pending","func":"if (msg.pwit == 1)\n   {msg.payload = 1;\n   node.status({fill:\"white\",shape:\"dot\",text:\"Button iluminated\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":260,"wires":[[]]}]

You have obviously put a lot of time and effort into building the FSM. I'll take a detailed look at the code when I have some spare time away from supporting my IoT students.

Couple of observations you might like to consider...

:sponge: Clean-up: Use let systic = Date.now();

You can simplify your script to:
let systic = Date.now();
instead of:

let systic = new Date();
systic = systic.getTime();

:light_bulb: Readability tip: Add a default case in the switch

This helps catch unexpected values for msg.state:

default:
    node.warn("Unknown state: " + msg.state);
    break
2 Likes

Many thanks, the Date.now() replacement is what I was already searching for and it works perfect. Up on today, I am not that enthusiastic about the object oriented programming. For that reason it probably took me a while to understand the JSON. Nevertheless, NodeRed is my favourite new toy for the moment. In the meantime, I already installed the dashboard nodes to explore NodeReds true benefits against plain code.

Beside Date.now() I did more tidy up with comments, formatting and removing unused variables. Playing around with the traffic light, I saw several times the situation that the pedestrians push button issued a steady on signal. Obviously its a initialisation problem what I cannot reproduce. Hopefully it has nothing to do with the FSM code itself.

[{"id":"b9f4f348fc3c2293","type":"tab","label":"Event based FSM","disabled":false,"info":"This is a traffic light 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 Mermaid composite states, nor the Mermaid possible fork and join instructions. \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":"2ac7a811e96626ab","type":"group","z":"b9f4f348fc3c2293","name":"Cars traffic lights output visualisation","style":{"label":true},"nodes":["5a7a9324ad66d778","87e25e6fcc5608bf","c83cd99fe5bef2d4","48260e70f213bd93"],"x":714,"y":319,"w":312,"h":262},{"id":"50e103befc11b594","type":"group","z":"b9f4f348fc3c2293","name":"Pedestrians traffic lights output visualisation","style":{"label":true},"nodes":["2bc45a5ab869e43b","31dae6f105ce351b","a95fcd60cf3c0d96","df684ecaf483b6fd"],"x":714,"y":19,"w":312,"h":282},{"id":"5a7a9324ad66d778","type":"function","z":"b9f4f348fc3c2293","g":"2ac7a811e96626ab","name":"Cars RED light","func":"if ((msg.state == 0) || (msg.state == 1))\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":820,"y":420,"wires":[[]]},{"id":"87e25e6fcc5608bf","type":"function","z":"b9f4f348fc3c2293","g":"2ac7a811e96626ab","name":"Cars YELLOW light","func":"if ((msg.state == 1) || (msg.state == 3))\n   {msg.payload = 1;\n   node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});}\nreturn;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":480,"wires":[[]]},{"id":"c83cd99fe5bef2d4","type":"function","z":"b9f4f348fc3c2293","g":"2ac7a811e96626ab","name":"Cars GREEN light","func":"if (msg.state == 2)\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":830,"y":540,"wires":[[]]},{"id":"67417c1343c39888","type":"function","z":"b9f4f348fc3c2293","name":"Traffic Light State Machine","func":"// FSM implemented by switch / case using UTC system time for timers\n\nlet systic = Date.now();                // UTC in milliseconds for system clock\n\nswitch (msg.state)\n{\n  case 0:                               // state red\n   if (msg.timer - systic < 0)          // wait for OFF time elapsed\n      {\n        msg.pred = 1;                   // switch pedestrians red on\n        msg.pgrn = 0;                   // switch pedestirans green off\n        msg.timer = systic+1000;        // start red_yellow time\n        msg.state = 1;                  // next state red_yellow \n      }\n    break;\n\n  case 1:                               // state  red_yellow\n    if (msg.timer - systic < 0)         // wait for red_yellow time elapsed\n        {   \n            msg.timer = systic+6000;    // start green time\n            msg.state = 2;              // next state green\n        }\n    break;\n \n  case 2:                               // state green\n    if (flow.get(\"Button\") == 0)        // wait for pedestrians request button\n    {\n        if (msg.timer - systic < 0)     // wait for greem time elapsed\n        {\n            msg.timer = systic+1000;    // start yellow time\n            msg.state = 3;              // next state yellow\n        }\n    } \n    else\n    { \n        msg.pwit = 1;                   // switch button illumination on\n        msg.timer = systic+1000;        // start yellow time\n        msg.state = 3;                  // next state yellow\n    }\n    break;\n    \n  case 3:                               // state yellow\n    if (msg.timer - systic < 0)         // wait for yellow time elapsed\n    {\n        msg.pred = 0;                   // switch pedestrians red off\n        msg.pgrn = 1;                   // switch pedestrians green on\n        msg.pwit = 0;                   // no more pedestrian requests pending\n        msg.timer = systic+4000;        // start red time\n        msg.state = 0;                  // next state red\n    }\n    break;\n}           \n\n// finally always display the most important FSM resources  state + time\n\nnode.status({text:\"State = \" + msg.state + \",\\\nTimer = \" + (msg.timer-systic)});               \n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":260,"wires":[["2bc45a5ab869e43b","31dae6f105ce351b","df684ecaf483b6fd","ef37107f8a62ff9d","5a7a9324ad66d778","87e25e6fcc5608bf","c83cd99fe5bef2d4"]]},{"id":"122114297a8a195c","type":"inject","z":"b9f4f348fc3c2293","name":"Startup","props":[{"p":"state","v":"0","vt":"num"},{"p":"timer","v":"0","vt":"num"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":280,"y":200,"wires":[["67417c1343c39888"]]},{"id":"2bc45a5ab869e43b","type":"function","z":"b9f4f348fc3c2293","g":"50e103befc11b594","name":"Pedestrians RED","func":"if (msg.pred == 1)\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":830,"y":120,"wires":[[]]},{"id":"31dae6f105ce351b","type":"function","z":"b9f4f348fc3c2293","g":"50e103befc11b594","name":"Pedestrians GREEN","func":"if (msg.pgrn == 1)\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":840,"y":180,"wires":[[]]},{"id":"a95fcd60cf3c0d96","type":"comment","z":"b9f4f348fc3c2293","g":"50e103befc11b594","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":870,"y":60,"wires":[]},{"id":"48260e70f213bd93","type":"comment","z":"b9f4f348fc3c2293","g":"2ac7a811e96626ab","name":"This is a Moore 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 prefere the Mealy style for FSM designs.\n","x":870,"y":360,"wires":[]},{"id":"ef37107f8a62ff9d","type":"delay","z":"b9f4f348fc3c2293","name":"FSM Cycle Time","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":280,"y":260,"wires":[["67417c1343c39888"]],"info":"Node Reds runtime will be blocked if using 100% of the available computing time for polling in the loops. Therefore we slowdown the loops to allow cooperative thread switching.\n\nFor 100mS delay in the FSM loop, timer resolution will be 100mS.\n"},{"id":"4e4c5f823ce7b63e","type":"inject","z":"b9f4f348fc3c2293","name":"Pedestrians Request Button","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":160,"y":380,"wires":[["f954d3b8c7ac92f6"]]},{"id":"f954d3b8c7ac92f6","type":"trigger","z":"b9f4f348fc3c2293","name":"","op1":"","op2":"","op1type":"num","op2type":"num","duration":"200","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":400,"y":380,"wires":[["7bf88bc34b7b2c53"]]},{"id":"7bf88bc34b7b2c53","type":"function","z":"b9f4f348fc3c2293","name":"Input Mirror","func":"if (msg.payload == 1) flow.set(\"Button\", 1); \nif (msg.payload == 0) flow.set(\"Button\", 0);\nreturn null;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":380,"wires":[[]]},{"id":"df684ecaf483b6fd","type":"function","z":"b9f4f348fc3c2293","g":"50e103befc11b594","name":"Request Pending","func":"if (msg.pwit == 1)\n   {msg.payload = 1;\n   node.status({fill:\"white\",shape:\"dot\",text:\"Button iluminated\"});\n   }\nelse\n   {msg.payload = 0;\n   node.status({});\n   }\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":260,"wires":[[]]}]