Persistent values in Node Red?

Context: Light programming being done in Node Red on a RIO. HMI is Ignition.

I have a Node Red where I am using the scratchpad to allow Ignition to access values that need to be read and written. The biggest problem so far is that Node Red doesn’t persist any values on power cycle.

I found this thread: Groov Rio node-red contextStore persistence it sounds like this is not really a supported method for persistence. Is there a supported method for persistence?

Alternatively I could write some default values to the scratchpad for set points and such, then update the scratch pad from Ignition with the actual values once it reconnects. Is there a way to insure that a Flow or a part of flow runs first and once on boot?

I am new to Node Red, so this feels clunky, but here is my code so far:
(I don’t think I would want to use Node Red for much programming, but CoDeSys isn’t available for the RIO (yet)…)

[{"id":"d1117df142d0a677","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"4da262bb925e6e4d","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":380,"wires":[]},{"id":"4ca5625dc0732e0a","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-digital","moduleIndex":"0","channelIndex":"8","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"cmd","valueType":"msg","name":"CMD","x":710,"y":380,"wires":[["08af0b6268a9e1fc"]]},{"id":"5ce9c4b8158daf57","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":380,"wires":[["53ad6dc8c6c139c6"]]},{"id":"53ad6dc8c6c139c6","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-analog","moduleIndex":"0","channelIndex":"3","mmpAddress":"0xF0D81004","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"Read Well Level","x":320,"y":380,"wires":[["27b66f186e972aed"]]},{"id":"4733a47c8ab6da57","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":220,"wires":[["2b1c2d4a26d1df58"]]},{"id":"2b1c2d4a26d1df58","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"10","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"Load Scratchpad Values from HMI","x":400,"y":220,"wires":[["7dd524962a7a771d"]]},{"id":"391821a9e36324f9","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1270,"y":220,"wires":[]},{"id":"57983ee5860ac6c1","type":"comment","z":"d1117df142d0a677","name":"Load Vars from Scratchpad","info":"This Flow will pull values that HMI changes in the scratchpad.","x":130,"y":160,"wires":[]},{"id":"7dd524962a7a771d","type":"function","z":"d1117df142d0a677","name":"Save HMI Values to flow.*","func":"// Functions for Bits from Stack Overflow\nfunction bit_test(num, bit){\n    return ((num>>bit) % 2 != 0)\n}\n\nfunction bit_set(num, bit){\n    return num | 1<<bit;\n}\n\nfunction bit_clear(num, bit){\n    return num & ~(1<<bit);\n}\n\nfunction bit_toggle(num, bit){\n    return bit_test(num, bit) ? bit_clear(num, bit) : bit_set(num, bit);\n}\n\n// HMI Counter:\nvar HMI_Count = flow.get('HMI_Count')||0;\nHMI_Count = msg.payload[0];\nflow.set('HMI_Count',HMI_Count);\n\n// HOA:\nvar HOA = flow.get('HOA')||1;\nHOA = msg.payload[1];\nflow.set('HOA',HOA);\n\n// ON_SP:\nvar ON_SP = flow.get('ON_SP')||17.0;\nON_SP = msg.payload[2]/10.0;\nflow.set('ON_SP',ON_SP);\n\n// OFF_SP:\nvar OFF_SP = flow.get('OFF_SP')||5.0;\nOFF_SP = msg.payload[3]/10.0;\nflow.set('OFF_SP',OFF_SP);\n\n// Speed_SP:\nvar Speed_SP = flow.get('Speed_SP')||100.0;\nSpeed_SP = msg.payload[4]/10.0;\nflow.set('Speed_SP',Speed_SP);\n\n// HMI_DI:\nvar HMI_DI = flow.get('HMI_DI')||0;\nHMI_DI = msg.payload[5];\nflow.set('HMI_DI',HMI_DI);\n\n// Reset VFD Fault\nvar VFD_Reset = flow.get('VFD_Reset')||0;\nVFD_Reset = bit_test(HMI_DI, 0);\nflow.set('VFD_Reset',VFD_Reset);\n\n// Reset Pump Runtime 1\nvar Pump_R1 = flow.get('Pump_R1')||0;\nPump_R1 = bit_test(HMI_DI, 1);\nflow.set('Pump_R1',Pump_R1);\n\n// Reset Pump Runtime 2\nvar Pump_R2 = flow.get('Pump_R2')||0;\nPump_R2 = bit_test(HMI_DI, 2);\nflow.set('Pump_R2',Pump_R2);\n\n// Reset Flow Total 1\nvar Flow_T1 = flow.get('Flow_T1')||0;\nFlow_T1 = bit_test(HMI_DI, 3);\nflow.set('Flow_T1',Flow_T1);\n\n// Reset Flow Total 2\nvar Flow_T2 = flow.get('Flow_T2')||0;\nFlow_T2 = bit_test(HMI_DI, 4);\nflow.set('Flow_T2',Flow_T2);\n\nmsg.payload = [HMI_Count,HOA,ON_SP,OFF_SP,Speed_SP,HMI_DI]\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":220,"wires":[["391821a9e36324f9"]]},{"id":"27b66f186e972aed","type":"function","z":"d1117df142d0a677","name":"Run Pump?","func":"// Level:\nvar Level = flow.get('Level')||1.0;\nvar HOA = flow.get('HOA')||1;\nvar VFD_Fault = flow.get('VFD_Fault')||false;\nvar CMD = flow.get('CMD')||false;\nvar Call = flow.get('Call')||false;\nvar ON_SP = flow.get('ON_SP')||17.0;\nvar OFF_SP = flow.get('OFF_SP')||5.0;\nvar Speed_SP = flow.get('Speed_SP')||100.0;\nLevel = msg.payload;\nflow.set('Level',Level);\n\n\n\nif (HOA == 0) {\n    CMD = false;\n} else if(VFD_Fault) {\n    CMD = false;\n} else if (HOA == 2) {\n    CMD = true;\n} else if (HOA == 1) {\n    if (Level > ON_SP) {\n        CMD = true;\n    } else if (Level < OFF_SP) {\n        CMD = false;\n    }\n}\nif (Level > ON_SP) {\n    Call = true;\n} else if (Level < OFF_SP) {\n    Call = false;\n}\nflow.set('CMD',CMD);\nflow.set('Call',Call);\n\nif (CMD) { \n    msg.speed = Speed_SP\n} else {\n    msg.speed = 0.0\n}\n\nmsg.cmd = CMD;\nmsg.level = Level * 10;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":380,"wires":[["4ca5625dc0732e0a"]]},{"id":"9684982098190457","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-digital","moduleIndex":"0","channelIndex":"6","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"Read VFD Fault","x":320,"y":300,"wires":[["cd948352edc1db67"]]},{"id":"adfc641cf5f84c6e","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":300,"wires":[["9684982098190457"]]},{"id":"3b407fce5e8fe8d2","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":300,"wires":[]},{"id":"cd948352edc1db67","type":"function","z":"d1117df142d0a677","name":"VFD Fault","func":"// Functions for Bits from Stack Overflow\nfunction bit_test(num, bit){\n    return ((num>>bit) % 2 != 0)\n}\n\nfunction bit_set(num, bit){\n    return num | 1<<bit;\n}\n\nfunction bit_clear(num, bit){\n    return num & ~(1<<bit);\n}\n\nfunction bit_toggle(num, bit){\n    return bit_test(num, bit) ? bit_clear(num, bit) : bit_set(num, bit);\n}\n\n// VFD_Fault:\nvar VFD_Fault = flow.get('VFD_Fault')||false;\nVFD_Fault = msg.payload;\nflow.set('VFD_Fault',VFD_Fault);\n\n// Pump_Info\nvar Pump_Info = flow.get('Pump_Info')||0;\nif(VFD_Fault) {\n    bit_set(Pump_Info,1)\n}\nif(VFD_Fault == 0) {\n    bit_clear(Pump_Info,1)\n}\n\nmsg.payload = VFD_Fault;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":300,"wires":[["3b407fce5e8fe8d2"]]},{"id":"b235b59428397b2c","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":100,"wires":[["a17a3d7e32c7aca2"]]},{"id":"d93c646640282293","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":100,"wires":[]},{"id":"97eefd31a39273e2","type":"function","z":"d1117df142d0a677","name":"PLC Counter","func":"// PLC_Count:\nvar PLC_Count = flow.get('PLC_Count')||0;\nPLC_Count = msg.payload;\n\nPLC_Count++;\n\nif (PLC_Count > 2000000000) {\n    PLC_Count = 0;\n}\n\nflow.set('PLC_Count',PLC_Count);\n\nmsg.payload = PLC_Count;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":100,"wires":[["4f2732ebfca6a3f8"]]},{"id":"a17a3d7e32c7aca2","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81028","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"PLC Counter","x":330,"y":100,"wires":[["97eefd31a39273e2"]]},{"id":"4f2732ebfca6a3f8","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81028","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","name":"Write PLC Count","x":730,"y":100,"wires":[["d93c646640282293"]]},{"id":"08af0b6268a9e1fc","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81030","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"level","valueType":"msg","name":"Level to HMI","x":870,"y":380,"wires":[["50608db94ce33d6e"]]},{"id":"6a1a8b97793f6464","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":500,"wires":[["2e8ff1d8ac30f535"]]},{"id":"2e8ff1d8ac30f535","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-digital","moduleIndex":"0","channelIndex":"5","mmpAddress":"0xF0D81004","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"status","valueType":"msg","itemName":"","name":"Read Pump Run Status","x":350,"y":500,"wires":[["be00435fe055d5e4"]]},{"id":"be00435fe055d5e4","type":"function","z":"d1117df142d0a677","name":"Pump Timers","func":"function bit_test(num, bit){\n    return ((num>>bit) % 2 != 0)\n}\n\nfunction bit_set(num, bit){\n    return num | 1<<bit;\n}\n\nfunction bit_clear(num, bit){\n    return num & ~(1<<bit);\n}\n\nfunction bit_toggle(num, bit){\n    return bit_test(num, bit) ? bit_clear(num, bit) : bit_set(num, bit);\n}\n\n// Status:\nvar S_Status = flow.get('Status')||false;\nvar P_Status = msg.status;\nvar Status = msg.status;\nvar StartTime = flow.get('StartTime')||0;\nvar AccTime = 0;\n\n// HMI_DI:\nvar HMI_DI = flow.get('HMI_DI')||0;\n\n// Reset Pump Runtime 1\nvar Pump_R1 = flow.get('Pump_R1')||0;\nPump_R1 = bit_test(HMI_DI, 1);\nflow.set('Pump_R1',Pump_R1);\n\n// Reset Pump Runtime 2\nvar Pump_R2 = flow.get('Pump_R2')||0;\nPump_R2 = bit_test(HMI_DI, 2);\nflow.set('Pump_R2',Pump_R2);\n\n// Other Vars\nvar P_Seconds = flow.get('P_Seconds')||0.0;\nvar THours_R0 = flow.get('Hours_R0')||0;\nvar THours_R1 = flow.get('Hours_R1')||0;\nvar THours_R2 = flow.get('Hours_R2')||0;\n\n\nif (StartTime > 0) {\n    AccTime = msg.payload - StartTime\n}\n\nif (!S_Status && P_Status) {\n    StartTime = msg.payload;\n    S_Status = true;\n} else if (S_Status && P_Status) {\n    P_Seconds = P_Seconds + AccTime / 1000;\n} else if (S_Status && !P_Status) {\n    flow.set('P_Seconds',P_Seconds);\n    StartTime = 0;\n    S_Status = false;\n}\nif (P_Seconds > 360.0) {\n    THours_R0++;\n    THours_R1++;\n    THours_R2++;\n    P_Seconds = P_Seconds - 360.0;\n    StartTime = msg.payload;\n    flow.set('THours_R0',THours_R0);\n    flow.set('THours_R1',THours_R1);\n    flow.set('THours_R2',THours_R2);\n    flow.set('P_Seconds',P_Seconds);\n    flow.set('StartTime',StartTime);\n} else if (THours_R0 > 2000000000) {\n    THours_R0 = 0;\n    flow.set('THours_R0',THours_R0);\n} else if (Pump_R1) {\n    THours_R1 = 0;\n    flow.set('THours_R1',THours_R1);\n    HMI_DI = bit_clear(HMI_DI, 1);\n} else if (Pump_R2) {\n    THours_R2 = 0;\n    flow.set('THours_R2',THours_R2);\n    HMI_DI = bit_clear(HMI_DI, 2);\n}\n\nflow.set('HMI_DI',HMI_DI);\nflow.set('Status',Status);\nmsg.hours = [THours_R0, THours_R1, THours_R2]\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":500,"wires":[["75461fc90b894bbd"]]},{"id":"50608db94ce33d6e","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-analog","moduleIndex":"0","channelIndex":"4","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"speed","valueType":"msg","name":"Set Pump Speed CMD","x":1080,"y":380,"wires":[["4da262bb925e6e4d"]]},{"id":"d51e1d1ed0963045","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":500,"wires":[]},{"id":"75461fc90b894bbd","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D8103C","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"hours[0]","valueType":"msg","name":"Hours","x":770,"y":500,"wires":[["227b4309867593de"]]},{"id":"227b4309867593de","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81040","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"hours[1]","valueType":"msg","name":"Hours","x":910,"y":500,"wires":[["cd1425aecf438fef"]]},{"id":"cd1425aecf438fef","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81044","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"hours[2]","valueType":"msg","name":"Hours","x":1050,"y":500,"wires":[["d51e1d1ed0963045"]]},{"id":"c09efcbc81408815","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":560,"wires":[["fee8f16df449eb1a"]]},{"id":"fee8f16df449eb1a","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-analog","moduleIndex":"0","channelIndex":"2","mmpAddress":"0xF0D81004","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"payload","valueType":"msg","itemName":"","name":"Read Pump Run Speed","x":350,"y":560,"wires":[["f3fa617ee9d5a10b"]]},{"id":"f3fa617ee9d5a10b","type":"function","z":"d1117df142d0a677","name":"Pump Speed","func":"// Pump_Speed:\nvar Pump_Speed = flow.get('Pump_Speed')||0;\nPump_Speed = msg.payload * 10.0;\nflow.set('Pump_Speed',Pump_Speed);\n\n\nmsg.payload\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":560,"wires":[["213afdeaa99701ef"]]},{"id":"3bf722cf69a93865","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":560,"wires":[]},{"id":"213afdeaa99701ef","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81034","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","name":"Pump Speed","x":790,"y":560,"wires":[["3bf722cf69a93865"]]},{"id":"b07ec93190189c83","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":620,"wires":[["2fec3772d772f4b4"]]},{"id":"2fec3772d772f4b4","type":"function","z":"d1117df142d0a677","name":"Pump Info Builder","func":"// Pump Bits:\nvar Status = flow.get('Status')||false;\nvar VFD_Fault = flow.get('VFD_Fault')||false;\nvar Call = flow.get('Call')||false;\nvar CMD = flow.get('CMD')||false;\nvar Pump_Info = 0;\n\n\nPump_Info = Status + (VFD_Fault << 1) + (Call << 2) + (CMD << 3)\n\n\nmsg.payload = Pump_Info\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":620,"wires":[["9237eca872020457"]]},{"id":"4ded074277f550b8","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":620,"wires":[]},{"id":"9237eca872020457","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D8102C","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","name":"Pump Info","x":770,"y":620,"wires":[["4ded074277f550b8"]]},{"id":"500cf5f3f54e8142","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":680,"wires":[["3cb04f07142cac5e"]]},{"id":"3cb04f07142cac5e","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"channel-analog","moduleIndex":"0","channelIndex":"1","mmpAddress":"0xF0D81004","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"flow","valueType":"msg","itemName":"","name":"Read Flow Meter","x":330,"y":680,"wires":[["2b12fad40dbd568b"]]},{"id":"2b12fad40dbd568b","type":"function","z":"d1117df142d0a677","name":"Flow Totals","func":"// Functions for Bits from Stack Overflow\nfunction bit_test(num, bit){\n    return ((num>>bit) % 2 != 0)\n}\n\nfunction bit_set(num, bit){\n    return num | 1<<bit;\n}\n\nfunction bit_clear(num, bit){\n    return num & ~(1<<bit);\n}\n\nfunction bit_toggle(num, bit){\n    return bit_test(num, bit) ? bit_clear(num, bit) : bit_set(num, bit);\n}\n\n// Flow: (Assuming GPM)\nvar Flow = msg.flow;\nvar Flow_T0 = flow.get('Flow_T0')||1.0;\nvar Flow_T1 = flow.get('Flow_T1')||1.0;\nvar Flow_T2 = flow.get('Flow_T2')||1.0;\n\n// HMI_DI:\nvar HMI_DI = flow.get('HMI_DI')||0;\nvar Reset1 = bit_test(HMI_DI, 3);\nvar Reset2 = bit_test(HMI_DI, 4);\n\nvar Now = msg.payload;\nvar Last = flow.get('Last')||msg.payload;\nvar msecs = Now - Last;\nvar F_Total = flow.get('F_Total')||1.0;\nvar Counts_Total = 1000.0; // == Galons per Count.\n\nif (Flow > 5.0) {\n    if (msecs > 0) {\n        weight = msecs / 1000.0;\n        sec_Total = GPM * weight / 60.0; // Divide by 60 because we assume GPM. Change for others.\n        F_Total += secTotal; \n    }\n    if (F_Total > Counts_Total) {\n        Flow_T0++;\n        Flow_T1++;\n        Flow_T2++;\n        F_Total -= Counts_Total;\n    }\n}    \n\nif (Flow_T0 > 2000000000) {\n    Flow_T0 = 0;\n}\nif (Reset1) {\n    Flow_T1 = 0;\n    HMI_DI = bit_clear(HMI_DI, 3);\n}\nif (Reset2) {\n    Flow_T2 = 0;\n    HMI_DI = bit_clear(HMI_DI, 4);\n}\n\n\n\nflow.set('F_Total',F_Total);\nflow.set('Flow_T0',Flow_T0);\nflow.set('Flow_T1',Flow_T1);\nflow.set('Flow_T2',Flow_T2);\nflow.set('HMI_DI',HMI_DI);\nmsg.totals = [Flow_T0,Flow_T1,Flow_T2]\nmsg.payload = Flow * 10;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":680,"wires":[["1c6d3caa0ceae154"]]},{"id":"85e3d8f3a0b3c812","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":680,"wires":[]},{"id":"3210d3b5c2800cf3","type":"inject","z":"d1117df142d0a677","name":"Every Second","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":440,"wires":[["717e17eb5f08ad65"]]},{"id":"717e17eb5f08ad65","type":"groov-io-read","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D8103C","mmpType":"int32","mmpLength":"6","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"Load Scratchpad Totalizers","x":360,"y":440,"wires":[["ce95301ec25d216b"]]},{"id":"7a5515285c7a1638","type":"debug","z":"d1117df142d0a677","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1250,"y":440,"wires":[]},{"id":"ce95301ec25d216b","type":"function","z":"d1117df142d0a677","name":"Save Totals to Flow","func":"// Hours_R0:\nvar Hours_R0 = flow.get('Hours_R0')||0;\nHours_R0 = msg.payload[0];\nflow.set('Hours_R0',Hours_R0);\n\n// Hours_R1:\nvar Hours_R1 = flow.get('Hours_R1')||0;\nHours_R1 = msg.payload[1];\nflow.set('Hours_R1',Hours_R1);\n\n// Hours_R2:\nvar Hours_R2 = flow.get('Hours_R2')||0;\nHours_R2 = msg.payload[2];\nflow.set('Hours_R2',Hours_R2);\n\n// Flow_T0:\nvar Flow_T0 = flow.get('Flow_T0')||0;\nFlow_T0 = msg.payload[3];\nflow.set('Flow_T0',Flow_T0);\n\n// Flow_T1:\nvar Flow_T1 = flow.get('Flow_T1')||0;\nFlow_T1 = msg.payload[4];\nflow.set('Flow_T1',Flow_T1);\n\n// Flow_T2:\nvar Flow_T2 = flow.get('Flow_T2')||0;\nFlow_T2 = msg.payload[5];\nflow.set('Flow_T2',Flow_T2);\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":440,"wires":[["7a5515285c7a1638"]]},{"id":"7924106d31c13d86","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81048","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"totals[0]","valueType":"msg","name":"Totals","x":830,"y":680,"wires":[["0739e337abf25863"]]},{"id":"0739e337abf25863","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D8104C","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"totals[1]","valueType":"msg","name":"Totals","x":970,"y":680,"wires":[["c01866a67a2dbb25"]]},{"id":"c01866a67a2dbb25","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81050","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"totals[2]","valueType":"msg","name":"Totals","x":1110,"y":680,"wires":[["85e3d8f3a0b3c812"]]},{"id":"1c6d3caa0ceae154","type":"groov-io-write","z":"d1117df142d0a677","device":"4e9fa67a17225a9e","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF0D81038","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"","valueType":"msg.payload","name":"Flow","x":690,"y":680,"wires":[["7924106d31c13d86"]]},{"id":"4e9fa67a17225a9e","type":"groov-io-device","address":"localhost","msgQueueFullBehavior":"DROP_OLD"}]

Modifying the settings file is a great way to do this, but if you can’t use that method for any reason, a great option would be to write the relevant data to a file and read it back in some time after deploy.
The built-in file nodes will be able to handle this, and having an inject node set to Inject once after X seconds, give it a few seconds to handle the NR runtime start up, will fire when Node-RED first starts up. It can then read in the file, parse the data, and then write to your setpoints.

Does that sound like a reasonable approach for your application?

Modifying the settings file sounds like a good idea, accept that based on the linked thread, it 1) isn’t supported by Opto, 2) can cause corruption of the context, when it is writing the data to persistent memory, because of an active bug on Node-Red.
This is all correct, right?

