Looking for someone with PID node experience

I have a control application for the production of tubing. My problem is that the PID nodes that are currently available don't work well with a large "dead time" element. The time that it takes from a change in process control (changing the speed of the puller) to the measurement of the change is at least a minute. It is impossible to move the laser gauge any closer to the tubing extruder, so I am stuck with this limitation.
I would like to commission someone to create a custom node to address my issues.

node-red-contrib-pid should be capable of doing it as well as it is possible to do with PID control. What have you done that convinces you that it is not possible?

I've used the PID nodes that are currently available, but I've had to detune because of oscillation, it takes too long to stabilize.

What stabilisation time did you achieve?

About 20 minutes to stabilize.

A process consisting of an almost pure transport delay is not easy to control well, but you should be able to get that down to below 10 minutes using PID control if the loop is tuned correctly. This is what I can get with node-red-contrib-pid driving a rough simulation of your process consisting of a 60 second transport delay and a 5 second RC lag.

image

The orange line is the setpoint which I switched from 50 to 75, the red line is the output of the PID node, scaled so that 25 on the scale is full output, and the blue line is the process value. As you can see it is very close in about 7 minutes. I know the vertical axis does not relate to your process but that is just a scaling and offset adjustment which will only affect the PB setting.

Here is the flow if you want to play with it.

