Groov EPIC Performance Issue

Hi Team, I was hoping to get some general advice on performance and speeds. We currently have 8 charts running on PAC control and very little code in node-RED, which is just logging to a SQL database. I stopped PAC Control and the speed went from 98% down to 84%, but typically without PAC control strategy running, the system sits at 60% (give or take 4%).

Can anyone advise me what the problem might be? Happy to provide any logs necessary. I did check through my logs and didn’t see anything too fishy, but I don’t know particularly what I’m looking for either.

Stopping Node-RED while running PAC Control makes the CPU usage go from 97% down to 80%.

I wonder if you have checked out my blog this week?
Sounds like it might be worth a look at your charts…

3 Likes

@jacksonlance02 your question has been tugging at some lose strings in my brain the past few days…
My EPIC sits at about 12% CPU most of the time.
With about 10 to 12 test charts running (with delays) its about 18%
I also have about 20 Node-RED flows doing different things.

Are you running groov View on this EPIC?

Do you have shell access turned on in your EPIC?
If so, what does the command htop show?
Im wondering why your CPU is so high with it at rest.

1 Like

I can’t say I know what I’m looking at, but it doesn’t look good regardless.

I will clarify a few things:

We are running Groov View and most of the gadgets running on Groov View are being derived from the I/O, as most of our I/O is just analog inputs that don’t go through PAC Control first.

We are also running Node-RED for logging to a My-SQL database located on a NAS server elsewhere in the network. We also use Node-RED to scan for I/O changes and has Telegram messaging to notify of these changes.

We run 8 charts on PAC Control, which I have since added delays to - this seemed to make a decent difference however it still performs in the late 10’s of % (as opposed to the early 20’s, before adding delays).

I am in the middle of figuring out a better logging method because I’m not sure that Node-RED is the best option for this, we have far too much data that needs to be logged, and Node-RED can’t keep up. We do however need to keep using Node-RED for communications from Denkovi I/O Boards and Emergency Audio Announcements.

After implementing the Delay changes to PAC, my total CPU usage has dropped to sit around 75% now which is incredible. It looks like the issue is Node-RED (rather, the way I have constructed it).

Update: I have cut all useless crap out of Node-RED that has kind of just been lurking in the background from previous trials and tribulations, and now Node-RED performs much better, but overall system performance CPU usage sits around 65%, and Node-RED is reporting 35%-45% CPU usage.

Because of the three different Systems we use, I am interacting with I/O a lot (Groov View, Node-RED, PAC Control). Would this be the main CPU greed here? There would be a lot of crossover. Granted I have given very generous Delays throughout PAC Control and Node-RED has generous inject intervals where I have the control over the inject rate, I’m thinking the I/O Read Nodes I am using to scan for changes within in our I/O units would be causing some grief.

Apologies that this post has turned into a novel, would just prefer to get every known vector out on to paper.

1 Like

In Node Red, do you have multiple I/O read nodes for the same data type?(int32,float,ai….) basically one read node per value?
Or do you have a single read node for each value type that returns an array of values?

1 Like

one read node per i/o… for example, I have circa 30 Analog Input 4-20maH temperature sensors and they all have their own read nodes. I would love to learn how to make this more modular by returning an array? I didn’t know that was possible with the I/O nodes.

This is a small example of the messaging function using Read Nodes, when the change is triggered it sends a message via telegram.

1 Like

@jacksonlance02 Im not sure it is possible as I use snap read nodes, these can return an array, I have never used nodes for epic, we will see what @Beno says.

1 Like

Thanks for the htop screenshot (a little wider would have been fantastic - note for next time).
Also thanks for the Node-RED example - more on that shortly.

I marked up your HTOP so we can break it down a bit…

1. My gut experience tells me that a load average of 5.0 is pretty high, which is telling you nothing you don’t know - hence this post in the first place! But its a nice sanity check for both of us.
This is a hard number to get ones head around, but if you want to take a deep dive into all things HTOP, this is one read you are going to need to take your time with: htop explained | peteris.rocks

2. Yup, Node-RED is top of the CPU list. @torchard will be along shortly with some suggestions once he and I go over your post and screenshots.

3. I think this is the ‘self scanning’ I/O nodes. That is something I am going to focus on with Terry. They are hard coded to scan at once a second and that’s a bit high since you seem to have quite a few of them and they probably don’t need to be all updating at once a second.

4. I think is groov View (your screenshot cuts off some important names on the far right).
You mention that you have one or more I/O units in groov View because you just need to get the data and don’t need it going via PAC Control. This is good. No problem there.
My question is, do you need to poll those I/O points at the default once a second?

Can you back this off? Even once every 2 or three seconds would make a big difference.
Do you have more than one I/O unit configured in groov View?
Are they all setup to scan at once a second? Can any of them be different rates?
You get the idea.

5. The MMP scanner is the Linux process that is connecting the CPU to the I/O. Its busy because of all the Node-RED I/O nodes (once a second) and the groov View I/O units (once a second) and your PAC Control charts - now with added speed delays < grin >.
As you tweak Node-RED and groov View, this will calm down.

6. I think this is the REST sever (screenshot cut off). Those Node-RED I/O nodes are polling via the REST server and so this is pretty busy doing all that authentication. MMP is quicker. I will flag that to mention to Terry.

7. I could be wrong, but I think the MMP scanner is split into digital and analog. Hence two of these processes.
You can see whats going on with the scan times via groov Manage I/O

8 and 9. Is two of your 64 charts. I know you may not have 64 running charts, but if you look at HTOP you will see 64 instances of SoftPAC, any charts that are not running are still listed, but of course not taking up much / any CPU.
This 17% and 11.6% CPU is not horrible, but also not great. I am glad that adding delays has sped things up for you. Be sure and have a delay in every path through every chart. Often times delays are only added to condition branches or like my blog, super obvious looping condition branches and main loops through the chart are forgotten. This 17% & 11.6% is telling me you still have some chart paths that might benefit from a delay review.

10. This is nice chart. Only 4.8% CPU. Good delays in this one.

Ok, bit of novel to chew through.
Hope it helps and will get Terry to add his Node-RED speed tips to this thread.

BTW, I have a long running task to rewrite the ‘best practices’ and ‘optimization’ docs into one and focus on the groov family devices in said re-write, so your topic is another reminder that I really need to get that job done!

5 Likes

The groov i/o read nodes do support reading an entire module at once, but the input node that your using is only per-channel.
Each channel really needs its own deadband, and since the input node relies on a deadband, you can only do one at a time.

There is a work around though! You would definitely benefit from reducing the volume and frequency of REST requests with these nodes, so just do that one module request, break out the separate channels, and deadband them separately.
The “tricky” part is splitting up the payload array into individual flow wires, but you can set a function node to have as many outputs as your module has channels.

