Timer counts to a label

Sounds simple but I'm struggling. I need to monitor an event that lasts ~ 20 seconds and output that to a label on the UI.There are literally dozens of "countdown" timer nodes that have set values, but nothing (apparent) that uses time-on/time-off with continuous output suitable for writing to a label. I've got an inject node sending a timestamp payload & TRUE command to start it, and another inject node with timestamp payload & FALSE command to stop it. I wrote a routine in a function to do the subtraction and write the result(s) out, but it locks up N-R. I think it's doing to many write-counts and I don't know how to throttle it.

var tStart;
var tDiff;
var d = new Date();
var tStop = d.getTime();

tStart = msg.payload;
while (msg.command === true) {
msg.topic = tStop - tStart;
node.send(msg.topic)
}

return msg;`.

Anyhow, this is what I'd like to end up with: Stop Watch

Thanks!

Russ

Sorry, but I am not quite getting how you want to control it.

Do you want to send a message saying (something like) "Run for 8 seconds"?
and an optional STOP message?

Or what?

It cant work like this as your loop will run forever and block the whole node event loop effectively killing nodered.
You have to understand two things here:

  • node.js which nodered runs on is single threaded so one thing happens at a time and if something like an infinite loop is happening it will never get to do anything else but the loop
  • everytime you send a msg to a function node a new instance of the function is created so your first node can never get the stop message as it will never no about it as it will arrive to a different instance

So keeping those two factors in mind you will have to use something like setInterval() instead of a while loop (which is mostly a bad idea in node.js, you should try to avoid blocking code patterns in general as they go against its nature) to write a function that lets say checks for the stop every 100 milliseconds and doesn’t block the eventloop in the meantime. Secondly you will have to use context storage which you can read in the documentation about as a variable saved to context can be accessed from every instance of the same function node. So when a stop message arrives set a variable accordingly in the nodes context and check that context var on every interval.
This should give you some starting pointers if you want to do everything within a function node.
You could also look at the concept of an external tick send by an inject at an interval triggering the checking for a stop message and otherwise sending the ellapsed time.
Have a look here at something using that concept:

Hope this gives you some ideas, Johannes

1 Like

I thought you were using a node that showed the count as status ? If so then a status node will be able to grab that and pass it on as msg.status. which you can then move to payload and pass out to the UI.

If you check the link I posted... I want that type of continuous counter in a label on my UI, triggered by one node and stopped by another.

Johannes - I knew my code was overloading the system. I am an extreme rookie and everything I've done to this point is by trying to adapt various snippets found on 'net search engines. I have, on numerous occasions, attempted to use "context" and have never got it to work. Seems there's always a gripe about a variable not defined or something.of that nature. Also, your code is not in a code block so it's unusable...

dceejay - The node I'm using to capture the total elapsed time is "interval length". I don't see a status count anywhere on it...

Thanks!

Russ

Johannes, your setInterval() looks promising! I've got a couple problems though. Here's a pic of my function code and it's output to the debug panel. As you can see, it starts fine. I get the error "Function tried to send a message of type number", which I do not understand. Of course I want to send a number, does that need to be converted to a string to get out of the function? And then, sending command = false does nothing to clear the interval and the program runs on until I comment out the lines in the timer function.

I changed the code a bit and put in an "else" block:

var aInterval;
var tStart;
var d = new Date();
var tStop = d.getTime();

tStart = msg.payload;

aInterval = setInterval(mTimer, 1000);

  function mTimer () {
    if (msg.command === false) {
      clearInterval(aInterval);
}   else {
    msg.payload = tStop - tStart;
    node.send(msg.payload);
}

return msg;

And now nothing runs, my function node has a red triangle that says "invalid properties: -noerr"

Almost there, as usual...

Russ

Did you read this?

and look at the linked documentation about context storage here:

and you are seeing the error because of:

this needs to be:

node.send(msg);

or

node.send({payload:var_you_want_to_send});

if you only want to send the original payload property as both node.send() and return msg will only accept an object and not types like number or string.
But its really important that you understand what I wrote above about how everytime you send something to a function node a new instance of that function node is spawned that doesn’t know about anything in the previous or future versions of itself as it can only directly access its own instances scope. The only way around this is context storage as that is scope accessible by all instances of the function node.
So you need to wrap the calling of the interval function in a if clause as to only call it when you want, if the message is stop dont call the function but instead set a context variable to true that the interval function checks on every iteration.

Johannes

Something like this:

[{"id":"a1a1422e.29c68","type":"inject","z":"c1dbfea7.32d2d","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":230,"y":180,"wires":[["d7390833.cafea"]]},{"id":"2ba8b712.8596","type":"inject","z":"c1dbfea7.32d2d","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"stop","payloadType":"str","x":220,"y":260,"wires":[["d7390833.cafea"]]},{"id":"d7390833.cafea","type":"function","z":"c1dbfea7.32d2d","name":"","func":"if (msg.payload === \"stop\" && context.get(\"running\") === true) {\n    context.set(\"stop\", true);\n} else if (context.get(\"running\") !== true) {\n    context.set(\"running\",true);\n    const timestampStart = msg.payload;\n    everySecond = setInterval(()=>{\n        if (context.get(\"stop\") === true) {\n            const ellapsed = Date.now() - timestampStart;\n            node.send({payload:ellapsed});\n            context.set(\"stop\",false);\n            context.set(\"running\",false);\n            clearInterval(everySecond);\n        }\n    },1000);\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":420,"y":220,"wires":[["37537273.e9a946"]]},{"id":"37537273.e9a946","type":"debug","z":"c1dbfea7.32d2d","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":590,"y":220,"wires":[]}]

Be aware this is a very rough example of the some concepts and something i threw together in five minutes.
This is the function:

if (msg.payload === "stop" && context.get("running") === true) {
    context.set("stop", true);
} else if (context.get("running") !== true) {
    context.set("running",true);
    const timestampStart = msg.payload;
    everySecond = setInterval(()=>{
        if (context.get("stop") === true) {
            const ellapsed = Date.now() - timestampStart;
            node.send({payload:ellapsed});
            context.set("stop",false);
            context.set("running",false);
            clearInterval(everySecond);
        }
    },1000);
}
return null;

If you have questions ask me, but this should give you something to start with.

Johannes

PS:
really read the links i posted. You maybe don’t really need the setInterval at all as you can just save the timestamp on start and than calculate the difference on a sto message all without a set interval function. The advantage of using setInterval or any tick based timing mechanism is that you could send elapsed time updates while still running.

I must have spent several days trying to understand the "contexts" page to no avail. I could never get any of it to work, nor anything from the N-R "writing functions" page. The author's writing style is way beyond my level of comprehension at this point. Now you're tossing me a "=>" symbol, which I had to go look up :upside_down_face: That's how far behind I am!

I tried your flow, it sends nothing to the debug window until stopped, which may be what you intended. I'm prolly not making myself clear. I want, lets say a debug node, to continually write the time difference between one inject node's timestamp (start) and the other inject node's stop command at an adjustable user-defined rate. Hence my excitement concerning the setInterval() method. I'll study on your code in the meantime.

Thanks!

Russ

if (msg.payload === "stop" && context.get("running") === true) {
    context.set("stop", true);
} else if (context.get("running") !== true) {
    context.set("running",true);
    const timestampStart = Date.now();
    everySecond = setInterval(()=>{
        if (context.get("stop") === true) {
            const ellapsed = Date.now() - timestampStart;
            node.send({payload:ellapsed});
            context.set("stop",false);
            context.set("running",false);
            clearInterval(everySecond);
        } else {
            node.send({payload:Date.now()-timestampStart});
        }
    },1000);
}
return null;

Just add the extra else with the one line:

node.send({payload:Date.now()-timestampStart});

to the interval function and it will send the time since start on every second. Keep in mind that this will not be accurate in the millisecond area and especially setInterval has an accumulative error that will be worse the longer it runs as the code also takes some milliseconds to run and due to how the single threaded node.js Event loop works it doesn’t guarantee that a setTimeout or setInterval will actually be called after the exact time they were set too but instead when the event loop gets to them.
This is especially true for setInterval as it can accumulate over time.

Johannes

Johannes,

I "Billy-Bobbed" your code a bit to get exactly what I want, the output now goes to a label as msg.topic and behaves just like the stopwatch in my 1st post. Can't thank you enough for sticking with me! :star_struck: :star_struck: :star_struck:

var aVar
if (msg.payload === "stop" && context.get("running") === true) {
    context.set("stop", true);
} else if (context.get("running") !== true) {
    context.set("running",true);
    const timestampStart = msg.payload;
    everySecond = setInterval(()=>{
        if (context.get("stop") === true) {
            const ellapsed = ((Date.now() - timestampStart)/1000).toFixed(2);
            node.send({payload:ellapsed});
            context.set("stop",false);
            context.set("running",false);
            clearInterval(everySecond);
        } else {
            aVar = ((Date.now()-timestampStart)/1000).toFixed(2);
            node.send({payload:aVar});
        }
    },100);
}
return null;

Here's the flow for anyone else wanting Johanne's stopwatch:

[{"id":"e076a3b0.b94978","type":"inject","z":"c199b9b6.017a2","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":460,"y":1340,"wires":[["77cb7238.0744ac"]]},{"id":"2e3384f1.d9bfec","type":"inject","z":"c199b9b6.017a2","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"stop","payloadType":"str","x":450,"y":1380,"wires":[["77cb7238.0744ac"]]},{"id":"77cb7238.0744ac","type":"function","z":"c199b9b6.017a2","name":"","func":"var aVar\nif (msg.payload === \"stop\" && context.get(\"running\") === true) {\n    context.set(\"stop\", true);\n} else if (context.get(\"running\") !== true) {\n    context.set(\"running\",true);\n    const timestampStart = msg.payload;\n    everySecond = setInterval(()=>{\n        if (context.get(\"stop\") === true) {\n            const ellapsed = ((Date.now() - timestampStart)/1000).toFixed(2);\n            node.send({payload:ellapsed});\n            context.set(\"stop\",false);\n            context.set(\"running\",false);\n            clearInterval(everySecond);\n        } else {\n            aVar = ((Date.now()-timestampStart)/1000).toFixed(2);\n            node.send({payload:aVar});\n        }\n    },100);\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":620,"y":1340,"wires":[["1a2ca2d0.ebf355"]],"info":"var aVar\nif (msg.payload === \"stop\" && context.get(\"running\") === true) {\n    context.set(\"stop\", true);\n} else if (context.get(\"running\") !== true) {\n    context.set(\"running\",true);\n    const timestampStart = msg.payload;\n    everySecond = setInterval(()=>{\n        if (context.get(\"stop\") === true) {\n            const ellapsed = ((Date.now() - timestampStart)/1000).toFixed(2);\n            node.send({payload:ellapsed});\n            context.set(\"stop\",false);\n            context.set(\"running\",false);\n            clearInterval(everySecond);\n        } else {\n            aVar = ((Date.now()-timestampStart)/1000).toFixed(2);\n            node.send({payload:aVar});\n        }\n    },100);\n}\nreturn null;"},{"id":"1a2ca2d0.ebf355","type":"change","z":"c199b9b6.017a2","name":"msg Swap","rules":[{"t":"move","p":"payload","pt":"msg","to":"topic","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":1380,"wires":[["19d1cfe7.04ab7"]]},{"id":"19d1cfe7.04ab7","type":"ui_text","z":"c199b9b6.017a2","group":"36e6edd6.dd93b2","order":2,"width":2,"height":1,"name":"","label":"","format":"{{msg.topic}}","layout":"row-spread","x":780,"y":1350,"wires":[]},{"id":"36e6edd6.dd93b2","type":"ui_group","z":"","name":"Label Spacer","tab":"81831aab.f2f568","order":4,"disp":false,"width":"6","collapse":false},{"id":"81831aab.f2f568","type":"ui_tab","z":"","name":"GTO-500 Gate Opener","icon":"dashboard","disabled":false,"hidden":false}]

Thanks again!

Russ

Happy that it works for you.
It would be more accurate if you use a recursive setTimeout that can compensate drift but that might over complicate things for now.

Yes sir, it certainly would be (more complicated). This is simply timing the "swing" of a driveway gate. Nothing critical, although I did see there were routines for better accuracy...

Russ