If that is all true then files is probably the way to go:
Every 5 minutes:
Read Scratchpad values 0-19.
Save to file.

On start up wait 30 seconds:
Load file.
Save to Scratchpad values 0-19.

Is that the general idea?

Thanks,
-Caleb

There is a OptoMMP address (Status Area Write) you can write to (a 3) that will save the scratchpad to flash. That is what it is for and it is what I would recommend using.

1 Like

Philip,

That sounds like what I’m looking for. Can you tell me where to find that?

-Caleb

Take a long look at the MMP protocol guide:

There is a ton of cool stuff in there.
Your answer is on page 118:

Don’t thrash the flash. Every 5 minutes like you say is fine, but it does have a write life cycle… so less is more.

1 Like

I’ll read through that. Looks like a gold mine.

Just one other question before I get too far. Is the OptoMMP Software Development Kit relevant for the RIO?

Thanks,
-Caleb

It will work with it, if that is what you are asking.

1 Like

I see what you mean. It could be used with the RIO, but if I’m using Ignition its not relevant in this situation, only would be useful if I needed to write my own program for the server.

Writing a 3 to that address does the trick. And since flash does have a limited number of writes, I’ll plan to only do it once an hour unless a significant value changed. Then all my info will be stored…

Thanks for all the help!
-Caleb