Here’s an example I threw together for a 12 channel module:

The function node has 12 outputs set in the Setup tab, for however many channels the module has. Then, the code in the “separate” function node:

var payloadList = [];

for(var i in msg.payload)
    payloadList.push({payload: msg.payload[i].value});

return payloadList;

This takes the array of objects, goes through each and grabs the value property, and adds that to an array as a { "payload" : <value> } object. By returning that array it will send the individual objects through each output port.

Once each channel is on it’s own wire, you can deadband them, process them, and do whatever else you need individually, while doing way less REST requests for the same amount of data.

If you want to use this flow as a baseline to see how it works, here’s the import JSON:

[{"id":"6fbf2e97a94b4290","type":"function","z":"2ea736ebb17b6e9b","name":"separate","func":"var payloadList = [];\n\nfor(var i in msg.payload)\n    payloadList.push({payload: msg.payload[i].value});\n\nreturn payloadList;","outputs":12,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":1020,"wires":[["b85ced26e97c31f2"],["f4a0f4c38c9b0a51"],["6e4ae0c38bfaee5d"],["61e9272afc4f356f"],["2759aa78834a8655"],["fc340f7b00194e56"],["df6c0898bd4e99a1"],["35e58ad59ffe580e"],["66125c6553f64ad1"],["1055ab6c7a8fefd6"],["782612c2fc4ca21f"],["a4e808a7808603b7"]],"outputLabels":["0","1","2","3","4","5","6","7","8","9","10","11"]},{"id":"f0515ed8da983aef","type":"groov-io-read","z":"2ea736ebb17b6e9b","device":"fb014acc80dd41d6","dataType":"module-analog","moduleIndex":"3","channelIndex":"","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"","x":380,"y":1020,"wires":[["6fbf2e97a94b4290"]]},{"id":"b85ced26e97c31f2","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":800,"wires":[["21483370b1e94e71"]]},{"id":"a4e808a7808603b7","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1240,"wires":[["f57cd2000da79763"]]},{"id":"f4a0f4c38c9b0a51","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":840,"wires":[["cf60db75f576dc55"]]},{"id":"6e4ae0c38bfaee5d","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":880,"wires":[["840048bdbb671dea"]]},{"id":"61e9272afc4f356f","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":920,"wires":[["da5387227def7ebd"]]},{"id":"2759aa78834a8655","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":960,"wires":[["2260140094b7d5c2"]]},{"id":"fc340f7b00194e56","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1000,"wires":[["ae4ab772e6f46824"]]},{"id":"df6c0898bd4e99a1","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1040,"wires":[["87e2a9ececd0b6a6"]]},{"id":"35e58ad59ffe580e","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1080,"wires":[["def439d78051e614"]]},{"id":"66125c6553f64ad1","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1120,"wires":[["a97422e29804b36f"]]},{"id":"1055ab6c7a8fefd6","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1160,"wires":[["59bdbd72638dcd3a"]]},{"id":"782612c2fc4ca21f","type":"rbe","z":"2ea736ebb17b6e9b","name":"","func":"deadbandEq","gap":"0.01","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":800,"y":1200,"wires":[["f6d26872c7632d69"]]},{"id":"4d76a6d6f2cee87d","type":"inject","z":"2ea736ebb17b6e9b","name":"","props":[],"repeat":"3","crontab":"","once":true,"onceDelay":"1","topic":"","x":210,"y":1020,"wires":[["f0515ed8da983aef"]]},{"id":"21483370b1e94e71","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":800,"wires":[["0f37d1be202d06e1"]]},{"id":"f57cd2000da79763","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1240,"wires":[["0c3ee39fc00f7397"]]},{"id":"cf60db75f576dc55","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":840,"wires":[["80c8f784d9bcbd1b"]]},{"id":"840048bdbb671dea","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":880,"wires":[["b36f9923891be168"]]},{"id":"da5387227def7ebd","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":920,"wires":[["b62cade9a958ed3d"]]},{"id":"2260140094b7d5c2","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":960,"wires":[["4b64f8278a05a3ac"]]},{"id":"ae4ab772e6f46824","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1000,"wires":[["e906f3e8b882db8a"]]},{"id":"87e2a9ececd0b6a6","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1040,"wires":[["d37ba266d205de39"]]},{"id":"def439d78051e614","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1080,"wires":[["8083df9537a2bcfa"]]},{"id":"a97422e29804b36f","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1120,"wires":[["3d31282a8b26ebee"]]},{"id":"59bdbd72638dcd3a","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1160,"wires":[["f51a172121ad77cf"]]},{"id":"f6d26872c7632d69","type":"change","z":"2ea736ebb17b6e9b","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1200,"wires":[["05858ce63bce1676"]]},{"id":"0f37d1be202d06e1","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 1","mode":"link","links":[],"x":1075,"y":800,"wires":[]},{"id":"0c3ee39fc00f7397","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 12","mode":"link","links":[],"x":1075,"y":1240,"wires":[]},{"id":"80c8f784d9bcbd1b","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 2","mode":"link","links":[],"x":1075,"y":840,"wires":[]},{"id":"b36f9923891be168","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 3","mode":"link","links":[],"x":1075,"y":880,"wires":[]},{"id":"b62cade9a958ed3d","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 4","mode":"link","links":[],"x":1075,"y":920,"wires":[]},{"id":"4b64f8278a05a3ac","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 5","mode":"link","links":[],"x":1075,"y":960,"wires":[]},{"id":"e906f3e8b882db8a","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 6","mode":"link","links":[],"x":1075,"y":1000,"wires":[]},{"id":"d37ba266d205de39","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 7","mode":"link","links":[],"x":1075,"y":1040,"wires":[]},{"id":"8083df9537a2bcfa","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 8","mode":"link","links":[],"x":1075,"y":1080,"wires":[]},{"id":"3d31282a8b26ebee","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 9","mode":"link","links":[],"x":1075,"y":1120,"wires":[]},{"id":"f51a172121ad77cf","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 10","mode":"link","links":[],"x":1075,"y":1160,"wires":[]},{"id":"05858ce63bce1676","type":"link out","z":"2ea736ebb17b6e9b","name":"link out 11","mode":"link","links":[],"x":1075,"y":1200,"wires":[]},{"id":"fb014acc80dd41d6","type":"groov-io-device","address":"localhost","msgQueueFullBehavior":"DROP_OLD"}]
4 Likes

Thank you both heaps for your help and suggestions - I have implemented some of these changes and the performance has increased a lot - however we are still sitting around 40% at idle. I will keep trying to make changes and optimize performance.

2 Likes

Given the database logging you are doing, Im not sure you going to get it much lower.
I think you have done really well, going from around mid 90’s CPU to 40’s.

By all means keep looking and if you find any tips, please drop back and let us know so we can consider putting them in the ‘Performance Optimization’ doc we are working on.

4 Likes

Coming back to this…

Noticing this chromium line has been sitting at the top of this screen for a few days now, from my understanding this has something to do with the groov Touch Screen, right?

We do not have any external screen connected via HDMI and we barely use the little touch screen that is on the opto system itself. What might be causing this?

I’m not in front of a system right now to compare.
Do you have the display timeout set? ie, does it blank after x minutes?
I’m not sure that will ‘switch off Chrome’ per say, but it might help. Or do you have a small groov View project running that you need to access on the front screen?

The other thing I will mention that I am going to dig into is that some processes seem to use some CPU when, in reality, the Linux kernel is allowing that process to share ‘nice,’ and so it’s not as scary as it seems.
I need to double check how to read HTOP as per the link I posted a few back up the page and see just how much of an impact this Chome process might be really having.

2 Likes

Those ‘change’ nodes in your example are just placeholders for [do some post-processing], right? This post has been extremely helpful.

Welcome to the forums, Stephen!

And yes, those are just placeholders for “processing”, you can reconfigure or replace them however you need.

I’m glad this was helpful! Feel free to share more about how you’re using this in your application, it might be useful to others that find this thread.

1 Like

Hello Terry, thanks for all the video help as well as here in the forums; your guys’ candor here has made things way easier as this has been my first rodeo in this realm.
I’ve been building a DAQ system using groov View and Node-RED for a laboratory testing/control setup. Here’s the json for the ‘daq and logging’ flow that this thread helped me build (stripped of anything proprietary); I need to read in an arbitrary number of analog input modules for thermocouple readings (two in this flow, but we’ll be adding more), bundle/format the values, and pipe the collection out to a few destinations including a csv on the epic.

(I know I can just download the pen data from the trend plots in groov View, but this saves some monotonous formatting; the trends are a nice backup if someone forgets to hit ‘record’)

In contrast to the example above, I don’t want a deadband applied, since we want to log every data point at a set interval. The ‘heartbeat’ sets a configurable sampling interval at startup, with inject nodes to drive it faster or slower.

(json in next response; hits character limit)



json part 1

[{"id": "40211f9928c67e8d","type": "catch","z": "237432cea239de8b","name": "","scope": null,"uncaught": false,"x": 200,"y": 40,"wires": [["af03db4d701e4904"]]},{"id": "af03db4d701e4904","type": "debug","z": "237432cea239de8b","name": "","active": true,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 295,"y": 40,"wires": [],"l": false},{"id": "12135eef93ef97af","type": "comment","z": "237432cea239de8b","name": "Stephen Gordinier, Mar. 2026","info": "","x": 480,"y": 40,"wires": []},{"id": "211e8f87b320af41","type": "group","z": "237432cea239de8b","name": "DAQ from Analog Input Modules","style": {"label": true},"nodes": ["7a7c333c85a5f5df","78eed138404245f3","5ec2edf91401996a","71ad442ea0fa6363","b5ad4d4eebcec615","ad50188e126d7485","bf79086287db5be0","924b6fc0a1c26ee2","325e9dda97913335","deaf68c4121b276b","235b4fce058cb8d5","24caa6c2b24b721d","ad3f5464606415e0","311f0bcde7d0da65","7ebdeaf256124911","869b696705f10ed9","1669f03e1a34333a","50446d328630d38b","ea5e9ee5c4d60c44","56f58928da4dbf5b","74d4d54ba06ed4ba","93c99afc2b6b504b","d4a255351cac9a81"],"x": 134,"y": 359,"w": 1612,"h": 502},{"id": "7a7c333c85a5f5df","type": "comment","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Arbitrary # of Input Modules","info": "","x": 620,"y": 600,"wires": []},{"id": "78eed138404245f3","type": "groov-io-read","z": "237432cea239de8b","g": "211e8f87b320af41","device": "d918290744a0a7b4","dataType": "module-analog","moduleIndex": "1","channelIndex": "","mmpAddress": "0xF0D81000","mmpType": "int32","mmpLength": "1","mmpEncoding": "ascii","value": "","valueType": "msg.payload","itemName": "","name": "Module 1 Read","x": 620,"y": 480,"wires": [["bf79086287db5be0","24caa6c2b24b721d","93c99afc2b6b504b"]]},{"id": "5ec2edf91401996a","type": "csv","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Convert Message to CSV","spec": "rfc","sep": ",","hdrin": "","hdrout": "none","multi": "mult","ret": "\\r\\n","temp": "","skip": "0","strings": true,"include_empty_strings": true,"include_null_values": true,"x": 1270,"y": 780,"wires": [["deaf68c4121b276b","71ad442ea0fa6363"]]},{"id": "71ad442ea0fa6363","type": "file","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Append Data to CSV","filename": "daqfilename","filenameType": "msg","appendNewline": false,"createDir": false,"overwriteFile": "false","encoding": "none","x": 1540,"y": 800,"wires": [[]]},{"id": "b5ad4d4eebcec615","type": "function","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Build Payload for CSV","func": "var csv = {};\n\n// Set up headers with first column containing timestamp in local 24h time.\nconst time = new Date(msg.timestamp);\nconst timestamp = time.getFullYear() + '-' +\n ('00' + (time.getMonth()+1)).slice(-2) + '-' +\n ('00' + time.getDate()).slice(-2) + ' ' +\n ('00' + time.getHours()).slice(-2) + ':' +\n ('00' + time.getMinutes()).slice(-2) + ':' +\n ('00' + time.getSeconds()).slice(-2);\n\nconst headers = [\"timestamp\"];\nconst pad = n => String(n).padStart(2, '0');\n\nfor (let i = 1; i <= 2; i++) {\n for (let j = 1; j <= 12; j++) {\n headers.push(TC_${i}${pad(j)});\n }\n}\n\n// Ensure payload is an array of length 24\n// Normalize to exactly 24 entries (pad with empty if needed)\nconst values = Array.isArray(msg.payload)\n ? msg.payload\n : (typeof msg.payload === \"string\" ? msg.payload.split(\",\") : []);\nconst normalized = Array.from({ length: 24 }, (_, i) => {\n const v = values[i];\n if (v === \"\" || v === null || v === undefined) return \"\";\n const num = Number(v);\n return isNaN(num) ? \"\" : num;\n});\n\n// Build key:value object\nconst payloadObj = {};\npayloadObj[\"timestamp\"] = timestamp;\n\nheaders.slice(1).forEach((key, idx) => {\n payloadObj[key] = normalized[idx];\n});\n\n// Assign to csv.payload\ncsv.payload = {};\ncsv.payload = payloadObj;\ncsv.daqfilename = global.get(\"daqfilename\");\n\nreturn csv;\n","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 1280,"y": 660,"wires": [["924b6fc0a1c26ee2","311f0bcde7d0da65","74d4d54ba06ed4ba"]]},{"id": "ad50188e126d7485","type": "join","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Combine Module Readings","mode": "custom","build": "string","property": "payload","propertyType": "msg","key": "payload","joiner": ",","joinerType": "str","useparts": true,"accumulate": false,"timeout": "1.5","count": "24","reduceRight": false,"reduceExp": "","reduceInit": "","reduceInitType": "num","reduceFixup": "","x": 1250,"y": 600,"wires": [["b5ad4d4eebcec615","ad3f5464606415e0"]]},{"id": "bf79086287db5be0","type": "function","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Separate TCs Per Module","func": "// Discussion of setting up batch module read and splitting method:\n// https://forums.opto22.com/t/groov-epic-performance-issue/5759/9\n\n// (Since we care about logging every data point, we don't want the deadband filter he mentions.)\n// Reading the entire module is why we use the \"groov i/o read\" node instead of the \"groov i/o input\"\n// node, which is only for watching a single point for changes.\n\nvar returnobj = [];\nconst timestamp = msg.timestamp;\n\n// The module index comes in as a string in the msg from the groov node\nconst tcmodule = parseInt(msg.body.moduleIndex);\n\n// Each analog input module has 12 inputs\nfor (let i = 0; i <= 11; i++) {\n returnobj[i] = {\n // If the reading is null, leave it null, otherwise trim to 3 decimal places\n payload: (msg.payload[i].value === null) ? null : parseFloat(msg.payload[i].value.toFixed(3)),\n timestamp: msg.timestamp,\n // Add msg.parts so the join node can combine readings from all modules into one big message\n // 'count' should be 12 * number of analog input modules we're aggregating\n parts: {\n id: timestamp,\n index: 12*(tcmodule-1)+i,\n count: 24 \n }\n };\n}\n\nreturn returnobj;\n","outputs": 12,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 870,"y": 520,"wires": [["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"],["ad50188e126d7485","7ebdeaf256124911"]]},{"id": "924b6fc0a1c26ee2","type": "debug","z": "237432cea239de8b","g": "211e8f87b320af41","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 1455,"y": 640,"wires": [],"l": false},{"id": "325e9dda97913335","type": "inject","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Manual Acq Ping","props": [{"p": "timestamp","v": "","vt": "date"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","x": 400,"y": 500,"wires": [["78eed138404245f3"]]},{"id": "deaf68c4121b276b","type": "debug","z": "237432cea239de8b","g": "211e8f87b320af41","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 1455,"y": 760,"wires": [],"l": false},{"id": "235b4fce058cb8d5","type": "switch","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Acq Pause Gate","property": "pauseacq","propertyType": "global","rules": [{"t": "false"}],"checkall": "true","repair": false,"outputs": 1,"x": 400,"y": 460,"wires": [["78eed138404245f3"]]},{"id": "24caa6c2b24b721d","type": "debug","z": "237432cea239de8b","g": "211e8f87b320af41","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 775,"y": 400,"wires": [],"l": false},{"id": "ad3f5464606415e0","type": "debug","z": "237432cea239de8b","g": "211e8f87b320af41","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 1455,"y": 580,"wires": [],"l": false},{"id": "311f0bcde7d0da65","type": "link out","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Broadcast Array of TC Readings","mode": "link","links": ["8615ae63dc9a4e71"],"x": 1570,"y": 680,"wires": [],"l": true},{"id": "7ebdeaf256124911","type": "function","z": "237432cea239de8b","d": true,"g": "211e8f87b320af41","name": "Individual Reading [Unused]","func": "const idx = msg.parts.index;\n\nconst tcmodule = Math.floor(idx / 12) + 1;\nconst channel = (idx % 12) + 1;\nconst pad = n => String(n).padStart(2, '0');\nmsg.topic = TC${tcmodule}_${pad(channel)};\ndelete msg.parts;\n\nreturn msg;\n\n// Works, just disabling because not currently needed","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 1240,"y": 440,"wires": [["869b696705f10ed9","1669f03e1a34333a"]]},{"id": "869b696705f10ed9","type": "debug","z": "237432cea239de8b","g": "211e8f87b320af41","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 1455,"y": 420,"wires": [],"l": false},{"id": "1669f03e1a34333a","type": "link out","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Broadcast Individual TC Reading","mode": "link","links": [],"x": 1580,"y": 460,"wires": [],"l": true},{"id": "50446d328630d38b","type": "link in","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Rcv Heartbeat","links": ["c677f7b715ec076a"],"x": 230,"y": 460,"wires": [["235b4fce058cb8d5"]],"outputLabels": ["Heartbeat"],"icon": "font-awesome/fa-heartbeat","l": true},{"id": "ea5e9ee5c4d60c44","type": "link in","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Rcv CSV Headers on UI Click ","links": ["53d73dd6b46821f7"],"x": 1260,"y": 820,"wires": [["71ad442ea0fa6363"]],"l": true},{"id": "56f58928da4dbf5b","type": "groov-io-read","z": "237432cea239de8b","g": "211e8f87b320af41","device": "d918290744a0a7b4","dataType": "module-analog","moduleIndex": "2","channelIndex": "","mmpAddress": "0xF0D81000","mmpType": "int32","mmpLength": "1","mmpEncoding": "ascii","value": "","valueType": "msg.payload","itemName": "","name": "Module 2 Read","x": 620,"y": 560,"wires": [["bf79086287db5be0"]]},{"id": "74d4d54ba06ed4ba","type": "switch","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Recording Gate","property": "recording","propertyType": "global","rules": [{"t": "true"}],"checkall": "true","repair": false,"outputs": 1,"x": 1300,"y": 720,"wires": [["5ec2edf91401996a"]]},{"id": "93c99afc2b6b504b","type": "delay","z": "237432cea239de8b","g": "211e8f87b320af41","name": "","pauseType": "delay","timeout": ".5","timeoutUnits": "seconds","rate": "1","nbRateUnits": "1","rateUnits": "second","randomFirst": ".2","randomLast": ".5","randomUnits": "seconds","drop": false,"allowrate": false,"outputs": 1,"x": 600,"y": 520,"wires": [["56f58928da4dbf5b"]]},{"id": "d4a255351cac9a81","type": "comment","z": "237432cea239de8b","g": "211e8f87b320af41","name": "Configure count for # of Modules","info": "","x": 1250,"y": 560,"wires": []},{"id": "d918290744a0a7b4","type": "groov-io-device","address": "localhost","msgQueueFullBehavior": "DROP_OLD"},{"id": "d92a097fa9a08dee","type": "group","z": "237432cea239de8b","name": "Initialization and Heartbeat","style": {"label": true},"nodes": ["c677f7b715ec076a","c9ac1706b803376e","963bb44a150ddec0","926b6a6b736adc27","d958ae42891cf0a2","bbf7509a2d55d2fa","f897bc90879b242d","81835d50fa278961","1875004269cced91","87fdb57618e4d702","5ea8951783f48d68","6ae6740aab8630a6","328a4f3d030440ba","27658fc32aca95ec"],"x": 134,"y": 79,"w": 1212,"h": 262},{"id": "c677f7b715ec076a","type": "link out","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Broadcast Heartbeat","mode": "link","links": ["23388e7b952fcd18","50446d328630d38b","108d7208f3f696a9","34c7b80131f2a75a"],"x": 1220,"y": 220,"wires": [],"inputLabels": ["Heartbeat"],"icon": "font-awesome/fa-heartbeat","l": true},{"id": "c9ac1706b803376e","type": "comment","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Polling/Refresh Rate for all Flows","info": "","x": 910,"y": 260,"wires": []},{"id": "963bb44a150ddec0","type": "debug","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 1135,"y": 260,"wires": [],"l": false},{"id": "926b6a6b736adc27","type": "link out","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Broadcast Wakeup","mode": "link","links": [],"x": 1230,"y": 160,"wires": [],"icon": "font-awesome/fa-coffee","l": true},{"id": "d958ae42891cf0a2","type": "function","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Initialize Global Setpoints and Mappings","func": "// This function runs once on boot, and can be re-triggered by the reset inject button.\n\n// Output message will have a timestamp from the boot signal (first \"heartbeat\")\n// as well as the msg.delay property that sets the heartbeat rate (in milliseconds).\n// This node is followed by a brief delay node to allow initial startup before heartbeat continues.\nlet obj = {\n \"boot\": true,\n \"timestamp\": msg.payload,\n \"delay\" : 2000\n};\n\n// Timestamp of boot signal is first \"heartbeat\" set in global context\n// TODO: Heartbeat being a global variable was necessary for TPO workaround.\n// with timeprop v2.0 release (thanks to me raising this issue!), consider removal.\n// Default state is to NOT be recording to .csv\n// TODO: Consider persistence so no data is skipped on an unintentional reboot?\nglobal.set(\"heartbeat\", obj.timestamp); \nglobal.set(\"heartbeatinterval\", obj.delay);\nglobal.set(\"recording\", false); \n\n// Path in groov EPIC to store/receive DAQ recording .CSV files.\nglobal.set(\"daqpath\", \"/home/dev/unsecured/\");\n\n// Since the filename is displayed in dashboard UI, need a placeholder if not yet set.\n// Filename is populated with timestamp at instant DAQ button is pressed in UI.\n// (See Dashboard tab)\nif (global.get(\"daqfilename\") === undefined || global.get(\"daqfilename\") === null) {\n global.set(\"daqfilename\", \"[No filename set in recent memory.]\");\n}\n\n// Default state is to not simulate TCs\nglobal.set(\"simdaq\", false); \n\n// Default state is to not poll analog inputs\nglobal.set(\"pauseacq\", true); \n\n// Default state is to not drive real digital outputs\nglobal.set(\"W1\", { \"sim\": true }); \nglobal.set(\"W2\", { \"sim\": true });\nglobal.set(\"W3\", { \"sim\": true });\nglobal.set(\"W4\", { \"sim\": true });\nglobal.set(\"W5\", { \"sim\": true });\nglobal.set(\"W6\", { \"sim\": true });\n\n// Global mapping of thermocouples to input channels and red/yellow limits.\n// Between the numbers, channels, and labels, this is three names for each data point. That's too much.\nglobal.set(\"tcmap\", {\n \"TC1\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 0, \"label\": \"TC_1_01\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC2\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 1, \"label\": \"TC_1_02\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC3\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 2, \"label\": \"TC_1_03\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC4\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 3, \"label\": \"TC_1_04\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC5\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 4, \"label\": \"TC_1_05\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC6\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 5, \"label\": \"TC_1_06\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC7\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 6, \"label\": \"TC_1_07\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC8\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 7, \"label\": \"TC_1_08\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC9\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 8, \"label\": \"TC_1_09\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC10\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 9, \"label\": \"TC_1_10\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC11\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 10, \"label\": \"TC_1_11\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC12\": { \"description\": \"Example TC Description\", \"module\": 1, \"channel\": 11, \"label\": \"TC_1_12\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC13\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 4, \"label\": \"TC_2_05\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC14\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 5, \"label\": \"TC_2_06\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC15\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 6, \"label\": \"TC_2_07\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC16\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 7, \"label\": \"TC_2_08\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC17\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 8, \"label\": \"TC_2_09\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC18\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 9, \"label\": \"TC_2_10\", \"redmin\": null, \"yelmin\": null, \"yelmax\": null, \"redmax\": null},\n \"TC19\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 10, \"label\": \"TC_2_11\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99},\n \"TC20\": { \"description\": \"Example TC Description\", \"module\": 2, \"channel\": 11, \"label\": \"TC_2_12\", \"redmin\": -99, \"yelmin\": -50, \"yelmax\": 50, \"redmax\": 99}\n});\n\nconst alarms = {\n \"TC2\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC3\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC4\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC5\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC15\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC16\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC17\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC19\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false },\n \"TC20\": { \"redminflag\": false, \"yelminflag\": false, \"yelmaxflag\": false, \"redmaxflag\": false }\n};\n\nglobal.set(\"alarms\", alarms);\n\n// Global mapping of which heater circuit (WX) is controlled by which thermocouple (TCY), as well as setpoints and power levels per operating regime.\n// For zone-based control, this (and the downstream) will need a refactor.\n// These are strings on purpose, even though it could be an indexed array.\nconst heatermap = {\n \"W1\": { \"module\": 0, \"channel\": 0, \"tc\": \"TC3\", \"srv-lo\": -48, \"srv-hi\": -40, \"nom-lo\": -30, \"nom-hi\": -25, \"bal-lo\": -30, \"bal-hi\": -25 },\n \"W2\": { \"module\": 0, \"channel\": 1, \"tc\": \"TC5\", \"srv-lo\": -48, \"srv-hi\": -40, \"nom-lo\": -30, \"nom-hi\": -25, \"bal-lo\": -30, \"bal-hi\": -25 },\n \"W3\": { \"module\": 0, \"channel\": 2, \"tc\": \"TC19\", \"srv-lo\": -48, \"srv-hi\": -40, \"nom-lo\": -30, \"nom-hi\": -25, \"bal-lo\": -30, \"bal-hi\": -25 },\n \"W4\": { \"module\": 0, \"channel\": 3, \"tc\": \"TC11\", \"srv-lo\": -48, \"srv-hi\": -40, \"nom-lo\": false, \"nom-hi\": false, \"bal-lo\": false, \"bal-hi\": false, \"nom-pwr\": 1.00, \"hbl-pwr\": 1.00, \"cbl-pwr\": 0.80, \"srv-pwr\": false },\n \"W5\": { \"module\": 0, \"channel\": 4, \"tc\": \"TC12\", \"srv-lo\": -48, \"srv-hi\": -40, \"nom-lo\": false, \"nom-hi\": false, \"bal-lo\": false, \"bal-hi\": false, \"nom-pwr\": 0.90, \"hbl-pwr\": 1.00, \"cbl-pwr\": 0.80, \"srv-pwr\": false },\n \"W6\": { \"module\": 0, \"channel\": 5, \"tc\": \"TC10\", \"srv-lo\": -48, \"srv-hi\": -40, \"nom-lo\": false, \"nom-hi\": false, \"bal-lo\": false, \"bal-hi\": false, \"nom-pwr\": 0.80, \"hbl-pwr\": 0.80, \"cbl-pwr\": 0.80, \"srv-pwr\": false }\n};\n\nglobal.set(\"heatermap\", heatermap);\n\n// Default cycle at boot \nglobal.set(\"cycle\", \"Survival\");\n\nreturn obj;","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 540,"y": 160,"wires": [["926b6a6b736adc27","81835d50fa278961","87fdb57618e4d702"]]},{"id": "bbf7509a2d55d2fa","type": "trigger","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Heartbeat","op1": "","op2": "0","op1type": "pay","op2type": "str","duration": "-10","extend": false,"overrideDelay": true,"units": "s","reset": "","bytopic": "all","topic": "topic","outputs": 1,"x": 840,"y": 220,"wires": [["f897bc90879b242d"]]},{"id": "f897bc90879b242d","type": "function","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Set Timestamp","func": "// This is triggered every heartbeat.\n\nlet obj = {\n \"timestamp\": new Date()\n};\n\nglobal.set(\"heartbeat\", obj.timestamp);\n\nreturn obj;","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 1000,"y": 220,"wires": [["963bb44a150ddec0","c677f7b715ec076a"]]},{"id": "81835d50fa278961","type": "debug","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Booted","active": true,"tosidebar": true,"console": false,"tostatus": false,"complete": "\"WOKE UP AT \" & $now()","targetType": "jsonata","statusVal": "","statusType": "auto","x": 755,"y": 120,"wires": [],"l": false},{"id": "1875004269cced91","type": "inject","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Wakeup, Reset","props": [{"p": "payload"}],"repeat": "","crontab": "","once": true,"onceDelay": 0.1,"topic": "","payload": "","payloadType": "date","x": 260,"y": 160,"wires": [["d958ae42891cf0a2"]],"info": "This inject node runs once on deploy/boot \r\nand triggers the OnInitialization function."},{"id": "87fdb57618e4d702","type": "delay","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Delay after wake-up before heartbeat starts","pauseType": "delay","timeout": "2","timeoutUnits": "seconds","rate": "1","nbRateUnits": "1","rateUnits": "second","randomFirst": "1","randomLast": "5","randomUnits": "seconds","drop": false,"allowrate": false,"outputs": 1,"x": 685,"y": 220,"wires": [["bbf7509a2d55d2fa"]],"l": false},{"id": "5ea8951783f48d68","type": "function","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "1/2","func": "\nlet interval = global.get(\"heartbeatinterval\");\n\nlet newinterval = Math.max(250, 0.5 * interval);\n\nglobal.set(\"heartbeatinterval\", newinterval);\n\nmsg.delay = newinterval;\n\nreturn msg;","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 650,"y": 260,"wires": [["bbf7509a2d55d2fa"]]},{"id": "6ae6740aab8630a6","type": "inject","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Decrease Sample Interval (Min: 250ms)","props": [{"p": "payload"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","payload": "true","payloadType": "bool","x": 330,"y": 260,"wires": [["5ea8951783f48d68"]]},{"id": "328a4f3d030440ba","type": "inject","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "Increase Sample Interval (Max: 60s)","props": [{"p": "payload"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","payload": "true","payloadType": "bool","x": 320,"y": 300,"wires": [["27658fc32aca95ec"]]},{"id": "27658fc32aca95ec","type": "function","z": "237432cea239de8b","g": "d92a097fa9a08dee","name": "2","func": "\nlet interval = global.get(\"heartbeatinterval\");\n\nlet newinterval = Math.min(64000,2*interval);\n\nglobal.set(\"heartbeatinterval\",newinterval);\n\nmsg.delay = newinterval;\n\nreturn msg;","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 650,"y": 300,"wires": [["bbf7509a2d55d2fa"]]}]

the rest (character limit):

json part 2

[{"id": "6c3e9e0287fe1784","type": "group","z": "237432cea239de8b","name": "Get (Simulated or Real) Thermocouple Readings Tied to Heater Circuits or Alarms","style": {"label": true},"nodes": ["8615ae63dc9a4e71","f350af7a03d07717","c8ed3b4df96d5bc3","d16ef59994fdafef","a6af4260312a3c45","23388e7b952fcd18","71fdd557421d3050","ca6761b60f1068ed","c735ef93f485562e","3e531f1c130d280b","2e8943069b989bd5","68646834139d0216","296302f18eeb7891","9311d767c243051f"],"x": 134,"y": 884,"w": 1178,"h": 563},{"id": "8615ae63dc9a4e71","type": "link in","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Rcv Array of TC Readings","links": ["311f0bcde7d0da65","a0492458e8bf3288"],"x": 270,"y": 1200,"wires": [["d16ef59994fdafef"]],"l": true},{"id": "f350af7a03d07717","type": "switch","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Sim Gate","property": "simdaq","propertyType": "global","rules": [{"t": "true"}],"checkall": "true","repair": false,"outputs": 1,"x": 460,"y": 1000,"wires": [["c8ed3b4df96d5bc3","71fdd557421d3050"]]},{"id": "c8ed3b4df96d5bc3","type": "function","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Sim Data","func": "// ----- configuration for simulated thermocouple sine waves -----\n\nconst heatermap = global.get(\"heatermap\"); // Looks like [{W1: TC3}, ...]\n\nconst simvars = global.get(\"simTCs\");\n\nfunction simsine(max, min, period, t) {\n return ((max - min) / 2) * Math.sin((2 * Math.PI * t) / period) +\n ((max + min) / 2);\n}\n\nconst outputs = new Array(12);\n\nfor (let i = 0; i < 12; i++) {\n let outMsg = {};\n const cfg = simvars[i];\n\n outMsg.timestamp = msg.timestamp;\n outMsg.payload = simsine(cfg.max, cfg.min, cfg.period, (outMsg.timestamp/1000)+(cfg.phaseshift*cfg.period));\n outMsg.topic = cfg.tc; // e.g. TC3\n outMsg.htr = cfg.htr; // e.g. W1\n outMsg.simdaq = true;\n \n outputs[i] = outMsg;\n}\n\n// Outgoing message from real or sim will both have timestamp, payload, topic, htr, simdaq\n\nreturn outputs;","outputs": 12,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 680,"y": 1000,"wires": [["ca6761b60f1068ed","c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"]]},{"id": "d16ef59994fdafef","type": "switch","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Acq Gate","property": "simdaq","propertyType": "global","rules": [{"t": "false"}],"checkall": "true","repair": false,"outputs": 1,"x": 460,"y": 1200,"wires": [["a6af4260312a3c45","71fdd557421d3050"]]},{"id": "a6af4260312a3c45","type": "function","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Acq Data","func": "// ----- configuration of actual thermocouple data channels -----\n\nconst heatermap = global.get(\"heatermap\"); // Looks like [ W1: { module: 0, channel: 0, tc: TC3, ... }, W2: {}, ... ]\nconst tcmap = global.get(\"tcmap\"); // Looks like [ TC1: { module: 1, channel: 0, label: TC_1_01, ... }, TC2: {}, ... ]\n\n// Note for reference:\n// heatermap.W1.tc\n// and\n// heatermap[\"W1\"].tc\n// are functionally identical. dot notation is more readable.\n// use dot notation when key (in this case W1) has no spaces, no hyphens, and does not start with a number.\n// Must use bracket notation when key contains special characters or if key is result of expression\n// (such as heatermap.W1.tc producing the key for tcmap.???.label).\n\n// There's a programmatic way to populate this list from the initial setpoints, I'm sure, but doing it manually for now.\nconst acqvars = [\n { htr: \"W1\", tc: heatermap.W1.tc, label: tcmap[heatermap.W1.tc].label},\n { htr: \"W2\", tc: heatermap.W2.tc, label: tcmap[heatermap.W2.tc].label},\n { htr: \"W3\", tc: heatermap.W3.tc, label: tcmap[heatermap.W3.tc].label},\n { htr: \"W4\", tc: heatermap.W4.tc, label: tcmap[heatermap.W4.tc].label},\n { htr: \"W5\", tc: heatermap.W5.tc, label: tcmap[heatermap.W5.tc].label},\n { htr: \"W6\", tc: heatermap.W6.tc, label: tcmap[heatermap.W6.tc].label },\n { htr: null, tc: \"TC2\", label: tcmap[\"TC2\"].label },\n { htr: null, tc: \"TC4\", label: tcmap[\"TC4\"].label },\n { htr: null, tc: \"TC15\", label: tcmap[\"TC15\"].label },\n { htr: null, tc: \"TC16\", label: tcmap[\"TC16\"].label },\n { htr: null, tc: \"TC17\", label: tcmap[\"TC17\"].label },\n { htr: null, tc: \"TC20\", label: tcmap[\"TC20\"].label },\n\n];\n\nconst outputs = new Array(12);\n\nfor (let i = 0; i < 12; i++) {\n let outMsg = {};\n const cfg = acqvars[i];\n\n outMsg.timestamp = msg.payload.timestamp;\n outMsg.payload = msg.payload[cfg.label]; // gets temp from (e.g.) key TC_1_01 in incoming payload array\n outMsg.topic = cfg.tc; // e.g. TC3\n outMsg.htr = cfg.htr; // e.g. W1\n outMsg.simdaq = false;\n \n outputs[i] = outMsg;\n}\n\n// Outgoing message from real or sim will both have timestamp, payload, topic, htr, simdaq\n\nreturn outputs;","outputs": 12,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 680,"y": 1200,"wires": [["ca6761b60f1068ed","c735ef93f485562e"],["ca6761b60f1068ed","c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"],["c735ef93f485562e"]]},{"id": "23388e7b952fcd18","type": "link in","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Rcv Heartbeat","links": ["c677f7b715ec076a","6987c742d31afe98"],"x": 310,"y": 1000,"wires": [["f350af7a03d07717"]],"icon": "font-awesome/fa-heartbeat","l": true},{"id": "71fdd557421d3050","type": "debug","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "msg","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 575,"y": 1100,"wires": [],"l": false},{"id": "ca6761b60f1068ed","type": "debug","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 825,"y": 940,"wires": [],"l": false},{"id": "c735ef93f485562e","type": "function","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Format Output","func": "let obj = {\n \"timestamp\" : msg.timestamp,\n \"payload\" : msg.payload, // temperature\n \"topic\" : msg.topic, // e.g. TC3\n \"htr\" : msg.htr, // e.g. W4\n \"simdaq\" : msg.simdaq, // true/false\n \"simhtr\" : global.get(${msg.htr}.sim) // true/false\n}\n\nreturn obj; ","outputs": 1,"timeout": 0,"noerr": 0,"initialize": "","finalize": "","libs": [],"x": 900,"y": 1100,"wires": [["3e531f1c130d280b","2e8943069b989bd5"]]},{"id": "3e531f1c130d280b","type": "link out","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "Broadcast Tracked TCs","mode": "link","links": ["e2e1f724e3f7517f","1dc272a42703e3b7","a8a84d46dbd7fb9f"],"x": 1130,"y": 1100,"wires": [],"icon": "font-awesome/fa-thermometer-3","l": true},{"id": "2e8943069b989bd5","type": "debug","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "msg","active": false,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 1035,"y": 1060,"wires": [],"l": false},{"id": "68646834139d0216","type": "group","z": "237432cea239de8b","g": "6c3e9e0287fe1784","style": {"stroke": "#999999","stroke-opacity": "1","fill": "none","fill-opacity": "1","label": true,"label-position": "nw","color": "#a4a4a4"},"nodes": ["021d61d61866e63c","8d27ebc85dba4e77","22d58655e1dee58d","d7bf05cf2de331c9"],"x": 854,"y": 1219,"w": 432,"h": 202},{"id": "021d61d61866e63c","type": "comment","z": "237432cea239de8b","g": "68646834139d0216","name": "This config: Three TCs linked to heaters and alarms,","info": "","x": 1070,"y": 1260,"wires": []},{"id": "8d27ebc85dba4e77","type": "comment","z": "237432cea239de8b","g": "68646834139d0216","name": "three linked to heaters only, six linked to alarms only.","info": "","x": 1070,"y": 1300,"wires": []},{"id": "22d58655e1dee58d","type": "comment","z": "237432cea239de8b","g": "68646834139d0216","name": "Total of 12 tracked TCs, with remaining eight used for","info": "","x": 1070,"y": 1340,"wires": []},{"id": "d7bf05cf2de331c9","type": "comment","z": "237432cea239de8b","g": "68646834139d0216","name": "logging only (when recording is toggled on).","info": "","x": 1050,"y": 1380,"wires": []},{"id": "296302f18eeb7891","type": "comment","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "(Rapid-sends every heartbeat,","info": "","x": 1140,"y": 1140,"wires": []},{"id": "9311d767c243051f","type": "comment","z": "237432cea239de8b","g": "6c3e9e0287fe1784","name": "once for each tracked TC)","info": "","x": 1130,"y": 1180,"wires": []},{"id": "fd3721aa8929efb8","type": "group","z": "237432cea239de8b","name": "Toggle Simulated TCs and Analog Polling","style": {"label": true},"nodes": ["6cf1067490e7dc6c","6f7d67f214f1dd87","b293f752bd190303","99fa51744566bf66","6f1c345c90672416","5760834369c13362","ce66d8d7c404826b","ec5575badfe747ed","fcd74680a25236a3","783d219a6e4a50e9","bd50a38703830116","7f0b5e268b1d3e19","6dfbfb7de876a254","c4a4e0cd49bd16b7","23db6951023752b8","a08829286c42de74","08b4c1e951ac997b"],"x": 1334,"y": 879,"w": 992,"h": 422},{"id": "6cf1067490e7dc6c","type": "comment","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "1. Not Getting Readings, Not Simulating Them [DEFAULT STATE]","info": "","x": 1590,"y": 960,"wires": []},{"id": "6f7d67f214f1dd87","type": "comment","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "3. Not Getting Readings, Simulating Them","info": "","x": 1520,"y": 1040,"wires": []},{"id": "b293f752bd190303","type": "comment","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "2. Getting Readings, Not Simulating Them","info": "","x": 1520,"y": 1000,"wires": []},{"id": "99fa51744566bf66","type": "comment","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Possible States:","info": "","x": 1440,"y": 920,"wires": []},{"id": "6f1c345c90672416","type": "inject","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Start Analog Module Polling","props": [{"p": "payload"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","payload": "false","payloadType": "bool","x": 1520,"y": 1100,"wires": [["ce66d8d7c404826b"]]},{"id": "5760834369c13362","type": "inject","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Pause Analog Module Polling","props": [{"p": "payload"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","payload": "true","payloadType": "bool","x": 1520,"y": 1140,"wires": [["ce66d8d7c404826b"]]},{"id": "ce66d8d7c404826b","type": "switch","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "","property": "payload","propertyType": "msg","rules": [{"t": "true"},{"t": "false"}],"checkall": "true","repair": false,"outputs": 2,"x": 1730,"y": 1140,"wires": [["ec5575badfe747ed"],["fcd74680a25236a3","c4a4e0cd49bd16b7"]]},{"id": "ec5575badfe747ed","type": "change","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Acq TCs Disabled","rules": [{"t": "set","p": "pauseacq","pt": "global","to": "true","tot": "bool"}],"action": "","property": "","from": "","to": "","reg": false,"x": 1930,"y": 1120,"wires": [["a08829286c42de74","08b4c1e951ac997b"]]},{"id": "fcd74680a25236a3","type": "change","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Acq TCs Enabled","rules": [{"t": "set","p": "pauseacq","pt": "global","to": "false","tot": "bool"}],"action": "","property": "","from": "","to": "","reg": false,"x": 1930,"y": 1160,"wires": [["a08829286c42de74","08b4c1e951ac997b"]]},{"id": "783d219a6e4a50e9","type": "inject","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Start Simulated Thermocouples","props": [{"p": "payload"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","payload": "true","payloadType": "bool","x": 1510,"y": 1220,"wires": [["7f0b5e268b1d3e19"]]},{"id": "bd50a38703830116","type": "inject","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Stop Simulated Thermocouples","props": [{"p": "payload"}],"repeat": "","crontab": "","once": false,"onceDelay": 0.1,"topic": "","payload": "false","payloadType": "bool","x": 1510,"y": 1260,"wires": [["7f0b5e268b1d3e19"]]},{"id": "7f0b5e268b1d3e19","type": "switch","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "","property": "payload","propertyType": "msg","rules": [{"t": "true"},{"t": "false"}],"checkall": "true","repair": false,"outputs": 2,"x": 1730,"y": 1220,"wires": [["6dfbfb7de876a254","ec5575badfe747ed"],["c4a4e0cd49bd16b7"]]},{"id": "6dfbfb7de876a254","type": "change","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Sim TCs Enabled","rules": [{"t": "set","p": "simdaq","pt": "global","to": "true","tot": "bool"}],"action": "","property": "","from": "","to": "","reg": false,"x": 1930,"y": 1200,"wires": [["a08829286c42de74","08b4c1e951ac997b"]]},{"id": "c4a4e0cd49bd16b7","type": "change","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Sim TCs Disabled","rules": [{"t": "set","p": "simdaq","pt": "global","to": "false","tot": "bool"}],"action": "","property": "","from": "","to": "","reg": false,"x": 1930,"y": 1240,"wires": [["a08829286c42de74","08b4c1e951ac997b"]]},{"id": "23db6951023752b8","type": "link in","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Rcv Toggle from UI","links": ["bdb4d69c22f0ce94"],"x": 1550,"y": 1180,"wires": [["7f0b5e268b1d3e19","ce66d8d7c404826b"]],"l": true},{"id": "a08829286c42de74","type": "link out","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "Broadcast Sim TC State","mode": "link","links": ["28cf9bc8036d411b"],"x": 2190,"y": 1180,"wires": [],"l": true},{"id": "08b4c1e951ac997b","type": "debug","z": "237432cea239de8b","g": "fd3721aa8929efb8","name": "","active": true,"tosidebar": true,"console": false,"tostatus": false,"complete": "true","targetType": "full","statusVal": "","statusType": "auto","x": 2095,"y": 1120,"wires": [],"l": false}]