Timer in function node

Hello.

I would like to make a countdown timer using a function node.
Counter needs to be reset to its initial value when the function is triggered, so as long the function is triggered before the counter value reaches 0 it will never timeout.
I will need also the possibility to stop it.
My idea was to clearTimeout every time the function node is triggered and then reset it back to the value I want so my countdown function will be executed when there is no trigger.

I'm aware of the various countdown and stop timer nodes, but I don't want to use them as they either cannot be set dynamically or output stuff I don't need.

My function:

clearTimeout(countdown); //this should clear the Timeout 
setTimeout(countdown, 3000); //this should trigger function when no trigger recieved
context.set(['target', 'current'] , [5000, 0]);
var target = context.get('target'); // 5s
var current = context.get('current'); // 0 secs

function countdown() {
        current = context.get('current') + 1000;
        context.set('current', current);
        let diff = target-current;
        let sec = (diff/1000); // gets secs
        if (diff > 0 ) {
            setTimeout(countdown, 1000);
            msg.timeout = false;
            msg.sec = sec;
            node.send(msg);
            }
        else if (diff === 0 ) {
            msg.sec = 0;
            msg.timeout = true;
            node.send(msg);
        }
}
return;

This is my test flow:

[{"id":"2c1af103.8bdcce","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"4c1e07a9.8e7ac8","type":"inject","z":"2c1af103.8bdcce","name":"Trigger","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":250,"y":120,"wires":[["e97d043.a3fa6f8"]]},{"id":"5baef51.34a8d0c","type":"debug","z":"2c1af103.8bdcce","name":"true","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":100,"wires":[]},{"id":"e97d043.a3fa6f8","type":"function","z":"2c1af103.8bdcce","name":"","func":"clearTimeout(countdown); //this should clear the Timeout \nsetTimeout(countdown, 3000); //this should trigger function when no trigger recieved\n//setTimeout(() => countdown(), 3000);\ncontext.set(['target', 'current'] , [5000, 0]);\nvar target = context.get('target'); // 5s\nvar current = context.get('current'); // 0 secs\n\nfunction countdown() {\n        current = context.get('current') + 1000;\n        context.set('current', current);\n        let diff = target-current;\n        let sec = (diff/1000); // gets secs\n        if (diff > 0 ) {\n            setTimeout(countdown, 1000);\n            msg.timeout = false;\n            msg.sec = sec;\n            node.send(msg);\n            }\n        else if (diff === 0 ) {\n            msg.sec = 0;\n            msg.timeout = true;\n            node.send(msg);\n        }\n}\nreturn;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":430,"y":120,"wires":[["8f67b9a2.77a558"]]},{"id":"8f67b9a2.77a558","type":"switch","z":"2c1af103.8bdcce","name":"","property":"timeout","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":600,"y":120,"wires":[["5baef51.34a8d0c"],["69eabb27.9e9834"]]},{"id":"69eabb27.9e9834","type":"debug","z":"2c1af103.8bdcce","name":"false","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":140,"wires":[]}]

I will appreciate any ideas on how to approach this.

You realise that there is a node that does that for you?

But if you want to do it in a function node, you need to remember that setTimeout is asynchronous. That means that whatever runs in the function is outside of the normal flow. Your example code seems to be trying to call the wrapping function from the inside. The setTimeout function returns a reference to itself that you can then use for cancelling. You need to cancel the currently running timout and create a new one.

Thank you for your advice.
How I can capture and reuse the reference to the setTimout function to cancel all timeouts currently active?
Can this be done by storing this in context as an array and running forEach function to clearTimeout?
The main problem will be that I don't know how to capture this reference.

You can do it like you described.

1 Like

You can save the reference to your setTimeout in context so that you can retrieve it from any of the instances of the function node like any other variable. You can also push those references to an array and than save the array as a context/flow/global variable no problem.
There is just one caveat to keep in mind. If you have context storage to disk enabled in your settings js file you’ll have to use the otherwise deprecated way of context.timervar = yourTimerVar; instead of the standard set.context() way and var yourTimerVar = context.timervar; to retrieve it as otherwise you will get a circular error as nodered tries to stringify the function reference for saving to disc.

Johannes

3 Likes

Since all you are doing is sending a msg every second while current is < target why dont you drop this notion of using timers in a function and use a 1sec inject?

example...

[{"id":"b89dcfdc.ee35c","type":"inject","z":"78ec231c.fea6bc","name":"","topic":"start","payload":"3000","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1080,"y":180,"wires":[["1cbaee7b.915c42"]]},{"id":"a7b12629.668d08","type":"inject","z":"78ec231c.fea6bc","name":"stop","topic":"stop","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1070,"y":220,"wires":[["1cbaee7b.915c42"]]},{"id":"fa48b94c.7fb0e8","type":"inject","z":"78ec231c.fea6bc","name":"","topic":"start","payload":"5000","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1080,"y":140,"wires":[["1cbaee7b.915c42"]]},{"id":"1cbaee7b.915c42","type":"function","z":"78ec231c.fea6bc","name":"","func":"let thisTimer = context.get(\"thisTimer\") || {};\n\nif(msg.topic == \"start\") {\n    thisTimer.current = msg.payload;\n}\n\nif(msg.topic == \"stop\") {\n    thisTimer.current = null;\n}\n    \nif(msg.topic == \"tick\") {\n    if(thisTimer.current > 0) {\n        thisTimer.current -= 1000;\n        if(thisTimer.current <= 0) {\n           msg.timeout = true; \n           msg.remaining = 0;\n        } else {\n            msg.timeout = false;\n            msg.remaining = thisTimer.current;\n        }\n        return msg;\n    }\n}\n\ncontext.set(\"thisTimer\", thisTimer) ;","outputs":1,"noerr":0,"x":1250,"y":160,"wires":[["4a9cb56d.8f43ec"]]},{"id":"4a9cb56d.8f43ec","type":"debug","z":"78ec231c.fea6bc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1250,"y":200,"wires":[]},{"id":"459ad1ac.f9be6","type":"inject","z":"78ec231c.fea6bc","name":"tick","topic":"tick","payload":"true","payloadType":"bool","repeat":"1","crontab":"","once":false,"onceDelay":0.1,"x":1070,"y":100,"wires":[["1cbaee7b.915c42"]]}]

1 Like

Below is my crack at it - I'm sure it could be simplified a bit, but seems pretty robust. Thanks to @JGKK for pointing out the context.timevar thing, would have come a bit unstuck otherwise.

@Steve-Mcl I like your approach, its definitely simpler however we're working on a pretty complex project and I've found inject nodes to be a bit flakey on boot. It may be down to the way we are running the system (all flows are loaded from a central NAS, which also contains all the nodes etc, which slows the overall boot time; but makes updating all the devices and keeping versions synchronized very nice and easy. This also reduces config in the case of the failure to basically a straight swap of device and freshly imaged SD card).

var timerEnable;

function timer() {
    context.set("timerValue", (context.get("timerValue") - 1));
    node.status({fill:"yellow",shape:"dot",text:context.get("timerValue")}); 
    if (context.get("timerValue") === 0) {
        msg.payload = "timeout"
        node.status({fill:"green",shape:"dot",text:context.get("timerValue")}); 
        node.send(msg);
        clearInterval(context.timer);
        context.set("timerRunning", false)
        return;
   }
}

if (msg.payload === "stop") {
    timerEnable = false;
}
else {
    timerEnable = true;
}

if (context.get("timerRunning") === false && timerEnable === true) { //Start
    context.set("timerValue", flow.get("timeout"));
    //msg.payload = 'timer start';
    node.status({fill:"blue",shape:"dot",text:context.get("timerValue")});
    context.timer = setInterval(timer,1000);
    context.set("timerRunning", true);
}

else if (context.get("timerRunning") === true && timerEnable === true) { //Reset
    context.set("timerValue", flow.get("timeout"));
    //msg.payload = 'timer reset';
    node.status({fill:"blue",shape:"ring",text:context.get("timerValue")});
    context.set("timerRunning", true);
}

else if (timerEnable === false) { //Stop
    //msg.payload = 'timer stop';
    clearInterval(context.timer);
    node.status({fill:"red",shape:"ring",text:"Stopped"});
    context.set("timerRunning", false);
}

return;
[
    {
        "id": "72140aba.c30d64",
        "type": "inject",
        "z": "59fcda25.edd454",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 480,
        "y": 120,
        "wires": [
            [
                "6e6703ab.87e13c"
            ]
        ]
    },
    {
        "id": "30b0c638.4abdfa",
        "type": "inject",
        "z": "59fcda25.edd454",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "stop",
        "payloadType": "str",
        "x": 470,
        "y": 200,
        "wires": [
            [
                "6e6703ab.87e13c"
            ]
        ]
    },
    {
        "id": "ff53b61c.471938",
        "type": "inject",
        "z": "59fcda25.edd454",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "15",
        "payloadType": "num",
        "x": 470,
        "y": 280,
        "wires": [
            [
                "4ec48c94.eaf824"
            ]
        ]
    },
    {
        "id": "4ec48c94.eaf824",
        "type": "change",
        "z": "59fcda25.edd454",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "timeout",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 660,
        "y": 280,
        "wires": [
            []
        ]
    },
    {
        "id": "6e6703ab.87e13c",
        "type": "function",
        "z": "59fcda25.edd454",
        "name": "",
        "func": "var timerEnable;\n\nfunction timer() {\n    context.set(\"timerValue\", (context.get(\"timerValue\") - 1));\n    node.status({fill:\"yellow\",shape:\"dot\",text:context.get(\"timerValue\")}); \n    if (context.get(\"timerValue\") === 0) {\n        msg.payload = \"timeout\"\n        node.status({fill:\"green\",shape:\"dot\",text:context.get(\"timerValue\")}); \n        node.send(msg);\n        clearInterval(context.timer);\n        context.set(\"timerRunning\", false)\n        return;\n   }\n}\n\nif (msg.payload === \"stop\") {\n    timerEnable = false;\n}\nelse {\n    timerEnable = true;\n}\n\nif (context.get(\"timerRunning\") === false && timerEnable === true) { //Start\n    context.set(\"timerValue\", flow.get(\"timeout\"));\n    //msg.payload = 'timer start';\n    node.status({fill:\"blue\",shape:\"dot\",text:context.get(\"timerValue\")});\n    context.timer = setInterval(timer,1000);\n    context.set(\"timerRunning\", true);\n}\n\nelse if (context.get(\"timerRunning\") === true && timerEnable === true) { //Reset\n    context.set(\"timerValue\", flow.get(\"timeout\"));\n    //msg.payload = 'timer reset';\n    node.status({fill:\"blue\",shape:\"ring\",text:context.get(\"timerValue\")});\n    context.set(\"timerRunning\", true);\n}\n\nelse if (timerEnable === false) { //Stop\n    //msg.payload = 'timer stop';\n    clearInterval(context.timer);\n    node.status({fill:\"red\",shape:\"ring\",text:\"Stopped\"});\n    context.set(\"timerRunning\", false);\n}\n\nreturn;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "context.set(\"timerRunning\", false);",
        "finalize": "",
        "x": 640,
        "y": 200,
        "wires": [
            [
                "3e2c38c1.05ad18"
            ]
        ]
    },
    {
        "id": "3e2c38c1.05ad18",
        "type": "debug",
        "z": "59fcda25.edd454",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 860,
        "y": 200,
        "wires": []
    },
    {
        "id": "c6b254e1.f62668",
        "type": "inject",
        "z": "59fcda25.edd454",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "6",
        "payloadType": "num",
        "x": 470,
        "y": 320,
        "wires": [
            [
                "4ec48c94.eaf824"
            ]
        ]
    }
]
1 Like

Thank you all for your help.
@JGKK thanks for pointing out how to access the contex when it's stored on disk and hints on how to clearTimeout. I managed to get my function working based on your and @TotallyInformation comment.
@Steve-Mcl I like your proposal, but as mentioned by @droberts this is a part of the bigger system and will be used to control the rest of the flow.
The purpose of this timer is to receive a signal from the machine sensor every time a part is produced. Cycle time will vary between machines and products, hence expected time between signals need to be set dynamically.
When the signal will not be received the countdown will start, this needs to be dynamic to accommodate different processes and can be reset by the next signal from the sensor. This is allowing to resolve quick issues without sending a trigger to lock the machine.

Now we have 3 timers that can be used in function node :slight_smile:

My function:

clearTimeout(context.t1);
clearTimeout(context.t2);
if (!msg.stop){
context.t1 = setTimeout(countdown, 3000); //change to dynamic after testing
node.status({fill:"green",shape:"dot",text:"waiting for signal"});
var t2;
context.set(['target', 'current'] , [15000, 0]); //chnge to dynamic after testing
var target = context.get('target'); 
var current = context.get('current'); // 0 secs
} else {
    node.status({fill:"red",shape:"dot",text:"stopped"});
    node.done();
}

function countdown() {
        current = context.get('current') + 1000;
        context.set('current', current);
        let diff = target-current;
        let sec = (diff/1000); // gets secs
        node.status({fill:"blue",shape:"dot",text:"trigger in:" + sec});
        if (diff > 0 ) {
            context.t2  = setTimeout(countdown, 1000);
            msg.timeout = false;
            msg.sec = sec;
            node.send(msg);
            }
        else if (diff === 0 ) {
            msg.sec = 0;
            msg.timeout = true;
            node.status({fill:"yellow",shape:"dot",text:"Timeout"});
            node.send(msg);
        }
}
return;

Example flow:

[{"id":"2c1af103.8bdcce","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"4c1e07a9.8e7ac8","type":"inject","z":"2c1af103.8bdcce","name":"Trigger","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":250,"y":120,"wires":[["e97d043.a3fa6f8"]]},{"id":"5baef51.34a8d0c","type":"debug","z":"2c1af103.8bdcce","name":"true","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":100,"wires":[]},{"id":"e97d043.a3fa6f8","type":"function","z":"2c1af103.8bdcce","name":"","func":"clearTimeout(context.t1);\nclearTimeout(context.t2);\nif (!msg.stop){\ncontext.t1 = setTimeout(countdown, 3000); //change to dynamic after testing\nnode.status({fill:\"green\",shape:\"dot\",text:\"waiting for signal\"});\nvar t2;\ncontext.set(['target', 'current'] , [15000, 0]); //chnge to dynamic after testing\nvar target = context.get('target'); \nvar current = context.get('current'); // 0 secs\n} else {\n    node.status({fill:\"red\",shape:\"dot\",text:\"stopped\"});\n    node.done();\n}\n\nfunction countdown() {\n        current = context.get('current') + 1000;\n        context.set('current', current);\n        let diff = target-current;\n        let sec = (diff/1000); // gets secs\n        node.status({fill:\"blue\",shape:\"dot\",text:\"trigger in:\" + sec});\n        if (diff > 0 ) {\n            context.t2  = setTimeout(countdown, 1000);\n            msg.timeout = false;\n            msg.sec = sec;\n            node.send(msg);\n            }\n        else if (diff === 0 ) {\n            msg.sec = 0;\n            msg.timeout = true;\n            node.status({fill:\"yellow\",shape:\"dot\",text:\"Timeout\"});\n            node.send(msg);\n        }\n}\nreturn;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":430,"y":120,"wires":[["8f67b9a2.77a558"]]},{"id":"8f67b9a2.77a558","type":"switch","z":"2c1af103.8bdcce","name":"","property":"timeout","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":600,"y":120,"wires":[["5baef51.34a8d0c"],["69eabb27.9e9834"]]},{"id":"69eabb27.9e9834","type":"debug","z":"2c1af103.8bdcce","name":"false","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":160,"wires":[]},{"id":"f46ad4e2.786098","type":"inject","z":"2c1af103.8bdcce","name":"Stop","props":[{"p":"stop","v":"true","vt":"bool"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":230,"y":180,"wires":[["e97d043.a3fa6f8"]]}]