Wanted: Simple state machine examples

As a newbe I already mastered some simple flows for combinatorical problems.

My system is a energy storage system running Venus what comes with a preconfigured NodeRed. Although I am long term C and assembly programmer without any knowledge and experience using Javascript, I like to go on using Node Red for this (and other ) reasons.

One thing what I need to master for solving simple sequential problems is the programming of state machines. I already browsed through the examples and found several state machine library examples. To be honest: They are too complicated for me or for learning Javascript by using them. I do not need universal state machine to invoke as instances or use of the same state machine with IO signals and variables what come from parameters, hierarchical subflows, recursion. Everything should be simple, limited to very basic instructions without using libraries or depenencies.

What about a typical and simple state machine example with only 2 states? Maybe a blinking output what uses any system timer. Or a toggle switch what uses one input and one output? Sounds trivial and probably there are already tons of predefined Nodes for everything available. I do not like to use this but like to practice this myself for learning about how to implement a volatile or non volatile state variable, using a condition, set or reset outputs or variable values and perform the state transititions.

How about this?

[{"id":"15be82e64e6a1236","type":"inject","z":"b2f18a716bd20f99","name":"blinker","props":[{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"blinker","x":240,"y":560,"wires":[["e6dadef2c2c137cb"]]},{"id":"dd6a566b5926eb26","type":"debug","z":"b2f18a716bd20f99","name":"debug 12","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":640,"y":560,"wires":[]},{"id":"e6dadef2c2c137cb","type":"change","z":"b2f18a716bd20f99","name":"","rules":[{"t":"set","p":"blink","pt":"flow","to":"$not($flowContext(\"blink\"))\t","tot":"jsonata"},{"t":"set","p":"payload","pt":"msg","to":"blink","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":560,"wires":[["dd6a566b5926eb26"]]},{"id":"c0cd7b959854592f","type":"inject","z":"b2f18a716bd20f99","name":"initialise blinker","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"blinker","x":220,"y":500,"wires":[["b4ec8158465244e0"]]},{"id":"b4ec8158465244e0","type":"change","z":"b2f18a716bd20f99","name":"","rules":[{"t":"set","p":"blink","pt":"flow","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":500,"wires":[[]]},{"id":"0c22989079d58c67","type":"inject","z":"b2f18a716bd20f99","name":"do something","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1.5","crontab":"","once":false,"onceDelay":0.1,"topic":"doing something","payload":"","payloadType":"date","x":220,"y":640,"wires":[["353d0355cea58a8a"]]},{"id":"e09b60e86b5056af","type":"debug","z":"b2f18a716bd20f99","name":"debug 32","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":640,"y":640,"wires":[]},{"id":"353d0355cea58a8a","type":"switch","z":"b2f18a716bd20f99","name":"only if flow.blink is true","property":"blink","propertyType":"flow","rules":[{"t":"true"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":440,"y":640,"wires":[["e09b60e86b5056af"],[]]}]

I tried a number of state machine nodes and decided that none of them provided what I was looking for, so now I usually code state machines in a function node using a standard pattern. Here is an example of a simple one.

[{"id":"9a6f082b12f0dff8","type":"function","z":"53afba7b0275006c","name":"Check charge state machine","func":"let stat = context.get(\"stat\", \"file\") ||  {state: \"waiting\"}\nconst threshold = 11.6\nconst percentThreshold = 90\n\nmsg.voltage = msg.payload.voltage\nmsg.percent = msg.payload.percent\n\nswitch (stat.state) {\n\n    case \"charging\":\n        if (!msg.payload.isCharging) {\n            stat.state = \"switch\"\n        }\n        break;\n        \n    case \"low\":\n        //node.warn(`percent: ${msg.payload.percent}`)\n        if (msg.payload.isCharging || msg.payload.voltage > threshold + 0.1 || msg.payload.percent < percentThreshold) {\n            stat.state = \"switch\"\n        }\n        break;    \n        \n        case \"percentage_low\":\n        if (msg.payload.isCharging || msg.payload.percent > percentThreshold) {\n            stat.state = \"switch\"\n        }\n        break;\n\n    case \"waiting\":\n        if (msg.payload.isCharging  ||  msg.payload.voltage < threshold) {\n            stat.state = \"switch\"\n        }\n        break;\n\n    default:\n        // should never get here\n        node.warn(\"Invalid payload in Check charge state machine\")\n}\nif (stat.state == \"switch\") {\n    //node.warn(\"switch\")\n    // decide on new state\n    if (msg.payload.isCharging ) {\n        stat.state = \"charging\"\n    } else if (msg.payload.percent < percentThreshold) {\n        stat.state = \"percentage_low\"\n    } else if (msg.payload.voltage < threshold) {\n        stat.state = \"low\"\n    } else {\n        stat.state = \"waiting\"\n    }\n    node.status( `${stat.state}` )\n    context.set(\"stat\", stat, \"file\")\n    msg.payload = stat.state\n} else {\n    msg = null  // only send a message when state has changed\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":220,"y":600,"wires":[["eb3a04f5024292be"]],"info":"```mermaid\nstateDiagram-v2\nstate Switch <<choice>>\n[*] --> Switch\nSwitch --> Charging: isCharging\nSwitch --> Low: voltage<12 && !isCharging\nSwitch --> Waiting: voltage>=12 && !isCharging\nCharging --> Switch: !isCharging\nLow --> Switch: isCharging || voltage > 12.1\nWaiting --> Switch: isCharging || voltage < 12\n```"}]
3 Likes

I must admit that my simple flow example was much harder than just using a function node!

Try these links... for ideas...

You can code an FSM using 'case' constructs in pure JavaScript.

There are also a number of Node-RED Nodes that implement FSMs...
(I've not used them as I code mine in JS), here is just one as an example...
Do a search for FSM in the flows library tab - you'll find countless nodes.

Finally, here's a link to a tutorial on FSMs I wrote for my IoT students a few years ago...

4 Likes

Many thanks for the examples. I imported all JSON for examination.

  1. Agree with @Colin , NodeRed graphics is a data flow diagram and no state machine. As many typical problems to solve are time related and not data related its probably best to use plain JS code instead of graphics.

We can recognize the state variable and the states "charging", "low", "percentage_low" and "waiting". The JS syntax is a little strange for me but definitely I am going to play around with that code to explore it more.

  1. Traffic lights by @dynamicdave is a nice introduction to state machines and generally a nice example to explore state machines. The flow uses graphics instead plain JS.

I can see 3 states red,yellow, green or 1,2,3 although this are not the 4 states mentioned in the description pages with additional red-yellow state. The state variable is a counter what increments in separate node but it could be modified easily with a variable to change from every to every other state available.

Mainly I do not agree with the "Trigger every 3 seconds". This concept cannot serve the requirements that red and green state are typically much longer than yellow and red-yellow state.

State machines are always executed or triggered with maximum system cycle time. Its further inherent that they are only checking a state variable, then checking a condition inside this active state and maybe doing any action if condition is true. This action is always a "no time" action such as set or reset of any output or writing a value to any variable. Mainly the action must not be anything like "waiting for 3 seconds".

If any action takes time what is greater than system cycle time, its a own state and not a action what leads to a transitition. Anyhow time is a important resource normally managed by the operating system. I assume Node Red has something similar. However I did not find out how it is working.

Personally I am using the functions "set_timer" what is the action to start a time and "test_timer" what is the condition on any time related state machine at barebone systems. Both are trivial and consist of a single line of C instruction what relies to a system timer variable what simply increments. Every realtime operating system and even every PLC is working like this. It is possible to start hundreds of times concurently. Each timer occupies a single timer variable but only the system timer increments. All other variables are only for comparision with the system timer.

Assume all, there are more examples by @dynamicdave to check and hopefully I am able to cover some of my requirements in own example soon.

I think the traffic lights example was just an example of how to code a state machine, not something that was necessarily for detailed analysis of whether it actually worked or not.

Anyway its a nice toy to learn how to use. Here is my current "no frills" version with many things removed and renamed for better understanding.

  1. like the real world, traffic lights are most time red (10sec) and only short time green (5sec). The states for yellow and red-yellow are one second

  2. start stop toggle now works without further noticable delays

For the moment I failed with the following modifications

  1. swap the Nodes FSM timer and EnableFSM in position. Dont know if this is a good idea but the timer condition is not where I want. Same with the enable/disable gate. Disable should be possibly any additional state. The position of the timer works for this particular traffic light example as every state waits for elapsed timer. If we like to modifiy the example to a traffic light what only turns green on request of a vehicle approximation contact, the RED state should depend on the condition of the input line instead the timer. Thats why I would prefer to check the timer generaly inside the state code. This gives some more code lines for this example but changes to a indepenend state machine example not using any special exceptions of this traffic light.

  2. In the meantime I understood the state to output decoding but I dont like it this way. If the programmer needs to modify the number of states for changes in the requirements, this node heavily depends from the previous node. Beside there is nothing like a enum for the states what is possibly also available for JS(?).

Instead of the output decode I would prefere a directly SET and RESET command for the output bits in the state code itself. This needs to be executed before the line what changes the state variable. Thats the reason why I already moved this line to the end of state before the "break" line.

Anything for traffic light state names like

const tl_states = {Red:'Red', Red_Yellow: 'red_yellow', Green: 'green', Yellow: 'yellow'};

should do similar things like enum if we can use in the switch statement and sorry for my lack in JS. I also dont know on how to format the JSON code in one line like you did before. Its not useful for human readers.

[
    {
        "id": "be3ab1ef.3f0a68",
        "type": "tab",
        "label": "Statemachine Trafficlight",
        "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,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1150,
        "y": 60,
        "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": 1160,
        "y": 120,
        "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": 1160,
        "y": 180,
        "wires": [
            []
        ]
    },
    {
        "id": "28cc6bcb.920094",
        "type": "function",
        "z": "be3ab1ef.3f0a68",
        "name": "Finite State Machine",
        "func": "// Example of coding a state machine.\n\nvar fsm_state = flow.get(\"state_counter\") || 0;     // local state variable\nvar defaultDelay = flow.get(\"defaultDelay\");\n\nswitch (fsm_state)\n    {\n        case 0:                                             // State 0 is RED\n            node.send( {payload:fsm_state, delay:1000});    // Start red_yellow time\n            fsm_state = 1;                                  // State 1 red_yellow follows\n            break;\n            \n        case 1:                                             // State 1 is RED/YELLOW\n            node.send( {payload:fsm_state, delay:5000});    // Start GREEN time\n            fsm_state = 2;                                  // State 2 GREEN follows\n            break;\n        \n        case 2:                                             // State 2 is GREEN\n            node.send( {payload:fsm_state, delay:1000});    // Start YELLOW time\n            fsm_state = 3;                                  // State 3 YELLOW follows\n            break;\n            \n        case 3:                                             // State 3 is YELLOW\n            node.send( {payload:fsm_state, delay:10000});   // Start 10 sek RED time\n            fsm_state = 0;                                  // State 0 RED follows\n            break;\n    }\n    \nflow.set(\"state_counter\", fsm_state);\nnode.status({text:\"State = \" + fsm_state});\nreturn null;\n",
        "outputs": "1",
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 860,
        "y": 120,
        "wires": [
            [
                "743e732.c12228c",
                "778d9f53.8e5f7",
                "c8a94827.acb49",
                "82ab245c.6aa488"
            ]
        ]
    },
    {
        "id": "634074c5.d267d4",
        "type": "inject",
        "z": "be3ab1ef.3f0a68",
        "name": "Start & Stop Toggle",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "delay",
                "v": "1",
                "vt": "num"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "1",
        "payloadType": "num",
        "x": 210,
        "y": 120,
        "wires": [
            [
                "ee02a850.8eee8",
                "82ab245c.6aa488"
            ]
        ]
    },
    {
        "id": "ee02a850.8eee8",
        "type": "function",
        "z": "be3ab1ef.3f0a68",
        "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": 480,
        "y": 220,
        "wires": [
            []
        ]
    },
    {
        "id": "3361daf1.f94576",
        "type": "function",
        "z": "be3ab1ef.3f0a68",
        "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": 650,
        "y": 120,
        "wires": [
            [
                "28cc6bcb.920094"
            ]
        ]
    },
    {
        "id": "82ab245c.6aa488",
        "type": "delay",
        "z": "be3ab1ef.3f0a68",
        "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": 470,
        "y": 120,
        "wires": [
            [
                "3361daf1.f94576"
            ]
        ]
    }
]

Have a look at this simple FSM of a Door Lock System.
The FSM is coded in JavaScript using Switch/Case constructs.


Here's the NR flow so you can import it and try it out...

[{"id":"bda7a9600da379e9","type":"tab","label":"Simple FSM","disabled":false,"info":"","env":[]},{"id":"973402db690a9e27","type":"group","z":"bda7a9600da379e9","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["6cc8955d46f8c597","2ce14d893bbc8912","dbf0aae9e55d2608","6c60b7c301ef32a6"],"x":154,"y":159,"w":212,"h":242},{"id":"1e5525d9bb6fa97b","type":"function","z":"bda7a9600da379e9","name":"FSM","func":"// Initialise state if not set\nlet state = flow.get(\"doorState\") || \"LOCKED\";\nlet input = msg.payload;\n\nswitch (state) {\n    case \"LOCKED\":\n        if (input === \"unlock\") {\n            state = \"UNLOCKED\";\n            msg.payload = \"Door is now UNLOCKED.\";\n            node.status({fill:\"green\", shape:\"dot\", text:\"UNLOCKED\"});\n        } else if (input === \"intrusion\") {\n            state = \"ALARM\";\n            msg.payload = \"Intrusion detected! ALARM triggered!\";\n            node.status({fill:\"red\", shape:\"ring\", text:\"ALARM\"});\n        } else {\n            msg.payload = \"Door remains LOCKED.\";\n            node.status({fill:\"blue\", shape:\"dot\", text:\"LOCKED\"});\n        }\n        break;\n\n    case \"UNLOCKED\":\n        if (input === \"lock\") {\n            state = \"LOCKED\";\n            msg.payload = \"Door is now LOCKED.\";\n            node.status({fill:\"blue\", shape:\"dot\", text:\"LOCKED\"});\n        } else if (input === \"intrusion\") {\n            state = \"ALARM\";\n            msg.payload = \"Intrusion while door was UNLOCKED! ALARM!\";\n            node.status({fill:\"red\", shape:\"ring\", text:\"ALARM\"});\n        } else {\n            msg.payload = \"Door remains UNLOCKED.\";\n            node.status({fill:\"green\", shape:\"dot\", text:\"UNLOCKED\"});\n        }\n        break;\n\n    case \"ALARM\":\n        if (input === \"lock\") {\n            state = \"LOCKED\";\n            msg.payload = \"ALARM cleared. Door LOCKED.\";\n            node.status({fill:\"blue\", shape:\"dot\", text:\"LOCKED\"});\n        } else {\n            msg.payload = \"ALARM still active!\";\n            node.status({fill:\"red\", shape:\"ring\", text:\"ALARM\"});\n        }\n        break;\n\n    default:\n        state = \"LOCKED\";\n        msg.payload = \"Unknown state. Resetting to LOCKED.\";\n        node.status({fill:\"grey\", shape:\"dot\", text:\"RESET\"});\n        break;\n}\n\n// Save new state\nflow.set(\"doorState\", state);\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":300,"wires":[["0db43f37fdb46b84"]]},{"id":"6cc8955d46f8c597","type":"inject","z":"bda7a9600da379e9","g":"973402db690a9e27","name":"unlock","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"unlock","payloadType":"str","x":270,"y":240,"wires":[["1e5525d9bb6fa97b"]]},{"id":"2ce14d893bbc8912","type":"inject","z":"bda7a9600da379e9","g":"973402db690a9e27","name":"lock","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"lock","payloadType":"str","x":270,"y":300,"wires":[["1e5525d9bb6fa97b"]]},{"id":"dbf0aae9e55d2608","type":"inject","z":"bda7a9600da379e9","g":"973402db690a9e27","name":"intrusion","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"intrusion","payloadType":"str","x":280,"y":360,"wires":[["1e5525d9bb6fa97b"]]},{"id":"dcda94acbba358b3","type":"inject","z":"bda7a9600da379e9","name":"Initial reset to LOCKED state","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":300,"y":100,"wires":[["1e5525d9bb6fa97b"]]},{"id":"0db43f37fdb46b84","type":"debug","z":"bda7a9600da379e9","name":"State","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":300,"wires":[]},{"id":"6c60b7c301ef32a6","type":"comment","z":"bda7a9600da379e9","g":"973402db690a9e27","name":"Simulated inputs","info":"","x":260,"y":200,"wires":[]},{"id":"1c6aa28beaa23d13","type":"comment","z":"bda7a9600da379e9","name":"Example of simple FSM coded with Switch/Case constructs","info":"","x":670,"y":260,"wires":[]},{"id":"9f050b39ca8e17c7","type":"comment","z":"bda7a9600da379e9","name":"Door Lock System","info":"","x":550,"y":200,"wires":[]}]

Here is an example of how I'm using them. This one basically gets the states of contact sensors, builds an array and then uses https://flows.nodered.org/node/node-red-contrib-persistent-fsm to tell me if all of the sensors are open or closed. If this is of any interest to you, I have several others using the fsm node I can share that are more complex. I didn't write the function node and I can't remember where I got it, but I think it was from somewhere on the forums here.

[{"id":"01a24da26d264ded","type":"group","z":"95b8ed7423780576","g":"be8d0f9120a180d2","name":"All Doors","style":{"stroke":"#000000","label":true,"fill":"#bfdbef","color":"#000000"},"nodes":["06c81e813849f3f7","40b94a442cd91a71","29830a4e3f07380d","7ece31086c5da729"],"x":1814,"y":1779,"w":212,"h":322},{"id":"06c81e813849f3f7","type":"state-machine","z":"95b8ed7423780576","g":"01a24da26d264ded","name":"Door Status","triggerProperty":"payload","triggerPropertyType":"msg","stateProperty":"payload","statePropertyType":"msg","initialDelay":"","persistOnReload":false,"outputStateChangeOnly":true,"throwException":false,"states":["Initializing...","All Doors closed","All Doors open"],"transitions":[{"name":"Front Door open, Sliding Glass Door open, Laundry Room Door open, Garage Door open, Garage Man Door open","from":"Initializing...","to":"All Doors open"},{"name":"Front Door closed, Sliding Glass Door closed, Laundry Room Door closed, Garage Door closed, Garage Man Door closed","from":"Initializing...","to":"All Doors closed"}],"x":1930,"y":2060,"wires":[["66d0e4412a7c8e3e"]],"icon":"font-awesome/fa-gears"},{"id":"40b94a442cd91a71","type":"join","z":"95b8ed7423780576","g":"01a24da26d264ded","name":"Get Values","mode":"custom","build":"object","property":"payload.status","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":true,"accumulate":true,"timeout":"","count":"5","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1920,"y":1820,"wires":[["29830a4e3f07380d"]]},{"id":"29830a4e3f07380d","type":"function","z":"95b8ed7423780576","g":"01a24da26d264ded","name":"Build Array","func":"var FrontDoor = msg.payload.status[\"FrontDoor\"];\nvar SlidingGlassDoor = msg.payload.status[\"SlidingGlassDoor\"];\nvar LaundryRoomDoor = msg.payload.status[\"LaundryRoomDoor\"];\nvar GarageDoor = msg.payload.status[\"GarageDoor\"];\nvar GarageManDoor = msg.payload.status[\"GarageManDoor\"];\n\nmsg.payload = [FrontDoor,SlidingGlassDoor,LaundryRoomDoor,GarageDoor,GarageManDoor];\n\nreturn msg;\n\n//Array is buit based on order of sensors","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1930,"y":1900,"wires":[["7ece31086c5da729"]]},{"id":"7ece31086c5da729","type":"change","z":"95b8ed7423780576","g":"01a24da26d264ded","name":"Set Payload","rules":[{"t":"set","p":"statusReport.doors.state","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"doors","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":" $string(payload[0]) & \", \"  & $string(payload[1]) & \", \"  & $string(payload[2]) & \", \"  & $string(payload[3]) & \", \"  & $string(payload[4])","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1930,"y":1980,"wires":[["06c81e813849f3f7"]]}]

JavaScript does not have a built-in enum data type like some other languages (e.g., TypeScript, C#, or Java). However, you can simulate enums in plain JavaScript using objects or Object.freeze() to create immutable enums. Here's the Door Lock System re-coded to make use of enum-styles for States and Events.


Note to unlock the door an 'event' and a 'code' are needed as an object like this...

{
  "event": "ENTER_CODE",
  "code": "1234"
}


Here's the json for the flow so you can Import it and try it out...

[{"id":"fbe4d5cbfa77795b","type":"function","z":"bda7a9600da379e9","name":"FSM with enum-style","func":"// Enum-like constants for states and events\nconst States = Object.freeze({\n    LOCKED: \"LOCKED\",\n    UNLOCKED: \"UNLOCKED\",\n    ALARMED: \"ALARMED\"\n});\n\nconst Events = Object.freeze({\n    ENTER_CODE: \"ENTER_CODE\",\n    LOCK: \"LOCK\",\n    FORCE_ENTRY: \"FORCE_ENTRY\",\n    RESET_ALARM: \"RESET_ALARM\"\n});\n\n// Get current state or default to LOCKED\nlet currentState = context.get(\"state\") || States.LOCKED;\n\n// Simulated correct code\nconst correctCode = \"1234\";\n\n// Parse incoming payload\nlet event = msg.payload;\n\n// Optional: if payload is an object, use event and code fields\nif (typeof event === \"object\") {\n    event = event.event;\n    msg.code = msg.payload.code;\n}\n\n// FSM logic\nswitch (currentState) {\n    case States.LOCKED:\n        if (event === Events.ENTER_CODE && msg.payload.code === correctCode) {\n            currentState = States.UNLOCKED;\n        } else if (event === Events.FORCE_ENTRY) {\n            currentState = States.ALARMED;\n        }\n        break;\n\n    case States.UNLOCKED:\n        if (event === Events.LOCK) {\n            currentState = States.LOCKED;\n        } else if (event === Events.FORCE_ENTRY) {\n            currentState = States.ALARMED;\n        }\n        break;\n\n    case States.ALARMED:\n        if (event === Events.RESET_ALARM) {\n            currentState = States.LOCKED;\n        }\n        break;\n\n    default:\n        node.error(\"Unknown state: \" + currentState);\n}\n\n// Save state\ncontext.set(\"state\", currentState);\n\n// Display currentState of FSM on function node-body\nnode.status({text:currentState});\n\n// Output state and optional message\nmsg.state = currentState;\nmsg.payload = {\n    state: currentState,\n    message: `Door is now ${currentState}` \n};\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":660,"wires":[["219f8336f9879f07"]]},{"id":"219f8336f9879f07","type":"debug","z":"bda7a9600da379e9","name":"State","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":660,"wires":[]},{"id":"8fe64eed6aa2d931","type":"inject","z":"bda7a9600da379e9","name":"Initial reset to LOCKED state","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"LOCKED","payloadType":"str","x":300,"y":480,"wires":[["fbe4d5cbfa77795b"]]},{"id":"137d4d6b5cb4e5b1","type":"comment","z":"bda7a9600da379e9","name":"Door Lock System with enum-style for States and Events","info":"","x":670,"y":560,"wires":[]},{"id":"f1b04b170bd5d3d9","type":"comment","z":"bda7a9600da379e9","name":"Example of simple FSM coded with Switch/Case constructs","info":"","x":670,"y":620,"wires":[]},{"id":"4f56e1275fe8aa06","type":"group","z":"bda7a9600da379e9","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["a32a59ae01d0671f","b892b1265dab2695","66291c66a3a7e61e","c65fb2230d93cd9d","f6a79cf663ed9eb1"],"x":154,"y":519,"w":252,"h":262},{"id":"a32a59ae01d0671f","type":"inject","z":"bda7a9600da379e9","g":"4f56e1275fe8aa06","name":"UNLOCK","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"event\":\"ENTER_CODE\",\"code\":\"1234\"}","payloadType":"json","x":280,"y":600,"wires":[["fbe4d5cbfa77795b"]]},{"id":"b892b1265dab2695","type":"inject","z":"bda7a9600da379e9","g":"4f56e1275fe8aa06","name":"LOCK","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"LOCK","payloadType":"str","x":270,"y":640,"wires":[["fbe4d5cbfa77795b"]]},{"id":"66291c66a3a7e61e","type":"inject","z":"bda7a9600da379e9","g":"4f56e1275fe8aa06","name":"RESET_ALARM","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"RESET_ALARM","payloadType":"str","x":300,"y":740,"wires":[["fbe4d5cbfa77795b"]]},{"id":"c65fb2230d93cd9d","type":"comment","z":"bda7a9600da379e9","g":"4f56e1275fe8aa06","name":"Simulated inputs","info":"","x":260,"y":560,"wires":[]},{"id":"f6a79cf663ed9eb1","type":"inject","z":"bda7a9600da379e9","g":"4f56e1275fe8aa06","name":"FORCE_ENTRY","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"FORCE_ENTRY","payloadType":"str","x":300,"y":700,"wires":[["fbe4d5cbfa77795b"]]}]

Note:
If you want real enum support, consider using TypeScript, which is a superset of JavaScript that includes enum as a native feature. Although I haven't used TypeScript, I believe some of the Forum members have gone down that path (and may be able to give some guidance).

That's easy, just click the 'compact' option when you Export the json.

1 Like

Another thing you could do is implement inputting correct and incorrect lock-codes.

Edit:
I've quickly had a go at re-coding the FSM in the traffic light system using the enum-style.
You should be able to replace the original function node with the following JavaScript...

// Define enum-style states
// If required, you could change the following values from numeric to text
// and then modify the state-decoding for the three traffic lights

const STATES = {
    RED: 0,
    RED_YELLOW: 1,
    GREEN: 2,
    YELLOW: 3
};

// Get current state or default to RED
let state = flow.get("state_counter") || STATES.RED;
let delay = 0;

// FSM logic using enum-style
switch (state) {
    case STATES.RED:
        delay = 1000; // RED duration
        node.send({ payload: state, delay: delay });
        state = STATES.RED_YELLOW;
        break;

    case STATES.RED_YELLOW:
        delay = 5000; // RED/YELLOW duration
        node.send({ payload: state, delay: delay });
        state = STATES.GREEN;
        break;

    case STATES.GREEN:
        delay = 1000; // GREEN duration
        node.send({ payload: state, delay: delay });
        state = STATES.YELLOW;
        break;

    case STATES.YELLOW:
        delay = 10000; // YELLOW duration
        node.send({ payload: state, delay: delay });
        state = STATES.RED;
        break;
}

// Save updated state
flow.set("state_counter", state);
node.status({ text: "State = " + Object.keys(STATES).find(key => STATES[key] === state) });

return null;

Note:
Your delay of 10000 on the yellow > red transition seems rather excessive for this demo.

2 Likes

Or save yourself the hassle and don't bother! :slight_smile: If you want better type safety with JavaScript, simply use JSDoc comments. TypeScript is at least double the work and effort. Might be useful for large programming teams doing very large apps but is really not that useful overall - in my opinion (I've worked with dozens of computer languages over the years). The advantage of JS is that you can use it both in the browser and at the back-end and it needs no build step.

ENUM's have actually been proposed for a future version of JS and look likely to make it in some form eventually. But as Dave has shown, you really don't need them in JS.

Just created v2 of the Traffic Light System with enum types for 'STATES' and 'STATE_TABLE'.
Also using separate output ports for the three traffic lights and msg.delay for the FSM Timer.

[{"id":"b5a1333e3bb6b1d0","type":"tab","label":"Statemachine Trafficlight","disabled":false,"info":"","env":[]},{"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"],"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":"6cc0d48304b8068c","type":"comment","z":"b5a1333e3bb6b1d0","name":"FSM-v2 with enum types for 'STATES' and 'STATE_TABLE'","info":"","x":630,"y":240,"wires":[]}]

Please note the above FSM is a Deterministic Finite State Machine (DFSM) or Linear FSM or Sequential FSM. If you need a 'condition' to be used to determine which state the machine goes to next, then it needs to be coded as a 'Mealy Machine' like the 'Door Lock System' above.

1 Like

Just for a laugh this morning I gave ChatGPT the JS for the FSM I used in the Door Lock System above and asked it to show me the state diagram or transition table.
Sort of reverse engineering my solution. This is the result I received.
ChatGPT did offer to create an image of the state diagram, but when I looked at it - it had missed off many of the transitions!!

Here's the transition table that summarizes all valid transitions for your Finite State Machine (FSM) based on the provided script:

Current State Event Condition Next State Comment
LOCKED ENTER_CODE msg.code === "1234" UNLOCKED Unlock with correct code
LOCKED FORCE_ENTRY — ALARMED Forced entry triggers alarm
LOCKED Any other event — LOCKED No change
UNLOCKED LOCK — LOCKED Lock the door
UNLOCKED FORCE_ENTRY — ALARMED Forced entry triggers alarm
UNLOCKED Any other event — UNLOCKED No change
ALARMED RESET_ALARM — LOCKED Reset alarm
ALARMED Any other event — ALARMED No change

Notes:

  • Events not listed under a state result in no state change.
  • msg.code is only evaluated when event === ENTER_CODE.
  • context.get("state") and context.set("state", currentState) are used to persist the FSM state across invocations.

AI is a hype what is useless for most cases. Remember the fuzzy logic hype a few years ago? It has certain applications but nobody talks about today.

Your enum style seems some improvement. Accept JS is not C and comes with other flaws and benefits. I adapted STATES in my version and dropped the comments therefore as the source is now more self explaining. I stopped yesterday when I failed changing the output decoder to STATE style as I prefer the output actions like they are given in your doorlock example.

Have to do some readings about NodeRED and JS for better understanding your examples. The state variable is "state" what lives on the stack while the node is executed. To remember the state at next execution, you use the methods node.get and node.set with another variable "state_counter" what is stored non volatile at the hard disk. (?) Is it possible to do it directly with only one variable? To be more generic, state_counter is always a state_variable and in other languages I always take a one or two character prefix in camel case style for this variable.

Have to sort my mind and do more reading about scope and livetime of data in NodeRed as this seems diffrent to other languages. Inside one tab there could be more than one flow. How do they communicate between, how and when is initialisation and other questions.

@DiverRich example refers to

node-red-contrib-persistent-fsm (node) - Node-RED

what refers to

what I want to design and understand myself more directly.

Try GitHub Copilot with Claude Sonnet 4 - it is a massive leap forwards. Still gets some stuff wrong of course but it keeps track of things far better and is able to work through initial errors and fix them as well as it self-tests things. Of course, it helps to give it some good guidance and a carefully worded prompt. I'm finding it more and more useful, giving me more creativity than ever.

I'm of different experience, I think declared types is useful very soon after a program grows out of a simple script. When I learned to code, it was also very convenient to click dot after some variable to see everything it contains. Effectively, you can know what something is without debugging. In JS, I almost always have to run something and put a console.log on input to know what to look for (exact names of a variable for instance). Due to popularity of JS, I guess most fresh programmers think differently about it?