Finite State Machine (FSM) with delay

I was asked the other day about how to create a Finite State Machine (FSM) with different delays between various transitions. Here is a simple example, based on the UK traffic light sequence, that makes use of the msg.delay option in the Delay node.

Here's a screenshot of the flow.

[{"id":"be3ab1ef.3f0a68","type":"tab","label":"Traffic Lights with variable delay","disabled":false,"info":"","env":[]},{"id":"743e732.c12228c","type":"function","z":"be3ab1ef.3f0a68","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1010,"y":120,"wires":[[]]},{"id":"778d9f53.8e5f7","type":"function","z":"be3ab1ef.3f0a68","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,"noerr":0,"x":1020,"y":180,"wires":[[]]},{"id":"c8a94827.acb49","type":"function","z":"be3ab1ef.3f0a68","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":1020,"y":240,"wires":[[]]},{"id":"28cc6bcb.920094","type":"function","z":"be3ab1ef.3f0a68","name":"Finite State Machine","func":"// Change the values of delay to suit your application\n\n\n// Here is the classical way of coding a state machine.\n// It uses a 'case construct' to check the current state and then set the next state.\n\nvar fsm_state = flow.get(\"state_counter\") || 0;\n\nvar defaultDelay = flow.get(\"defaultDelay\");\n\nswitch (fsm_state)\n    {\n        case 0:\n            fsm_state = 1; // Next state\n            node.send( {payload:fsm_state, delay:5000});  // delay is in milliseconds\n            break;\n            \n        case 1:\n            fsm_state = 2;\n            node.send( {payload:fsm_state, delay:defaultDelay});\n            break;\n        \n        case 2:\n            fsm_state = 3;\n            node.send( {payload:fsm_state, delay:defaultDelay});\n            break;\n            \n        case 3:\n            fsm_state = 0;\n            node.send( {payload:fsm_state, delay:defaultDelay});\n            break;\n    }\n    \nflow.set(\"state_counter\", fsm_state);\n\nnode.status({text:\"State counter = \" + fsm_state});\n\nreturn null;","outputs":"1","timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":180,"wires":[["743e732.c12228c","778d9f53.8e5f7","c8a94827.acb49","82ab245c.6aa488"]]},{"id":"77e8a5e.82598dc","type":"comment","z":"be3ab1ef.3f0a68","name":"Place specific delays in here","info":"","x":720,"y":140,"wires":[]},{"id":"634074c5.d267d4","type":"inject","z":"be3ab1ef.3f0a68","name":"Toggle button","props":[{"p":"payload"},{"p":"delay","v":"1000","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":170,"y":180,"wires":[["ee02a850.8eee8","82ab245c.6aa488"]]},{"id":"ee02a850.8eee8","type":"function","z":"be3ab1ef.3f0a68","name":"Status indicator","func":"var status = flow.get(\"status\") || \"stopped\";\n\nif (status == \"stopped\") {\n    flow.set(\"status\", \"running\");\n    node.status({text:\"State = running\"});\n}\nelse {\n    flow.set(\"status\", \"stopped\");\n     node.status({text:\"State = STOPPED\"});\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":300,"wires":[[]]},{"id":"3361daf1.f94576","type":"function","z":"be3ab1ef.3f0a68","name":"Indicator and Gate","func":"var status = flow.get(\"status\") || \"stopped\";\nif (status == \"running\") {\n     node.status({text:\"State = running\"});\n    return msg;\n}\nelse {\n     node.status({text:\"State = STOPPED\"});\n     return null;\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":180,"wires":[["28cc6bcb.920094"]]},{"id":"82ab245c.6aa488","type":"delay","z":"be3ab1ef.3f0a68","name":"","pauseType":"delayv","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":360,"y":180,"wires":[["3361daf1.f94576"]]},{"id":"98ee2ab4.5b3e38","type":"comment","z":"be3ab1ef.3f0a68","name":"Override delay with msg.delay","info":"","x":420,"y":240,"wires":[]},{"id":"203dae68.e2be8a","type":"inject","z":"be3ab1ef.3f0a68","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":150,"y":100,"wires":[["fd179166.5703b"]]},{"id":"fd179166.5703b","type":"function","z":"be3ab1ef.3f0a68","name":"Set default delay to 1 second","func":"flow.set(\"defaultDelay\", 1000);\n\nreturn null;","outputs":0,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":100,"wires":[]},{"id":"a373a8b72097daad","type":"comment","z":"be3ab1ef.3f0a68","name":"Connect to MQTT-Out node or GPIO pin","info":"","x":1300,"y":120,"wires":[]},{"id":"5732ad4983cbe4dd","type":"comment","z":"be3ab1ef.3f0a68","name":"Start/Stop button","info":"","x":160,"y":220,"wires":[]},{"id":"8d023d6015ef7f2d","type":"comment","z":"be3ab1ef.3f0a68","name":"Connect to MQTT-Out node or GPIO pin","info":"","x":1300,"y":180,"wires":[]},{"id":"cc463dec8a1978aa","type":"comment","z":"be3ab1ef.3f0a68","name":"Connect to MQTT-Out node or GPIO pin","info":"","x":1300,"y":240,"wires":[]}]

The 'case construct' is the place where you can define the delay for each transition.
In this example, I have set the delay from state-0 to state-1 as 5 secs, while the delay from state-1 to state-2 is using the defaultDelay (set in the start-up to 1 sec).
sunday_FSM_C

This is the setting needed in the Delay node to override the normal delay.
sunday_FSM_B

I hope someone finds this useful?

4 Likes

Hi Dave,
For me it is useful, thank you. I will study it.
BR,
Marco

1 Like

If you have any questions about the FSM flow, please feel free to ask.

fine
I'm going to try cooking in the oven when the temperature needs to drop gradually, e.g. 120-110-100-90-80-70-60 C at certain time intervals and then skip the delay if necessary
Thanks!

The traffic light example above is a bit of an overkill for a FSM as it steps through each state in a regular sequence (e.g. state 0, then state 1, state 2, state 3 and back to state 0) and could be implemented with a simple counter.

State Machines normally go from one state to another depending on the current state and the value of external inputs. Here's a made-up example of adding some conditions to a transition.

case 0:
        if (mode == "manual")
        {
            fsm_state = 1;
            node.send( {payload: fsm_state, delay:1000} );
        }
        else
        {
            if (timer == "on")
            {
               fsm_state = 3;
               node.send( {payload: fsm_state, delay:2000});   
            }
            else 
            {
              // Do nothing, remain in this state 
            }
        }
        break;

Yes just like you do last example,I think make some extra conditional checkings and goto then needed step.

Over the weekend, someone asked me for further details/examples of FSM.

I've found a couple of old threads I posted on the forum. Here are the links...

1 Like