[{"id":"b4973237.46ab6","type":"subflow","name":"Process Simulation","info":"","category":"","in":[{"x":37,"y":103,"wires":[{"id":"ec719d4d.0d54f8"}]}],"out":[{"x":728.5,"y":294,"wires":[{"id":"ae1a6e5.d4c0d9","port":0},{"id":"6f1adb80.74fc94","port":0}]}],"env":[{"name":"Password","type":"env","value":"password"}],"color":"#DDAA99"},{"id":"7fe4b5c3.32e58c","type":"function","z":"b4973237.46ab6","name":"150 sec RC + 20","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 150*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":640,"y":420,"wires":[["ae1a6e5.d4c0d9"]]},{"id":"1bacd004.9753c","type":"inject","z":"b4973237.46ab6","name":"Inject -0.2 at start","repeat":"","crontab":"","once":true,"topic":"","payload":"-0.2","payloadType":"num","x":134.5,"y":30,"wires":[["ec719d4d.0d54f8"]]},{"id":"999a52c2.f465f","type":"function","z":"b4973237.46ab6","name":"50 sec RC","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 50*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":454.5,"y":420,"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":[["fb9cb6a1.22ad78"]]},{"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":261.5,"y":421,"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":548.5,"y":506,"wires":[[]]},{"id":"ede39236.1961f8","type":"range","z":"b4973237.46ab6","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"scale","round":false,"name":"","x":90.5,"y":421,"wires":[["a823c9cf.2a6178"]]},{"id":"c8c7aab6.d61bc8","type":"function","z":"b4973237.46ab6","name":"5 sec RC","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 5*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,"initialize":"","finalize":"","libs":[],"x":540,"y":180,"wires":[["6f1adb80.74fc94"]]},{"id":"d1d01d4d.78dbc","type":"function","z":"b4973237.46ab6","name":"60 msg (60 second) 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 = 60;\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,"initialize":"","finalize":"","libs":[],"x":301.5,"y":181,"wires":[["c8c7aab6.d61bc8"]]},{"id":"6f1adb80.74fc94","type":"function","z":"b4973237.46ab6","name":"Clear all except payload","func":"msg2 = {payload: msg.payload};\nreturn msg2;","outputs":1,"noerr":0,"x":548.5,"y":266,"wires":[[]]},{"id":"fb9cb6a1.22ad78","type":"range","z":"b4973237.46ab6","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"scale","round":false,"name":"","x":90.5,"y":181,"wires":[["d1d01d4d.78dbc"]]},{"id":"110f5921.74b2f7","type":"comment","z":"b4973237.46ab6","name":"Ignore these nodes they are for another simulation I have been playing with","info":"","x":300,"y":380,"wires":[]},{"id":"697a4eb0.e2a0e","type":"PID","z":"b1e1b45f.b3fbd8","name":"","setpoint":"50","pb":"240","ti":"60","td":"0","integral_default":"0.5","smooth_factor":"0","max_interval":600,"enable":"1","disabled_op":"0","x":390,"y":180,"wires":[["f1c86ac4.99902","728f61f0.af394","f7b4b0f6.fcabe8","571d9f3f.d38ab8"]]},{"id":"4a8a734a.3f37b4","type":"change","z":"b1e1b45f.b3fbd8","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"op","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":260,"wires":[["63a8ea87.f1569c"]]},{"id":"b7c59b17.064f48","type":"inject","z":"b1e1b45f.b3fbd8","name":"Setpoint 50","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"setpoint","payload":"50","payloadType":"num","x":132,"y":203,"wires":[["697a4eb0.e2a0e"]]},{"id":"cc0e3aeb.9eda8","type":"inject","z":"b1e1b45f.b3fbd8","name":"Setpoint 75","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"setpoint","payload":"75","payloadType":"num","x":130.5,"y":252,"wires":[["697a4eb0.e2a0e"]]},{"id":"f1c86ac4.99902","type":"subflow:b4973237.46ab6","z":"b1e1b45f.b3fbd8","env":[],"x":393,"y":107,"wires":[["c2f1e6e7.c82ee","697a4eb0.e2a0e"]]},{"id":"c2f1e6e7.c82ee","type":"change","z":"b1e1b45f.b3fbd8","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"pv","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":614,"y":106,"wires":[["63a8ea87.f1569c"]]},{"id":"728f61f0.af394","type":"range","z":"b1e1b45f.b3fbd8","minin":"0","maxin":"1","minout":"0","maxout":"25","action":"scale","round":false,"property":"payload","name":"Scale power 0-25 for chart","x":430,"y":260,"wires":[["4a8a734a.3f37b4"]]},{"id":"63a8ea87.f1569c","type":"ui_chart","z":"b1e1b45f.b3fbd8","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":"15","removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#cf0005","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":819,"y":180,"wires":[[]]},{"id":"c98f51a2.fb3ba","type":"inject","z":"b1e1b45f.b3fbd8","name":"Clear chart on deploy","repeat":"","crontab":"","once":true,"topic":"","payload":"{\"data\":[]}","payloadType":"json","x":380,"y":323,"wires":[["a081966c.3e3cd8"]]},{"id":"a081966c.3e3cd8","type":"change","z":"b1e1b45f.b3fbd8","name":"","rules":[{"t":"move","p":"payload.data","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":616,"y":323,"wires":[["63a8ea87.f1569c"]]},{"id":"f7b4b0f6.fcabe8","type":"debug","z":"b1e1b45f.b3fbd8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":580,"y":180,"wires":[]},{"id":"571d9f3f.d38ab8","type":"change","z":"b1e1b45f.b3fbd8","name":"Extract Setpoint","rules":[{"t":"move","p":"setpoint","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"setpoint","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":220,"wires":[["63a8ea87.f1569c"]]},{"id":"c45a83a3.d00908","type":"ui_group","name":"PID","tab":"80cd4062.93a5","order":1,"disp":true,"width":"6"},{"id":"80cd4062.93a5","type":"ui_tab","name":"Home","icon":"dashboard"}]

Thanks! I'll have a look at it. I know I didn't include enough information. There is no lag time between a change in the puller speed and a change in the tubing diameter, you just don't see the change for a minute. I compensated by limiting the PID loop to 1 per min. I'm sure this isn't best practice, but it was the only way I could get in spec tubing.

All the control loop knows is the measured value, so from the point of view of the control system there is a 60 second delay between changing the puller speed and the measured value. In fact, as you point out, the actual time for the tube dimension to stabilise is 60 seconds less than shown on the graph.

That is a disaster, I am not surprised that it didn't work well. In fact I am surprised that you managed to get it stable at all. With a process such as you describe you need to be sampling at least every 5 seconds, and preferably every second or so. If you couldn't stabilise it using a reasonable sample rate then it is almost certainly the tuning that is at fault. Which pid node are you currently using and when you tried node-red-contrib-pid what tuning terms did you try?

If you post your current flow (just the bits around the pid node) I may be able to suggest tuning constants that will give you a reasonable starting point.

The problem I ran into, the PID would be making changes for the full minute, it would massively shoot past the set point and never would settle if I didn't limit the loops. The puller speed is usually about 42ft per min, with a max speed of 50ft/m. When its near spec diameter, the speed changes are 0.01ft/m. So it has a small way to go, to go horribly wrong. It's a fairly stable process, once everything is up to temperature.

That shows that the loop was not correctly tuned. If you give me some more information about the process I will build a better simulation of it and come up with suggested tuning constants.
You said the puller speed goes up to 50 and is typically 42, can you give some typical diameter values against speed so that I can setup that relationship please.
Do you limit the speed range that the PID output can set in order that it never goes too slow? If so then what to?
Am I right in thinking that the delay is inversely proportional to the speed? So if you double the speed it halves the delay? If so does that mean the sensor is about 40ft from the machine?

[Edit] Also, about how quickly does the drive motor respond? So If you ask it to change speed does it take something like 1 second to achieve the speed, or maybe 5 or 10?

The tubing we are making measures 0.625" +-.005". The speed that we start at is 40ft/m but as things warm up, it can be as high as 44ft/m (46ft/m in summer). I've not limited the speed range. The motor drive speed changes are almost instantaneous for our purposes (it takes about 2 seconds to go from 0 to 40ft/m). The gauge is about 40ft from the extruder.
Here is a video that shows equipment similar to ours: https://youtu.be/wE_KTLlrdMA

That's all good, thanks. I need an approx figure for the gain though. So about how much diameter change would there be if the speed were increased by 1ft/sec for example?

Edit: I see the extruder in the video has Eurotherm temperature controllers. It is Eurotherm that I worked for before I retired. That must be quite an old video though, I can't believe those particular controllers are still in production.

This is going to sound sad, but our extruder has Eurotherm controllers (we bought it new about 5 years ago). Adjusting the speed by 0.1fpm changes the OD of the tubing about .005".

That's what I needed to know. I have just been looking at how to simulate the pipe in javascript, it is a very interesting problem. It is basically a FIFO buffer where new samples are added on the front and drop off the end and are sent on as the value measured by your sensor. The complication is that when they drop off the end is dependent on the current speed.

I am surprised the temperature controllers are available that still look as they did nearly 20 years ago. I assume they are vastly different inside. I would have expected an extruder such as that in the video to have an integrated control system with a touch screen to do the whole thing, but apparently not. I worked on the first touch screen controllers but they were still expensive then.

1 Like

The Youtube video that I linked wasn't of our extruder, but the one we have is a super basic one as well with just buttons for start/stop and a dial for the speed. All the automation we have is done with a bunch (5 for tube making) of raspberry PI's in the various pieces of equipment, running MQTT to communicate between them. It all has worked quite well excepting the control of diameter.


image

Understood.
I have got a simulation of the pipe working, which was fun.

Just need to link it into the PID node and see what happens. Will have to leave it till tomorrow soon though, I might not get it going this evening.

1 Like

Looking forward to see what you come up with, I can't thank you enough.

I have something working that seems to be ok, simulating your extruder/puller. This is what the flow looks like

This is what the response looks like when I ask it to go from a diameter of 0.4 to 0.625. It gets within +-0.005 in a bit under 8 minutes, though the real system would no doubt take a bit longer, real systems always do. I know changing the setpoint is not something you do, but it shows the response to a large change in conditions. The red line is the setpoint and the blue line is the measured diameter. The orange line is the power output from tjhe PID, inverted so that low speed is at the bottom, and scaled to 0:0.5 on the chart.

To see how it performs to changes in conditions, I have provided inject nodes that add an offset in to the speed so that it has to settle out to a new speed, which shows a similar response to the chart above, but it isn't quite so easy to see what is going on.

I have also provided enable and disable injects and setup the PID node so that when disabled it outputs a speed of 40 ft/min so you can use that for setup whilst loading the machine (if I interpret the system correctly). Then to start it controlling switch it to enabled. Obviously for your system you could connect those to buttons or something.

The PID node outputs a value which represents 30 to 50 ft/min. It seemed like a good idea to have that rather than 0 to 50 as the machine should never be running that slow anyway and it stops things going completely berserk if something goes wrong. You can change that if you wish, obviously. If you did change that the PB would have to be adjusted accordingly (and also the initial integral and disabled power settings).

Have a play with the simulation and see if it behaves like the real system. Then if you want to try it on the extruder you should be able to replace the Extruder subflow with one that controls your system. The input expects the payload to be the speed in ft/min and it should output the measured diameter. Obviously you won't need the Speed Offset injectors. As I said before I suggest sampling the diameter at 1 Hz. Slower that that will compromise the control a bit, but you could go a bit slower without major issues, probably.

If you do try it on the machine make sure you have a chart showing the output from PID (or the speed) and the diameter. It is impossible to tune a system without those. You should not need to touch the tuning constants except the Proportional Band. Increasing it a bit will smooth out the response but make it a bit slower to home in on the setpoint, reducing it will increase the spikes as it settles down, and if reduced too far will make it unstable. It is quite critical on the PB setting. You shouldn't have to change the Integral time as that is dictated by the transport delay. Don't use any derivative, it isn't helpful on a process like this.

Here is the flow.

[{"id":"672e3dc1.ab6844","type":"subflow","name":"Extruder","info":"","category":"","in":[{"x":50,"y":30,"wires":[{"id":"fb357619.c5e78"}]}],"out":[{"x":760,"y":240,"wires":[{"id":"c98488d.67bb7f8","port":0}]},{"x":760,"y":360,"wires":[{"id":"5f11bf23.0930a8","port":0}]},{"x":760,"y":460,"wires":[{"id":"26db29c4.53870e","port":0}]}],"env":[],"meta":{},"color":"#DDAA99","status":{"x":180,"y":200,"wires":[{"id":"fb357619.c5e78","port":1}]}},{"id":"d2e53a07.d7da6","type":"function","z":"672e3dc1.ab6844","name":"Extruder simulation","func":"/** Extruder simulation\n * Given a speed (ft/min) in msg.payload\n * Calculates the pipe diameter\n * Returns diameter in msg.payload.value and\n * speed, in ft/sec in msg.payload.speed\n*/\n// uses diameter = m*x + c\n// these are estimated from 40 ft/min gives 0.625 and\n// an increase of 0.1 ft/min decreased d by 0.005\nconst m = -0.05\nconst c = 2.625\n\nmsg.payload = {value: m * msg.payload + c, speed: msg.payload/60}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":140,"wires":[["c98488d.67bb7f8","5f11bf23.0930a8"]]},{"id":"da2aaa3f.af0d88","type":"inject","z":"672e3dc1.ab6844","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"0.2","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"","payloadType":"str","x":420,"y":180,"wires":[["c98488d.67bb7f8"]]},{"id":"c98488d.67bb7f8","type":"function","z":"672e3dc1.ab6844","name":"Pipe simulation","func":"/**\n * Pass in topic: \"trigger\" to advance the pipe, otherwise payload should contain speed and (diameter) value\n * speed should be in ft/sec\n * msg.payload always returned with an array of measured values that have been dropped off the end\n * or no reply if none \n */\n \nconst maxSampleIntervalTime = 1500 \nlet speed = context.get(\"speed\")   // current speed of travel in units/second\nlet pipe = context.get(\"pipe\")\nlet lastTime = context.get(\"lastTime\") || new Date().getTime()\nconst now = new Date().getTime()\nlet samples = []\n\n// advance the pipe based on last time and speed\n//node.warn(`Advancing by ${speed * (now - lastTime)/1000}`)\nsamples = pipe.advance(speed * (now - lastTime)/1000)\n// pass on any that have dropped off the end\nfor (let i=0; i<samples.length; i++) {\n    node.send({payload: samples[i]})\n}\n\nlet mostRecent = pipe.getMostRecent()\n//node.send({topic: \"MOST RECENT\", payload: now - mostRecent.time})\nif (msg.topic == \"trigger\") {\n    // add another sample in if too long has elapsed since last one\n    if (mostRecent && now - mostRecent.time > maxSampleIntervalTime) {\n        //node.warn(\"adding extra sample\")\n        pipe.addSample(mostRecent.value)\n    }\n} else {\n    // not trigger so add new sample to front of pipe\n    pipe.addSample(msg.payload.value)\n    //node.warn(`speed was: ${speed}  setting to: ${msg.payload.speed}`)\n    speed = msg.payload.speed\n}\n\ncontext.set(\"lastTime\", now)\ncontext.set(\"speed\", speed)\nmsg.payload = pipe.getFifo()\n//msg = null\nreturn [null,msg]\n","outputs":2,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\n// pipe creation defaults, length 40 units with samples every 0.5 unit and value 0.0625/12\nconst defaultLength = 40                // 40 ft long\nconst defaultInterval = 1              // interval 1 ft\nconst defaultSampleValue = 0.5          // 0.5 inches  It doesn't matter that this is inches not ft\nconst defaultMaxSamples = 80          // max no of samples in fifo\nconst defaultMinIntervalTime = 500     // min interval between samples\n\n/**\n * \n*/\nclass Pipe {\n    // Constructs a pipe of the specified length and optionally prefills with samples with \n    // specified inter-sample interval distance\n    \n    constructor(length, maxSamples, minIntervalTime, interval=0, sampleValue = 0 ) {\n        this.length = length\n        this.maxSamples = maxSamples\n        this.minIntervalTime = minIntervalTime\n        this.fifo = []\n        if (interval) {\n            for (let position = interval; position <= length; position += interval)\n              this.addSample(sampleValue, position)\n        }\n    }\n    \n    // Adds a sample at the specified position from the end of the pipe\n    // if position is not specified then puts it at the front\n    // assumes that if a position is specified then it will be in front\n    // of any existing samples\n    // if the time since the last one is too short or there are too many in the buffer then it changes the value\n    // of the most recent instead, but don't do this if position is specified\n    addSample( value, position=defaultLength) {\n        const now = new Date().getTime()\n        //if (position == defaultLength && \n        //this.fifo.length) {\n        //    node.warn(`Time since last: ${now - this.fifo[0].time}`)\n        //}\n        if (position == defaultLength && \n        this.fifo.length && \n        now - this.fifo[0].time < this.minIntervalTime || \n        this.fifo.length >= this.maxSamples) {\n            //node.warn(\"Adjusting previous value\")\n            this.fifo[0].value = value\n        } else {\n            this.fifo.unshift({value: value, position: position, time: new Date().getTime()})\n        }\n    }\n    \n    // returns the most recent sample, or false if none\n    getMostRecent() {\n        let ans = false\n        if (this.fifo[0]) {\n            ans = this.fifo[0]\n        }\n        return ans\n    }\n    \n    getFifo() {\n        return this.fifo\n    }\n    \n    // advances the pipe by the specified distance returning array of any values \n    // that are now off the end, first in array is the first one to be passed on\n    advance(distance) {\n        let ans = []\n        // move all the samples down\n        for (let i=0; i<this.fifo.length; i++) {\n            this.fifo[i].position -= distance\n        }\n        // remove ones that are now off the end\n        while (this.fifo.length  &&  this.fifo[this.fifo.length-1].position < 0) {\n            ans.push(this.fifo[this.fifo.length-1].value)\n            this.fifo.pop()\n        }\n        return (ans)\n    }\n}\n\n// create a 40ft pipe\ncontext.set(\"pipe\", new Pipe(defaultLength, defaultMaxSamples, defaultMinIntervalTime, defaultInterval, defaultSampleValue))\ncontext.set(\"speed\", 40/60)     // default to 40 ft/min","finalize":"","libs":[],"x":620,"y":140,"wires":[[],["26db29c4.53870e"]]},{"id":"b5089425.c3ed5","type":"comment","z":"672e3dc1.ab6844","name":"Pass in speed in ft/min","info":"","x":220,"y":20,"wires":[]},{"id":"aabdfa66.28a83","type":"comment","z":"672e3dc1.ab6844","name":"Output 1 is measured diameter at end of pipe run","info":"","x":600,"y":280,"wires":[]},{"id":"69fe07dd.36a44","type":"comment","z":"672e3dc1.ab6844","name":"Output 2 is diameter at extruder","info":"","x":650,"y":400,"wires":[]},{"id":"5f11bf23.0930a8","type":"change","z":"672e3dc1.ab6844","name":"","rules":[{"t":"move","p":"payload.value","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":270,"y":360,"wires":[[]]},{"id":"ffe6de2a.9d3ae8","type":"comment","z":"672e3dc1.ab6844","name":"Output 3 is fifo length","info":"","x":680,"y":500,"wires":[]},{"id":"26db29c4.53870e","type":"function","z":"672e3dc1.ab6844","name":"Fifo length","func":"msg.payload = msg.payload.length\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":460,"wires":[[]]},{"id":"aa18de76.9aeb28","type":"comment","z":"672e3dc1.ab6844","name":"Or topic: \"offset\" with payload speed offset to apply","info":"","x":310,"y":60,"wires":[]},{"id":"fb357619.c5e78","type":"function","z":"672e3dc1.ab6844","name":"Add offset","func":"// Add in offset passed with topic: \"offset\"\nlet offset = context.get(\"offset\") || 0\nif (msg.topic == \"offset\") {\n    offset = msg.payload\n    context.set(\"offset\", offset)\n    msg = null\n} else {\n    msg.payload += offset\n}\n\nreturn [msg, {payload: `Offset: ${offset}`}];","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":120,"y":140,"wires":[["d2e53a07.d7da6"],[]]},{"id":"e7135ec.e2da62","type":"comment","z":"672e3dc1.ab6844","name":"Output 4 is speed after filter","info":"","x":700,"y":600,"wires":[]},{"id":"b1e1b45f.b3fbd8","type":"tab","label":"Extruder PID","disabled":false,"info":""},{"id":"697a4eb0.e2a0e","type":"PID","z":"b1e1b45f.b3fbd8","name":"","setpoint":".625","pb":"1.8","ti":"60","td":"0","integral_default":"0.5","smooth_factor":"0","max_interval":600,"enable":"1","disabled_op":"0.5","x":350,"y":360,"wires":[["728f61f0.af394","571d9f3f.d38ab8","ece9c1f1.90baf8"]]},{"id":"4a8a734a.3f37b4","type":"change","z":"b1e1b45f.b3fbd8","name":"op","rules":[{"t":"set","p":"topic","pt":"msg","to":"op","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":440,"wires":[["63a8ea87.f1569c"]]},{"id":"b7c59b17.064f48","type":"inject","z":"b1e1b45f.b3fbd8","name":"Setpoint 0.625","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"setpoint","payload":"0.625","payloadType":"num","x":102,"y":383,"wires":[["697a4eb0.e2a0e"]]},{"id":"cc0e3aeb.9eda8","type":"inject","z":"b1e1b45f.b3fbd8","name":"Setpoint 0.4","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"setpoint","payload":"0.4","payloadType":"num","x":100.5,"y":432,"wires":[["697a4eb0.e2a0e"]]},{"id":"93853a25.a3c87","type":"inject","z":"b1e1b45f.b3fbd8","name":"enable","repeat":"","crontab":"","once":false,"topic":"enable","payload":"true","payloadType":"bool","x":83,"y":268,"wires":[["697a4eb0.e2a0e"]]},{"id":"6f7c7c83.9d716c","type":"inject","z":"b1e1b45f.b3fbd8","name":"disable","repeat":"","crontab":"","once":false,"topic":"enable","payload":"false","payloadType":"bool","x":83.5,"y":318,"wires":[["697a4eb0.e2a0e"]]},{"id":"c2f1e6e7.c82ee","type":"change","z":"b1e1b45f.b3fbd8","name":"pv","rules":[{"t":"set","p":"topic","pt":"msg","to":"pv","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":550,"y":160,"wires":[["427cf036.de57f8","63a8ea87.f1569c"]]},{"id":"728f61f0.af394","type":"range","z":"b1e1b45f.b3fbd8","minin":"0","maxin":"1","minout":"0.5","maxout":"0.0","action":"scale","round":false,"property":"payload","name":"Scale power 0.5-0 for chart","x":380,"y":440,"wires":[["4a8a734a.3f37b4"]]},{"id":"63a8ea87.f1569c","type":"ui_chart","z":"b1e1b45f.b3fbd8","name":"","group":"c45a83a3.d00908","order":6,"width":"12","height":"8","label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"step","nodata":"","dot":false,"ymin":"0","ymax":"1","removeOlder":"15","removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#cf0005","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":770,"y":400,"wires":[[]]},{"id":"c98f51a2.fb3ba","type":"inject","z":"b1e1b45f.b3fbd8","name":"Clear chart on deploy","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"","topic":"","payload":"[]","payloadType":"jsonata","x":540,"y":500,"wires":[["63a8ea87.f1569c"]]},{"id":"571d9f3f.d38ab8","type":"change","z":"b1e1b45f.b3fbd8","name":"Extract Setpoint","rules":[{"t":"move","p":"setpoint","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"setpoint","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":560,"y":360,"wires":[["63a8ea87.f1569c"]]},{"id":"14de4776.231f61","type":"subflow:672e3dc1.ab6844","z":"b1e1b45f.b3fbd8","name":"","env":[],"x":400,"y":100,"wires":[["c2f1e6e7.c82ee","697a4eb0.e2a0e"],["c0582371.e2dbc8"],[]]},{"id":"ece9c1f1.90baf8","type":"range","z":"b1e1b45f.b3fbd8","minin":"0","maxin":"1","minout":"50","maxout":"30","action":"scale","round":false,"property":"payload","name":"Scale power 50-30 ft/min","x":510,"y":280,"wires":[["14de4776.231f61","11d99052.266548"]]},{"id":"11d99052.266548","type":"ui_text","z":"b1e1b45f.b3fbd8","group":"c45a83a3.d00908","order":1,"width":5,"height":1,"name":"","label":"Speed","format":"{{msg.payload | number:2}}","layout":"row-spread","x":770,"y":280,"wires":[]},{"id":"427cf036.de57f8","type":"ui_text","z":"b1e1b45f.b3fbd8","group":"c45a83a3.d00908","order":3,"width":5,"height":1,"name":"","label":"Diameter Measured","format":"{{msg.payload | number:3}}","layout":"row-spread","x":730,"y":160,"wires":[]},{"id":"c0582371.e2dbc8","type":"ui_text","z":"b1e1b45f.b3fbd8","group":"c45a83a3.d00908","order":5,"width":5,"height":1,"name":"","label":"Diameter At Extruder","format":"{{msg.payload | number:3}}","layout":"row-spread","x":720,"y":100,"wires":[]},{"id":"43effee.700cb8","type":"inject","z":"b1e1b45f.b3fbd8","name":"Speed offset 0","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"offset","payload":"0","payloadType":"num","x":190,"y":80,"wires":[["14de4776.231f61"]]},{"id":"8225e46.c830698","type":"inject","z":"b1e1b45f.b3fbd8","name":"Speed offset 2","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"offset","payload":"2","payloadType":"num","x":190,"y":120,"wires":[["14de4776.231f61"]]},{"id":"e96e0453.e2e98","type":"inject","z":"b1e1b45f.b3fbd8","name":"Speed offset 5","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"offset","payload":"5","payloadType":"num","x":190,"y":160,"wires":[["14de4776.231f61"]]},{"id":"c45a83a3.d00908","type":"ui_group","name":"PID","tab":"80cd4062.93a5","order":1,"disp":false,"width":"12","collapse":false},{"id":"80cd4062.93a5","type":"ui_tab","name":"PID","icon":"dashboard","disabled":false,"hidden":false}]

[Edit] I found a small error in the code and have replaced the flow.

1 Like

This is very impressive, I do have a problem with the sampling rate though, I get the values from the gauging computer via OPCUA, I'm not sure I can get 1Hz sampling rate, currently it's 1 sample per second.

1 sample per second is 1Hz