@Beno

Do you know what the mean time (writes) to failure on the flash is?
I’d like to design this to last at least 20 years.

Thanks,
-Caleb

How are you counting them?
All the Linux OS writes, Node-RED, PAC Control, Ignition, Data Services, Shell and groov View programs write to flash.

I don’t think I can count all of those, though I know I am not using PAC, Ignition, or groov View. Maybe I should state the question differently:

I have a few totalizers in Node Red, and want to capture some amount of granularity to my data in case power goes out on the RIO, so that my totals aren’t completely lost. However I also want the RIO to be functional for up to 20 years. What frequency of writes (Scratch pad to flash) would you recommend to balance these competing priorities?

Thanks, Caleb

Have you modified your Node-RED settings.js via shell yet?
Out of the box Node-RED does not store any of its values to flash.

My point about managing the flash is that their are other programs writing to flash, like the Linux OS, that will, depending on what tasks you have running on the RIO, might impact over all life span.
Its good to not put your Node-RED writes in a tight write cycle, so just asking and going over your options is a really important step.

Modifying settings via shell seems to have a few downsides:

  1. Not fully supported by Opto22 (correct me if I’m wrong about this, seemed like other customers didn’t get full support when they did this.)
  2. Requires a Shell License (I know this is free, but still is going to take time to obtain).
  3. Is not as easily repeatable as the below solution. IE: I can tell a Field Technician to load a Node Red Flow / Restore from a RIO backup very easily, but if every new install requires a Tech to modify a file via command line, that’s a bit more work.

