Sample flow for exercising node-red-contrib-pid
A sample flow for demonstrating the PID loop controller node-red-contrib-pid using a process simulation consisting of a 3 second transport delay followed by 10 second and 30 second lags and an offset.
A guide to tuning a process controlled by a PID controller, which uses this flow as an example to demonstrate the prinicples can be found at https://blog.clanlaw.org.uk/pid-loop-tuning.html
[{"id":"b4973237.46ab6","type":"subflow","name":"Process Simulation","info":"","in":[{"x":37,"y":103,"wires":[{"id":"ec719d4d.0d54f8"}]}],"out":[{"x":728.5,"y":294,"wires":[{"id":"ae1a6e5.d4c0d9","port":0}]}]},{"id":"7fe4b5c3.32e58c","type":"function","z":"b4973237.46ab6","name":"30 sec RC + 20","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 30*1000; // time constant in milliseconds\n\nvar lastValue = context.get('lastValue');\nif (typeof lastValue == \"undefined\") lastValue = msg.payload;\nvar lastTime = context.get('lastTime') || null;\nvar now = new Date();\nvar currentValue = msg.payload;\nif (lastTime === null) {\n // first time through\n newValue = currentValue;\n} else {\n var dt = now - lastTime;\n var newValue;\n \n if (dt > 0) {\n var dtotc = dt / tc;\n newValue = lastValue * (1 - dtotc) + currentValue * dtotc;\n } else {\n // no time has elapsed leave output the same as last time\n newValue = lastValue;\n }\n}\ncontext.set('lastValue', newValue);\ncontext.set('lastTime', now);\n\nmsg.payload = newValue + 20;\nreturn msg;","outputs":1,"noerr":0,"x":626.5,"y":207,"wires":[["ae1a6e5.d4c0d9"]]},{"id":"1bacd004.9753c","type":"inject","z":"b4973237.46ab6","name":"Inject -0.2 at start","topic":"","payload":"-0.2","payloadType":"num","repeat":"","crontab":"","once":true,"x":134.5,"y":30,"wires":[["ec719d4d.0d54f8"]]},{"id":"999a52c2.f465f","type":"function","z":"b4973237.46ab6","name":"10 sec RC","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 10*1000; // time constant in milliseconds\n\nvar lastValue = context.get('lastValue');\nif (typeof lastValue == \"undefined\") lastValue = msg.payload;\nvar lastTime = context.get('lastTime') || null;\nvar now = new Date();\nvar currentValue = msg.payload;\nif (lastTime === null) {\n // first time through\n newValue = currentValue;\n} else {\n var dt = now - lastTime;\n var newValue;\n \n if (dt > 0) {\n var dtotc = dt / tc;\n newValue = lastValue * (1 - dtotc) + currentValue * dtotc;\n } else {\n // no time has elapsed leave output the same as last time\n newValue = lastValue;\n }\n}\ncontext.set('lastValue', newValue);\ncontext.set('lastTime', now);\n\nmsg.payload = newValue;\nreturn msg;","outputs":1,"noerr":0,"x":451,"y":207,"wires":[["7fe4b5c3.32e58c"]]},{"id":"ec719d4d.0d54f8","type":"delay","z":"b4973237.46ab6","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":268,"y":104,"wires":[["ede39236.1961f8"]]},{"id":"a823c9cf.2a6178","type":"function","z":"b4973237.46ab6","name":"2 msg transport delay","func":"// stores messages in a fifo until the specified number have been received, \n// then releases them as new messages are received.\n// during the filling phase the earliest message is passed on each time \n// a message is received, but it is also left in the fifo\nvar fifoMaxLength = 2;\nvar fifo = context.get('fifo') || [];\n// push the new message onto the top of the array, messages are shifted down and\n// drop off the front\nvar length = fifo.push(msg); // returns new length\nif (length > fifoMaxLength) {\n newMsg = fifo.shift();\n} else {\n // not full yet, make a copy of the msg and pass it on\n var newMsg = JSON.parse(JSON.stringify(fifo[0]));\n}\ncontext.set('fifo', fifo);\nreturn newMsg;","outputs":1,"noerr":0,"x":258,"y":208,"wires":[["999a52c2.f465f"]]},{"id":"ae1a6e5.d4c0d9","type":"function","z":"b4973237.46ab6","name":"Clear all except payload","func":"msg2 = {payload: msg.payload};\nreturn msg2;","outputs":1,"noerr":0,"x":545,"y":293,"wires":[[]]},{"id":"ede39236.1961f8","type":"range","z":"b4973237.46ab6","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"scale","round":false,"name":"","x":87,"y":208,"wires":[["a823c9cf.2a6178"]]},{"id":"e0cb2a8c.4402a8","type":"PID","z":"f0414dd0.dd514","name":"","setpoint":"50","pb":"22","ti":"24","td":"6","integral_default":"0","smooth_factor":"0","max_interval":600,"enable":"1","disabled_op":"0","x":362,"y":175,"wires":[["5ec4239b.34e174","e94f888a.130a88"]]},{"id":"f70949c8.aab988","type":"change","z":"f0414dd0.dd514","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"op","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":601.5,"y":250,"wires":[["9f58a52d.076dd"]]},{"id":"2792f805.066c78","type":"inject","z":"f0414dd0.dd514","name":"Setpoint 30","topic":"setpoint","payload":"30","payloadType":"num","repeat":"","crontab":"","once":false,"x":104,"y":198,"wires":[["e0cb2a8c.4402a8"]]},{"id":"1bf3ce16.c3ae02","type":"inject","z":"f0414dd0.dd514","name":"Setpoint 80","topic":"setpoint","payload":"80","payloadType":"num","repeat":"","crontab":"","once":false,"x":102.5,"y":247,"wires":[["e0cb2a8c.4402a8"]]},{"id":"f69bbf05.bdd49","type":"inject","z":"f0414dd0.dd514","name":"enable","topic":"enable","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"x":95,"y":83,"wires":[["e0cb2a8c.4402a8"]]},{"id":"143523b2.2f165c","type":"inject","z":"f0414dd0.dd514","name":"disable","topic":"enable","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"x":95.5,"y":133,"wires":[["e0cb2a8c.4402a8"]]},{"id":"5ec4239b.34e174","type":"subflow:b4973237.46ab6","z":"f0414dd0.dd514","x":365,"y":102,"wires":[["de738fc5.75e4c8","e0cb2a8c.4402a8"]]},{"id":"de738fc5.75e4c8","type":"change","z":"f0414dd0.dd514","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"pv","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":586,"y":101,"wires":[["9f58a52d.076dd"]]},{"id":"e94f888a.130a88","type":"range","z":"f0414dd0.dd514","minin":"0","maxin":"1","minout":"0","maxout":"25","action":"scale","round":false,"name":"Scale power","x":427,"y":250,"wires":[["f70949c8.aab988"]]},{"id":"9f58a52d.076dd","type":"ui_chart","z":"f0414dd0.dd514","name":"","group":"c45a83a3.d00908","order":0,"width":"6","height":"6","label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"100","removeOlder":"3","removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#cf0005","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"x":791,"y":175,"wires":[[],[]]},{"id":"bef0a1f8.2e2cf","type":"inject","z":"f0414dd0.dd514","name":"Clear chart on deploy","topic":"","payload":"{\"data\":[]}","payloadType":"json","repeat":"","crontab":"","once":true,"x":352,"y":318,"wires":[["9079ffaf.4a096"]]},{"id":"9079ffaf.4a096","type":"change","z":"f0414dd0.dd514","name":"","rules":[{"t":"move","p":"payload.data","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":588,"y":318,"wires":[["9f58a52d.076dd"]]},{"id":"c45a83a3.d00908","type":"ui_group","z":"","name":"PID","tab":"80cd4062.93a5","disp":true,"width":"6"},{"id":"80cd4062.93a5","type":"ui_tab","z":"","name":"Home","icon":"dashboard"}]