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?

5 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

Hi Dave,

Thanks for your great example.
I would like to use it in a similar way for myself.
Unfortunately, I'm not a programmer and I'm a bit at a loss at the moment.
I've been trying to implement a separate start and stop button, as well as a reset button, but somehow I can't find the solution I'm looking for.

Can you perhaps give me a little nudge in the right direction?

Best regards
Ben

There are various ways to "code" a FSM (Finite State Machine) so I will need a few more details from you in order to provide a meaningful response.

For example, will the FSM be driven by a regular "clock pulse" (i.e. an Inject node) or by external inputs or a mixture of both?

The FSM that drives my home lighting and security system is driven by an inject node that produces a pulse every 3-secs - whilst the FSM that runs the roller-door for my garage is interrupt driven.

Note:
I am assuming you want to build something a bit more complex than a "traffic light" indicator ?
If so, please can you outline what your "machine" should look like and do?

Thanks for the quick response :).

Let me give you a bit more context and describe my project.

At work, I’m currently building a small machine that measures the thickness of battery cells. The whole setup works pneumatically with a cylinder and a digital dial gauge.

Now, I want to integrate electrical measurement into this system, as well as reading the cell’s barcode.

Here’s how the process should look:

  1. Manually insert the cell into the machine.

  2. Press “Start” on the Node-RED dashboard.

  3. Read the barcode.

  4. Measure the thickness.

  5. Perform the electrical measurement.

  6. Conduct a visual inspection (manually), selecting the type of defect on the dashboard.

  7. Save the measured values in a database.

That’s the general workflow.

There will also be additional tasks, like initializing the electrical measurement device, initializing the barcode scanner, checking the position of the cylinder based on the dial gauge when starting Node-RED, handling errors, and comparing the measured values with tolerance specifications. I’m still unsure whether this comparison should be implemented as a separate logic or integrated into the workflow.

The process should also allow stopping and resetting in case of errors to restart from the beginning.

I actually came across your example some time ago because I wanted to implement a logic for turning on my TV/multimedia setup with it, but I haven’t gotten around to it yet :sweat_smile:.

This work project, however, is the perfect opportunity to dive deeper into this topic :grin:.

In principle, I want to recreate something similar to a PLC step sequence. I find this principle brilliant because each step is processed one after the other, ensuring a clean workflow for the machine. Plus, each step must fulfill its condition before the next one starts.

The process isn’t particularly time-critical, so we’re just using a RevolutionPi with Node-RED. It’s also great because you can easily integrate all the devices and build a dashboard for it. And it’s not too expensive :sweat_smile:.

Wooo - that's a bit of a leap from a simple traffic light FSM indicator.
I'll have to study that and get back to you.

Thank you very much for your help :blush:.

Let me know once you have sorted out your "top level" Node-RED flow and how a FSM might
fit-in to the overall sequence.