I’m using this:
image

It seems to work great. Probably writes more info to Flash than is really needed (I really only need the first 30x 32 bit words), but will be very easy to implement.

I know I need to be careful with Flash, which is why I am asking the questions. Is there other information that you need from me to answer the following question?:

  1. I have a few totalizers in Node Red, and want to capture some amount of granularity to my data in case power goes out on the RIO, so that my totals aren’t completely lost. However I also want the RIO to be functional for up to 20 years. What frequency of writes (Scratchpad to flash) would you recommend to balance these competing priorities?

OR Is there a document that tells a) how many write cycles the flash is rated for and b) how to figure out what is writing to flash?

I should include the disclaimer that I’m not looking for certainty. That would be ridiculous. I’m trying to make sure that the probability (as far as can be known) of the flash lasting 20 years is greater than 50%.

Thanks for the help,
-Caleb

Sorry if I am coming over as evasive or hostile, I don’t mean to… It was a very big open question and I was struggling to understand where it was coming from and what the use case was… Your extra details here is very very helpful.
While I gather my thoughts, I want to ask yet another question… what about writing your totalizators to a file on a USB stick in the RIO?
You could also save any other information there as well and the tech could either yearly change the USB stick if you are worried about its life write cycle and also it could be part of the replacement process should it be required.
The Node-RED file read/write nodes are very easy to use and it puts you in the drivers seat for what gets written and how often… Just wondering if a USB drive would work for your application?

