UNPKG

node-red-contrib-pid

Version:
239 lines (227 loc) 10.2 kB
/** * Copyright 2016 Colin Law * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { "use strict"; function PID(config) { RED.nodes.createNode(this,config); var node = this; node.setpoint = Number(config.setpoint); node.enable = Number(config.enable); node.prop_band = Number(config.pb); node.t_integral = Number(config.ti); node.t_derivative = Number(config.td); node.integral_default = Number(config.integral_default); node.smooth_factor = Number(config.smooth_factor); node.max_interval = Number(config.max_interval); node.disabled_op = Number(config.disabled_op); // sanitise disabled output as this is used when all else fails if (isNaN(node.disabled_op)) { node.disabled_op = 0; } this.on('input', function(msg) { var newMsg = null; // Using msg.* to change specific PID property. if (msg.hasOwnProperty('setpoint')) { node.setpoint = Number(msg.setpoint); } if (msg.hasOwnProperty('enable')) { node.enable = Number(msg.enable); } if (msg.hasOwnProperty('prop_band')) { node.prop_band = Number(msg.prop_band); } if (msg.hasOwnProperty('t_integral')) { node.t_integral = Number(msg.t_integral); } if (msg.hasOwnProperty('t_derivative')) { node.t_derivative = Number(msg.t_derivative); } if (msg.hasOwnProperty('smooth_factor')) { node.smooth_factor = Number(msg.smooth_factor); } if (msg.hasOwnProperty('max_interval')) { node.max_interval = Number(msg.max_interval); } if (msg.hasOwnProperty('disabled_op')) { node.disabled_op = Number(msg.disabled_op); // sanitise disabled output as this is used when all else fails if (isNaN(node.disabled_op)) { node.disabled_op = 0; } } if (msg.hasOwnProperty('integral_default')){ node.integral_default = Number(msg.integral_default); } if (msg.topic == 'setpoint') { node.setpoint = Number(msg.payload); } else if (msg.topic == 'enable') { var wasEnabled = node.enable; node.enable = Number(msg.payload); // if now enabled just clear the status, can't do more until a pv sample is received if (node.enable && !wasEnabled) { node.status({}); } else if (wasEnabled && !node.enable) { // now disabled, force the power to the disabled value immediately newMsg = {payload: node.disabled_op}; node.status({fill:"yellow",shape:"dot",text:"Disabled"}); } } else if (msg.topic == 'prop_band') { node.prop_band = Number(msg.payload); } else if (msg.topic == 't_integral') { node.t_integral = Number(msg.payload); } else if (msg.topic == 't_derivative') { node.t_derivative = Number(msg.payload); } else if (msg.topic == 'smooth_factor') { node.smooth_factor = Number(msg.payload); } else if (msg.topic == 'max_interval') { node.max_interval = Number(msg.payload); } else if (msg.topic == 'disabled_op') { node.disabled_op = Number(msg.payload); // sanitise disabled output as this is used when all else fails if (isNaN(node.disabled_op)) { node.disabled_op = 0; } } else if (msg.topic == 'integral_default') { node.integral_default = Number(msg.payload); } else { // anything else is assumed to be a process value node.pv = Number(msg.payload); // this may give NaN which is handled in runControlLoop newMsg = runControlLoop(); } // if there is a message to send then merge in properties from old message as they should be preserved if (newMsg) { for (let attrname in msg) { if (!newMsg.hasOwnProperty(attrname)) { newMsg[attrname] = msg[attrname]; } } } node.send(newMsg); }); function runControlLoop() { //node.log("pv, setpoint, prop_band, t_integral, t_derivative, integral_default, smooth_factor, max_interval, enable, disabled_op"); //node.log(node.pv + " " + node.setpoint + " " + node.prop_band + " " + node.t_integral + " " + node.t_derivative + " " + node.integral_default + " " + node.smooth_factor + " " + node.max_interval + " " + node.enable + " " + node.disabled_op); var ans; node.status({}); // check we have a good pv value if (!isNaN(node.pv) && isFinite(node.pv)) { // even if we are disabled (enable == 0 or false) then run through the calcs to keep the derivative up to date // but lock the integral and set power to appropriate value at the end var time = Date.now(); var integral_locked = false; var factor; if (node.last_sample_time) { var delta_t = (time - node.last_sample_time)/1000; // seconds if (delta_t <= 0 || delta_t > node.max_interval) { // too long since last sample so leave integral as is and set deriv to zero node.status({fill:"red",shape:"dot",text:"Too long since last sample"}); node.derivative = 0 } else { if (node.smooth_factor > 0) { // A derivative smoothing factor has been supplied // smoothing time constant is td/factor but with a min of delta_t to stop overflows var ts = Math.max(node.t_derivative/node.smooth_factor, delta_t); factor = 1.0/(ts/delta_t); } else { // no integral smoothing so factor is 1, this makes smoothed_value the previous pv factor = 1.0; } var delta_v = (node.pv - node.smoothed_value) * factor; node.smoothed_value = node.smoothed_value + delta_v //node.log( "factor " + factor.toFixed(3) + " delta_t " + delta_t + " delta_v " + delta_v.toFixed(3) + // " smoothed " + node.smoothed_value.toFixed(3)); node.derivative = node.t_derivative * delta_v/delta_t; // lock the integral if abs(previous integral + error) > prop_band/2 // as this means that P + I is outside the linear region so power will be 0 or full // also lock if control is disabled var error = node.pv - node.setpoint; var pbo2 = node.prop_band/2.0; if ((Math.abs(error + node.integral) < pbo2) && node.enable) { integral_locked = false; if (node.t_integral <= 0) { // t_integral is zero (or silly), set integral to one end or the other // or half way if exactly on sp node.integral = Math.sign(error) * pbo2; } else { node.integral = node.integral + error * delta_t/node.t_integral; } } else { //node.log("Locking integral"); integral_locked = true; } // clamp to +- 0.5 prop band widths so that it cannot push the zero power point outside the pb // do this here rather than when integral is updated to allow for the fact that the pb may change dynamically if ( node.integral < -pbo2 ) { node.integral = -pbo2; } else if (node.integral > pbo2) { node.integral = pbo2; } } } else { // first time through so initialise context data node.smoothed_value = node.pv; // setup the integral term so that the power out would be integral_default if pv=setpoint node.integral = (0.5 - node.integral_default)*node.prop_band; node.derivative = 0.0; node.last_power = 0.0; // power last time through } var proportional = node.pv - node.setpoint; if (node.prop_band == 0) { // prop band is zero so drop back to on/off control with zero hysteresis if (proportional > 0) { power = 0.0; } else if (proportional < 0) { power = 1.0; } else { // exactly on sp so leave power as it was last time round power = node.last_power; } } else { var power = -1.0/node.prop_band * (proportional + node.integral + node.derivative) + 0.5; } // set power to disabled value if the loop is not enabled if (!node.enable) { power = node.disabled_op; node.status({fill:"yellow",shape:"dot",text:"Disabled"}); } else if (integral_locked) { node.status({fill:"green",shape:"dot",text:"Integral Locked"}); } else { node.status({fill:"green",shape:"dot"}); } node.last_sample_time = time; } else { // pv is not a good number so set power to disabled value power = node.disabled_op; node.status({fill:"red",shape:"dot",text:"Bad PV"}); } // if NaN vaues have been entered for params or something drastic has gone wrong // then set power to disabled value if (isNaN(power)) { power = node.disabled_op; } if (power < 0.0) { power = 0.0; } else if (power > 1.0) { power = 1.0; } node.last_power = power; ans = {payload: power, pv: node.pv, setpoint: node.setpoint, proportional: proportional, integral: node.integral, derivative: node.derivative, smoothed_value: node.smoothed_value} return ans; } } RED.nodes.registerType("PID",PID); }