Finally I have a timer system for FSM in the style I wanted. To escape from NodeReds synchronous timer system, attached example shows two flows in one tab. The upper flow contains a simple blinker FSM with only two states: On and Off. The state indicator toggles between 0 and 1, yellow blinker node should visualize a yellow blinking dot below.
The lower flow is a auxiliary timer loop used by the FSM. The Async FSM Timer Node always show a blue dot with 1 what indicates running time. This is, because the yellow blinker is always active. One state counts the off-time, the other state counts the on-time. Occasionally we see a small flicker on the blue dot. This happens sometimes for one cycle only, where time is elapsed and Node Red is doing the handshake over the flow-scoped BlinkTimer Variable before starting the timer to count the next time again.
Recent insights of this experiment
-
Node Reds execution Flow times shows reasonable fast cycles what are typically anything between 1mSec and 10mSec. Any worst cases for possible jitter not evaluated. Assume all, realtime performance is better than expected or required for all typical Node Red applications.
-
Inside a function node, any Javascript loop using do/while or non terminating for-loop will block anything inside the Node Red tab. Obviosly Node Red uses a cooperative multitask what will switch between each of the nodes. As a consequence, Node Red does not allow any graphical link to its own node. To escape from this limitation, the example shows inserted DummyFunctions inside both loops. The example contains two diffrent and asynchronous loops, both working contemporary.
-
There are probably many other solutions more simple with Node Red to make a blinking dot. This one follows the state machine design typically used for realtime applictions.
More details to explore
-
replace global state variable
Following the message passing architecture of Node Red, I like to replace the flow global state variable by a local variable passed from and to the FSM node as a parameter on stack. This should be feasable, as the timer is no more blocking the data flow and DummyFunct might pass out what is coming in without any changes.
-
replace global timer variable
Accidently I stumbled over the BigTimer - Scargill's Tech Blog project but got mad of all the possiblities without taking a look under its bonnet. There are too many possibilites not really mandatory for any problem, but nothing similar what I used in this example for the FSM times. To remove the global timer variables and even replace the timer flow of this example with a self written timer node could be feasable. From FSM view, this only requires to function calls.
One to start the timer: SetTimer (Timevariable);
One to check if time is elapsed: TestTimer (Timervariable);
A (Timervariable) parameter will be required for each contemporary running time. The timer loop used in this example is required for each active FSM. It could be eliminated by only one what derives the time from system time UTC time stamp. Examples and suggestions welcome.
[{"id":"aacb7c81f89be079","type":"tab","label":"Blink FSM for timer test","disabled":false,"info":"","env":[]},{"id":"2bf1285a50be6ae1","type":"group","z":"aacb7c81f89be079","name":"Auxilliary timer loop to escape from NodeReds synchronous timer desgin","style":{"label":true},"nodes":["11f4989c0dba9de8","1ff5cd5581ca6ad5","bf02810cef9dfd9b","7882b3c7fcbb8b55","407daca816bddd54","57c0bd75ad81b2ae","e1dd832363b3052d"],"x":214,"y":639,"w":992,"h":142},{"id":"24f2582fc405a8d1","type":"group","z":"aacb7c81f89be079","name":"Simple blink FSM with On and Off State","style":{"label":true},"nodes":["7334cf2d08ec61be","83b08ea7f44c0d4b","12d6c1b1507be065","8b99ba6916511706","b3c9ca3be64d4c23","fb360390aafa7be4"],"x":214,"y":439,"w":832,"h":162},{"id":"11f4989c0dba9de8","type":"delay","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"Async FSM Timer","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":650,"y":740,"wires":[["407daca816bddd54"]]},{"id":"1ff5cd5581ca6ad5","type":"inject","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"","props":[{"p":"delay","v":"0","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","x":310,"y":740,"wires":[["7882b3c7fcbb8b55"]]},{"id":"bf02810cef9dfd9b","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"TestTimer","func":"var LocalTimer = flow.get(\"GlobalTimer\"); // get actual timer value\nvar msg1;\nvar msg2;\n\nif (LocalTimer == 0)\n {\n msg2 = {payload:\"stop\"};\n return [null, msg2]\n }\n\nelse\n {\n msg1 = {delay:LocalTimer};\n return [msg1, null];\n }\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1120,"y":740,"wires":[["11f4989c0dba9de8"],["e1dd832363b3052d"]]},{"id":"7882b3c7fcbb8b55","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"TimerInit","func":"var LocalTimer = flow.get(\"GlobalTimer\"); // flow socpe, non volatile\nLocalTimer = 0; // first time value\nflow.set(\"GlobalTimer\", LocalTimer); // initialize global timer value\nmsg.delay = LocalTimer; // pass initial value to timer\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":740,"wires":[["11f4989c0dba9de8"]]},{"id":"407daca816bddd54","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"TimeElapsed","func":"var LocalTimer = flow.get(\"GlobalTimer\"); // get actual timer value\nLocalTimer = 0; // confirm elapsed time by handshake\nflow.set(\"GlobalTimer\", LocalTimer); // save confirmed data \nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":680,"wires":[["bf02810cef9dfd9b"]]},{"id":"57c0bd75ad81b2ae","type":"comment","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"1=Timer Running, 0=Timer Stopped","info":"","x":640,"y":680,"wires":[]},{"id":"e1dd832363b3052d","type":"function","z":"aacb7c81f89be079","g":"2bf1285a50be6ae1","name":"DummyFunct","func":"return msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":740,"wires":[["bf02810cef9dfd9b"]]},{"id":"7334cf2d08ec61be","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"Blink State Machine","func":"// Blinker FSM using asynchronous timer system\n\nvar LocalBTimer = flow.get(\"GlobalTimer\"); // get actual values\nvar LocalBState = flow.get(\"GlobalBState\");\nvar pYel;\n\nswitch (LocalBState)\n{\n case 0: // state OFF\n if (LocalBTimer == 0)\n {\n pYel = {payload:\"on\"};\n LocalBTimer=543;\n flow.set(\"GlobalTimer\" , LocalBTimer)\n LocalBState = 1; // next state ON\n } \n break;\n \n case 1: // state ON\n if (LocalBTimer == 0) \n {\n pYel = {payload:\"off\"};\n LocalBTimer=567;\n flow.set(\"GlobalTimer\" , LocalBTimer)\n LocalBState = 0; // next state OFF\n }\n break;\n}\n\nflow.set(\"GlobalBState\", LocalBState); // save state information\n // do not save Globaltimer, poll only\nnode.status({text:\"State = \" + LocalBState}); // display state information\nreturn [pYel,msg];\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":560,"wires":[["8b99ba6916511706"],["b3c9ca3be64d4c23"]]},{"id":"83b08ea7f44c0d4b","type":"inject","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"true","payloadType":"bool","x":310,"y":500,"wires":[["12d6c1b1507be065"]]},{"id":"12d6c1b1507be065","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"Init","func":"var LocalBState = flow.get(\"GlobalBState\");\nvar pRed;\n\nLocalBState = 1;\nflow.set(\"GlobalBState\", LocalBState);\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":500,"wires":[["7334cf2d08ec61be"]],"info":"Every FSM needs to be initialized at power up. This requires to set the state information what is depicted by a solid black start point in the Mermaid FSM diagram. \n\nFor the first state entry, Mealy Outputs needs to be initialized as well while Moore outputs rely on the state information iteself. Therefore this init funktion uses two outputs. One for the state machine what passes the initial state information, another for each output what passes the first event for the output signal.\n"},{"id":"8b99ba6916511706","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"YellowBlinker","func":"if (msg.payload ===\"on\")\n {msg.payload = 1;\n node.status({fill:\"yellow\",shape:\"dot\",text:\"Yellow ON\"});\n }\nelse\n {msg.payload = 0;\n node.status({});\n }\nreturn;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":950,"y":540,"wires":[[]]},{"id":"b3c9ca3be64d4c23","type":"function","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"DummyFunct","func":"return msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":560,"wires":[["7334cf2d08ec61be"]]},{"id":"fb360390aafa7be4","type":"comment","z":"aacb7c81f89be079","g":"24f2582fc405a8d1","name":"FSM use async timers, puls witdh 4:6","info":"","x":730,"y":480,"wires":[]}]