1 Like

Sorry, I know its a pretty big question, but at first didn’t realize how open it is.

I’m willing to be less granular on the totals if it guarantees flash lifetime to be about 20 years, but I still need to save setpoints to flash. If less granularity turns out to be a problem, we can add USBs later.

Thanks,
-Caleb

With changes made via SSH it’s just a matter of if you break functionality of the equipment and come to support we’ll still try to help, but you may have to use a paperclip to restore to factory defaults, update firmware, and confirm that it was the shell changes that broke it, not any kind of hardware issue. Opto supports their hardware completely, but we can’t account for all the things (good AND bad) that you can get up to with root access, but it doesn’t mean zero support after you enable shell. Just something to keep in mind.

For something as simple as editing the NR settings file I can’t imagine it would cause any issues, it’s something I’ve done a hundred times no problem, and could be done by just overwriting the file with one you could load in with the change already made, making it much easier for a tech to do themselves.

I’ll also give a +1 to Ben’s idea to use a USB drive, that would alleviate a lot of your concerns if it’s something you can look into.

There are so many other things that write to the flash more frequently than this. Looking on my EPIC there are profile logs that are written to every 15 minutes, looks about 4MB/day. I don’t think saving the scratchpad to flash once and hour will make a meaningful difference to the lifetime of the flash storage.

I don’t know which part is used in the RIO, but the EPIC PR1s part is an industrial mSATA using MLC in SLC mode which is good for around 30,000 erase cycles. With 16GB of storage (6GB available to us), that would be 468TB of writes - so even with write amplification, it will last a long time (2.7 GB/day for 20 years).

Edit: The PR1 shows the disk erase counts in manage (this hasn’t made it to the RIO). My bench unit which has been running for about 4 years now running various projects shows a disk erase count value of 198. This is less than 1% of the available erase cycles.

2 Likes