@bldgblocks/node-red-contrib-control 0.1.27 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,23 +6,28 @@
6
6
  </div>
7
7
  <div class="form-row">
8
8
  <label for="node-input-kp" title="Proportional gain (number)"><i class="fa fa-sliders"></i> Kp</label>
9
- <input type="number" id="node-input-kp" placeholder="0" step="any">
9
+ <input type="text" id="node-input-kp" placeholder="0" step="any">
10
+ <input type="hidden" id="node-input-kpType">
10
11
  </div>
11
12
  <div class="form-row">
12
13
  <label for="node-input-ki" title="Integral gain (number)"><i class="fa fa-sliders"></i> Ki</label>
13
- <input type="number" id="node-input-ki" placeholder="0" step="any">
14
+ <input type="text" id="node-input-ki" placeholder="0" step="any">
15
+ <input type="hidden" id="node-input-kiType">
14
16
  </div>
15
17
  <div class="form-row">
16
18
  <label for="node-input-kd" title="Derivative gain (number)"><i class="fa fa-sliders"></i> Kd</label>
17
- <input type="number" id="node-input-kd" placeholder="0" step="any">
19
+ <input type="text" id="node-input-kd" placeholder="0" step="any">
20
+ <input type="hidden" id="node-input-kdType">
18
21
  </div>
19
22
  <div class="form-row">
20
23
  <label for="node-input-setpoint" title="Target setpoint (number)"><i class="fa fa-crosshairs"></i> Setpoint</label>
21
- <input type="number" id="node-input-setpoint" placeholder="0" step="any">
24
+ <input type="text" id="node-input-setpoint" placeholder="0" step="any">
25
+ <input type="hidden" id="node-input-setpointType">
22
26
  </div>
23
27
  <div class="form-row">
24
28
  <label for="node-input-deadband" title="Deadband range around setpoint (non-negative number)"><i class="fa fa-arrows-h"></i> Deadband</label>
25
- <input type="number" id="node-input-deadband" placeholder="0" step="any" min="0">
29
+ <input type="text" id="node-input-deadband" placeholder="0" step="any" min="0">
30
+ <input type="hidden" id="node-input-deadbandType">
26
31
  </div>
27
32
  <div class="form-row">
28
33
  <label for="node-input-dbBehavior" title="Deadband behavior: ReturnToZero or HoldLastResult"><i class="fa fa-cog"></i> Deadband Behavior</label>
@@ -33,15 +38,18 @@
33
38
  </div>
34
39
  <div class="form-row">
35
40
  <label for="node-input-outMin" title="Minimum output limit (number, less than outMax, leave empty for no limit)"><i class="fa fa-arrow-down"></i> Out Min</label>
36
- <input type="number" id="node-input-outMin" placeholder="No min" step="any">
41
+ <input type="text" id="node-input-outMin" placeholder="No min" step="any">
42
+ <input type="hidden" id="node-input-outMinType">
37
43
  </div>
38
44
  <div class="form-row">
39
45
  <label for="node-input-outMax" title="Maximum output limit (number, greater than outMin, leave empty for no limit)"><i class="fa fa-arrow-up"></i> Out Max</label>
40
- <input type="number" id="node-input-outMax" placeholder="No max" step="any">
46
+ <input type="text" id="node-input-outMax" placeholder="No max" step="any">
47
+ <input type="hidden" id="node-input-outMaxType">
41
48
  </div>
42
49
  <div class="form-row">
43
50
  <label for="node-input-maxChange" title="Maximum output change per cycle (non-negative number)"><i class="fa fa-exchange"></i> Max Change</label>
44
- <input type="number" id="node-input-maxChange" placeholder="0" step="any" min="0">
51
+ <input type="text" id="node-input-maxChange" placeholder="0" step="any" min="0">
52
+ <input type="hidden" id="node-input-maxChangeType">
45
53
  </div>
46
54
  <div class="form-row">
47
55
  <label for="node-input-directAction" title="Direct (true) or reverse (false) action"><i class="fa fa-exchange"></i> Direct Action</label>
@@ -49,11 +57,8 @@
49
57
  </div>
50
58
  <div class="form-row">
51
59
  <label for="node-input-run" title="Enable (true) or disable (false) PID calculation"><i class="fa fa-play"></i> Run</label>
52
- <input type="checkbox" id="node-input-run" style="width: auto; vertical-align: middle;" checked>
53
- </div>
54
- <div class="form-row">
55
- <label><i class="fa fa-info-circle"></i> Changed Runtime Values</label>
56
- <pre id="node-runtime-changes" style="color: #555; white-space: pre-wrap;">Changed Values: None</pre>
60
+ <input type="text" id="node-input-run" style="width: auto; vertical-align: middle;" checked>
61
+ <input type="hidden" id="node-input-runType">
57
62
  </div>
58
63
  </script>
59
64
 
@@ -64,17 +69,26 @@
64
69
  color: "#301934",
65
70
  defaults: {
66
71
  name: { value: "" },
67
- kp: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
68
- ki: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
69
- kd: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
70
- setpoint: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
71
- deadband: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0 && isFinite(parseFloat(v)); } },
72
+ kp: { value: 0, required: true },
73
+ kpType: { value: "num" },
74
+ ki: { value: 0, required: true },
75
+ kiType: { value: "num" },
76
+ kd: { value: 0, required: true },
77
+ kdType: { value: "num" },
78
+ setpoint: { value: 0, required: true },
79
+ setpointType: { value: "num" },
80
+ deadband: { value: 0, required: true },
81
+ deadbandType: { value: "num" },
72
82
  dbBehavior: { value: "ReturnToZero" },
73
- outMin: { value: null, validate: function(v) { return v === "" || (!isNaN(parseFloat(v)) && isFinite(parseFloat(v))); } },
74
- outMax: { value: null, validate: function(v) { return v === "" || (!isNaN(parseFloat(v)) && isFinite(parseFloat(v))); } },
75
- maxChange: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0 && isFinite(parseFloat(v)); } },
83
+ outMin: { value: null },
84
+ outMinType: { value: "num" },
85
+ outMax: { value: null },
86
+ outMaxType: { value: "num" },
87
+ maxChange: { value: 0, required: true },
88
+ maxChangeType: { value: "num" },
76
89
  directAction: { value: false },
77
- run: { value: true }
90
+ run: { value: true },
91
+ runType: { value: "bool" }
78
92
  },
79
93
  inputs: 1,
80
94
  outputs: 1,
@@ -87,59 +101,67 @@
87
101
  },
88
102
  oneditprepare: function() {
89
103
  const node = this;
90
- $("#node-input-name").val(node.name || "");
91
- $("#node-input-kp").val(node.kp || 0);
92
- $("#node-input-ki").val(node.ki || 0);
93
- $("#node-input-kd").val(node.kd || 0);
94
- $("#node-input-setpoint").val(node.setpoint || 0);
95
- $("#node-input-deadband").val(node.deadband || 0);
96
- $("#node-input-dbBehavior").val(node.dbBehavior || "ReturnToZero");
97
- $("#node-input-outMin").val(node.outMin == null ? "" : node.outMin);
98
- $("#node-input-outMax").val(node.outMax == null ? "" : node.outMax);
99
- $("#node-input-maxChange").val(node.maxChange || 0);
100
- $("#node-input-directAction").prop("checked", !!node.directAction);
101
- $("#node-input-run").prop("checked", node.run !== false);
102
-
103
- $.getJSON(`/pid-block-runtime/${this.id}?t=${Date.now()}`, function(data) {
104
- const changes = [];
105
- if (data.name) changes.push(`name: ${data.name}`);
106
- if (data.kp !== parseFloat(node.kp || 0)) changes.push(`kp: ${data.kp.toFixed(2)}`);
107
- if (data.ki !== parseFloat(node.ki || 0)) changes.push(`ki: ${data.ki.toFixed(2)}`);
108
- if (data.kd !== parseFloat(node.kd || 0)) changes.push(`kd: ${data.kd.toFixed(2)}`);
109
- if (data.setpoint !== parseFloat(node.setpoint || 0)) changes.push(`setpoint: ${data.setpoint.toFixed(2)}`);
110
- if (data.deadband !== parseFloat(node.deadband || 0)) changes.push(`deadband: ${data.deadband.toFixed(2)}`);
111
- if (data.dbBehavior !== (node.dbBehavior || "ReturnToZero")) changes.push(`dbBehavior: ${data.dbBehavior}`);
112
- if (data.outMin != null && data.outMin !== parseFloat(node.outMin || null)) changes.push(`outMin: ${data.outMin.toFixed(2)}`);
113
- if (data.outMax != null && data.outMax !== parseFloat(node.outMax || null)) changes.push(`outMax: ${data.outMax.toFixed(2)}`);
114
- if (data.maxChange !== parseFloat(node.maxChange || 0)) changes.push(`maxChange: ${data.maxChange.toFixed(2)}`);
115
- if (data.directAction !== !!node.directAction) changes.push(`directAction: ${data.directAction}`);
116
- if (data.run !== (node.run !== false)) changes.push(`run: ${data.run}`);
117
- $("#node-runtime-changes").text(changes.length > 0 ? `Changed Values:\n${changes.join("\n")}` : "Changed Values: None");
118
- }).fail(function() {
119
- $("#node-runtime-changes").text("Changed Values: Unknown");
120
- });
121
- },
122
- oneditsave: function() {
123
- // Standard config properties are automatically saved by Node-RED
124
- },
125
- oneditvalidate: function() {
126
- const kp = parseFloat($("#node-input-kp").val());
127
- const ki = parseFloat($("#node-input-ki").val());
128
- const kd = parseFloat($("#node-input-kd").val());
129
- const setpoint = parseFloat($("#node-input-setpoint").val());
130
- const deadband = parseFloat($("#node-input-deadband").val());
131
- const outMin = $("#node-input-outMin").val() === "" ? null : parseFloat($("#node-input-outMin").val());
132
- const outMax = $("#node-input-outMax").val() === "" ? null : parseFloat($("#node-input-outMax").val());
133
- const maxChange = parseFloat($("#node-input-maxChange").val());
134
- return !isNaN(kp) && isFinite(kp) &&
135
- !isNaN(ki) && isFinite(ki) &&
136
- !isNaN(kd) && isFinite(kd) &&
137
- !isNaN(setpoint) && isFinite(setpoint) &&
138
- !isNaN(deadband) && isFinite(deadband) && deadband >= 0 &&
139
- (outMin === null || (!isNaN(outMin) && isFinite(outMin))) &&
140
- (outMax === null || (!isNaN(outMax) && isFinite(outMax))) &&
141
- (outMin === null || outMax === null || outMax > outMin) &&
142
- !isNaN(maxChange) && isFinite(maxChange) && maxChange >= 0;
104
+
105
+ try {
106
+ // Initialize typed inputs
107
+ $("#node-input-kp").typedInput({
108
+ default: "num",
109
+ types: ["num", "msg", "flow", "global"],
110
+ typeField: "#node-input-kpType"
111
+ }).typedInput("type", node.kpType || "num").typedInput("value", node.kp);
112
+
113
+ $("#node-input-ki").typedInput({
114
+ default: "num",
115
+ types: ["num", "msg", "flow", "global"],
116
+ typeField: "#node-input-kiType"
117
+ }).typedInput("type", node.kiType || "num").typedInput("value", node.ki);
118
+
119
+ $("#node-input-kd").typedInput({
120
+ default: "num",
121
+ types: ["num", "msg", "flow", "global"],
122
+ typeField: "#node-input-kdType"
123
+ }).typedInput("type", node.kdType || "num").typedInput("value", node.kd);
124
+
125
+ $("#node-input-setpoint").typedInput({
126
+ default: "num",
127
+ types: ["num", "msg", "flow", "global"],
128
+ typeField: "#node-input-setpointType"
129
+ }).typedInput("type", node.setpointType || "num").typedInput("value", node.setpoint);
130
+
131
+ $("#node-input-deadband").typedInput({
132
+ default: "num",
133
+ types: ["num", "msg", "flow", "global"],
134
+ typeField: "#node-input-deadbandType"
135
+ }).typedInput("type", node.deadbandType || "num").typedInput("value", node.deadband);
136
+
137
+ $("#node-input-outMin").typedInput({
138
+ default: "num",
139
+ types: ["num", "msg", "flow", "global"],
140
+ typeField: "#node-input-outMinType"
141
+ }).typedInput("type", node.outMinType || "num").typedInput("value", node.outMin);
142
+
143
+ $("#node-input-outMax").typedInput({
144
+ default: "num",
145
+ types: ["num", "msg", "flow", "global"],
146
+ typeField: "#node-input-outMaxType"
147
+ }).typedInput("type", node.outMaxType || "num").typedInput("value", node.outMax);
148
+
149
+ $("#node-input-maxChange").typedInput({
150
+ default: "num",
151
+ types: ["num", "msg", "flow", "global"],
152
+ typeField: "#node-input-maxChangeType"
153
+ }).typedInput("type", node.maxChangeType || "num").typedInput("value", node.maxChange);
154
+
155
+ $("#node-input-run").typedInput({
156
+ default: "bool",
157
+ types: ["bool", "msg", "flow", "global"],
158
+ typeField: "#node-input-runType"
159
+ }).typedInput("type", node.runType || "bool").typedInput("value", node.run);
160
+
161
+ } catch (err) {
162
+ console.error("Error in oneditprepare:", err);
163
+ }
164
+
143
165
  }
144
166
  });
145
167
  </script>
@@ -193,11 +215,11 @@ Ziegler-Nichols tuning sets `kp = 0.6*Ku`, `ki = 2*kp/Tu`, `kd = kp*Tu/8` after
193
215
  - Invalid config at startup: Red status (`invalid config` or specific), resets to defaults.
194
216
 
195
217
  ### Status
196
- - Green (dot): Configuration, reset, or tuning (e.g., `setpoint: 50.00`, `reset`, `tune: completed, Kp=1.20, Ki=0.40, Kd=0.90`).
197
- - Blue (dot): Output change (e.g., `in: 25.00, out: 50.00, setpoint: 50.00`).
198
- - Blue (ring): Output unchanged (e.g., `in: 25.00, out: 50.00, setpoint: 50.00`).
199
- - Red (ring): Errors (e.g., `invalid input`, `invalid setpoint`).
200
- - Yellow (ring): Unknown context (e.g., `unknown context`).
218
+ - Green (dot): Configuration, reset, or tuning
219
+ - Blue (dot): Output change
220
+ - Blue (ring): Output unchanged
221
+ - Red (ring): Errors
222
+ - Yellow (ring): Unknown context
201
223
 
202
224
  ### References
203
225
  - [Node-RED Documentation](https://nodered.org/docs/)
@@ -1,22 +1,15 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function PIDBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
6
+
4
7
  const node = this;
5
8
 
6
- // Initialize runtime state
9
+ // Initialize runtime state
7
10
  node.runtime = {
8
- name: config.name || "",
9
- kp: parseFloat(config.kp) || 0,
10
- ki: parseFloat(config.ki) || 0,
11
- kd: parseFloat(config.kd) || 0,
12
- setpoint: parseFloat(config.setpoint) || 0,
13
- deadband: parseFloat(config.deadband) || 0,
14
- dbBehavior: config.dbBehavior || "ReturnToZero",
15
- outMin: config.outMin ? parseFloat(config.outMin) : null,
16
- outMax: config.outMax ? parseFloat(config.outMax) : null,
17
- maxChange: parseFloat(config.maxChange) || 0,
18
- directAction: !!config.directAction,
19
- run: config.run !== false,
11
+ name: config.name,
12
+ dbBehavior: config.dbBehavior,
20
13
  errorSum: 0,
21
14
  lastError: 0,
22
15
  lastDError: 0,
@@ -25,36 +18,35 @@ module.exports = function(RED) {
25
18
  tuneMode: false,
26
19
  tuneData: { oscillations: [], lastPeak: null, lastTrough: null, Ku: 0, Tu: 0 }
27
20
  };
28
-
29
- // Validate initial config
30
- if (isNaN(node.runtime.kp) || isNaN(node.runtime.ki) || isNaN(node.runtime.kd) ||
31
- isNaN(node.runtime.setpoint) || isNaN(node.runtime.deadband) || isNaN(node.runtime.maxChange) ||
32
- !isFinite(node.runtime.kp) || !isFinite(node.runtime.ki) || !isFinite(node.runtime.kd) ||
33
- !isFinite(node.runtime.setpoint) || !isFinite(node.runtime.deadband) || !isFinite(node.runtime.maxChange)) {
34
- node.status({ fill: "red", shape: "ring", text: "invalid config" });
35
- node.runtime.kp = node.runtime.ki = node.runtime.kd = node.runtime.setpoint = node.runtime.deadband = node.runtime.maxChange = 0;
36
- }
37
- if (node.runtime.deadband < 0 || node.runtime.maxChange < 0) {
38
- node.status({ fill: "red", shape: "ring", text: "invalid deadband or maxChange" });
39
- node.runtime.deadband = node.runtime.maxChange = 0;
40
- }
41
- if (node.runtime.outMin != null && node.runtime.outMax != null && node.runtime.outMax <= node.runtime.outMin) {
42
- node.status({ fill: "red", shape: "ring", text: "invalid output range" });
43
- node.runtime.outMin = node.runtime.outMax = null;
44
- }
45
- if (!["ReturnToZero", "HoldLastResult"].includes(node.runtime.dbBehavior)) {
46
- node.status({ fill: "red", shape: "ring", text: "invalid dbBehavior" });
47
- node.runtime.dbBehavior = "ReturnToZero";
21
+
22
+ try {
23
+ node.runtime.kp = parseFloat(RED.util.evaluateNodeProperty( config.kp, config.kpType, node ));
24
+ node.runtime.ki = parseFloat(RED.util.evaluateNodeProperty( config.ki, config.kiType, node ));
25
+ node.runtime.kd = parseFloat(RED.util.evaluateNodeProperty( config.kd, config.kdType, node ));
26
+ node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node ));
27
+ node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
28
+ node.runtime.outMin = parseFloat(RED.util.evaluateNodeProperty( config.outMin, config.outMinType, node ));
29
+ node.runtime.outMax = parseFloat(RED.util.evaluateNodeProperty( config.outMax, config.outMaxType, node ));
30
+ node.runtime.maxChange = parseFloat(RED.util.evaluateNodeProperty( config.maxChange, config.maxChangeType, node ));
31
+ node.runtime.run = RED.util.evaluateNodeProperty( config.run, config.runType, node ) === true;
32
+ } catch (err) {
33
+ node.error(`Error evaluating properties: ${err.message}`);
48
34
  }
49
35
 
50
36
  // Initialize internal variables
51
- let storekp = node.runtime.kp;
52
- let storeki = node.runtime.ki;
53
- let storemin = node.runtime.outMin;
54
- let storemax = node.runtime.outMax;
55
- let kpkiConst = node.runtime.kp * node.runtime.ki;
56
- let minInt = kpkiConst === 0 ? 0 : (node.runtime.outMin || -Infinity) * kpkiConst;
57
- let maxInt = kpkiConst === 0 ? 0 : (node.runtime.outMax || Infinity) * kpkiConst;
37
+ let storekp = parseFloat(config.kp) || 0;
38
+ let storeki = parseFloat(config.ki) || 0;
39
+ let storekd = parseFloat(config.kd) || 0;
40
+ let storesetpoint = parseFloat(config.setpoint) || 0;
41
+ let storedeadband = parseFloat(config.deadband) || 0;
42
+ let storeOutMin = config.outMin ? parseFloat(config.outMin) : null;
43
+ let storeOutMax = config.outMax ? parseFloat(config.outMax) : null;
44
+ let storemaxChange = parseFloat(config.maxChange) || 0;
45
+ let storerun = !!config.run; // convert to boolean
46
+
47
+ let kpkiConst = storekp * storeki;
48
+ let minInt = kpkiConst === 0 ? 0 : (storeOutMin || -Infinity) * kpkiConst;
49
+ let maxInt = kpkiConst === 0 ? 0 : (storeOutMax || Infinity) * kpkiConst;
58
50
  let lastOutput = null;
59
51
 
60
52
  node.on("input", function(msg, send, done) {
@@ -67,6 +59,60 @@ module.exports = function(RED) {
67
59
  return;
68
60
  }
69
61
 
62
+ // Evaluate typed-input properties
63
+ try {
64
+ if (utils.requiresEvaluation(config.kpType)) {
65
+ node.runtime.kp = parseFloat(RED.util.evaluateNodeProperty( config.kp, config.kpType, node, msg ));
66
+ }
67
+ if (utils.requiresEvaluation(config.kiType)) {
68
+ node.runtime.ki = parseFloat(RED.util.evaluateNodeProperty( config.ki, config.kiType, node, msg ));
69
+ }
70
+ if (utils.requiresEvaluation(config.kdType)) {
71
+ node.runtime.kd = parseFloat(RED.util.evaluateNodeProperty( config.kd, config.kdType, node, msg ));
72
+ }
73
+ if (utils.requiresEvaluation(config.setpointType)) {
74
+ node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node, msg ));
75
+ }
76
+ if (utils.requiresEvaluation(config.deadbandType)) {
77
+ node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node, msg ));
78
+ }
79
+ if (utils.requiresEvaluation(config.outMinType)) {
80
+ node.runtime.outMin = parseFloat(RED.util.evaluateNodeProperty( config.outMin, config.outMinType, node, msg ));
81
+ }
82
+ if (utils.requiresEvaluation(config.outMaxType)) {
83
+ node.runtime.outMax = parseFloat(RED.util.evaluateNodeProperty( config.outMax, config.outMaxType, node, msg ));
84
+ }
85
+ if (utils.requiresEvaluation(config.maxChangeType)) {
86
+ node.runtime.maxChange = parseFloat(RED.util.evaluateNodeProperty( config.maxChange, config.maxChangeType, node, msg ));
87
+ }
88
+ if (utils.requiresEvaluation(config.runType)) {
89
+ node.runtime.run = RED.util.evaluateNodeProperty( config.run, config.runType, node, msg ) === true;
90
+ }
91
+ } catch (err) {
92
+ node.error(`Error evaluating properties: ${err.message}`);
93
+ }
94
+
95
+ // Validate config
96
+ if (isNaN(node.runtime.kp) || isNaN(node.runtime.ki) || isNaN(node.runtime.kd) ||
97
+ isNaN(node.runtime.setpoint) || isNaN(node.runtime.deadband) || isNaN(node.runtime.maxChange) ||
98
+ !isFinite(node.runtime.kp) || !isFinite(node.runtime.ki) || !isFinite(node.runtime.kd) ||
99
+ !isFinite(node.runtime.setpoint) || !isFinite(node.runtime.deadband) || !isFinite(node.runtime.maxChange)) {
100
+ node.status({ fill: "red", shape: "ring", text: "invalid config" });
101
+ node.runtime.kp = node.runtime.ki = node.runtime.kd = node.runtime.setpoint = node.runtime.deadband = node.runtime.maxChange = 0;
102
+ }
103
+ if (node.runtime.deadband < 0 || node.runtime.maxChange < 0) {
104
+ node.status({ fill: "red", shape: "ring", text: "invalid deadband or maxChange" });
105
+ node.runtime.deadband = node.runtime.maxChange = 0;
106
+ }
107
+ if (node.runtime.outMin != null && node.runtime.outMax != null && node.runtime.outMax <= node.runtime.outMin) {
108
+ node.status({ fill: "red", shape: "ring", text: "invalid output range" });
109
+ node.runtime.outMin = node.runtime.outMax = null;
110
+ }
111
+ if (!["ReturnToZero", "HoldLastResult"].includes(node.runtime.dbBehavior)) {
112
+ node.status({ fill: "red", shape: "ring", text: "invalid dbBehavior" });
113
+ node.runtime.dbBehavior = "ReturnToZero";
114
+ }
115
+
70
116
  // Handle context updates
71
117
  if (msg.hasOwnProperty("context")) {
72
118
  if (!msg.hasOwnProperty("payload")) {
@@ -156,14 +202,14 @@ module.exports = function(RED) {
156
202
  }
157
203
 
158
204
  if (!msg.hasOwnProperty("payload")) {
159
- node.status({ fill: "red", shape: "ring", text: "missing input" });
205
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
160
206
  if (done) done();
161
207
  return;
162
208
  }
163
209
 
164
210
  const input = parseFloat(msg.payload);
165
211
  if (isNaN(input) || !isFinite(input)) {
166
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
212
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
167
213
  if (done) done();
168
214
  return;
169
215
  }
@@ -173,7 +219,18 @@ module.exports = function(RED) {
173
219
  let interval = (currentTime - node.runtime.lastTime) / 1000; // Seconds
174
220
  node.runtime.lastTime = currentTime;
175
221
 
176
- let outputMsg = { payload: 0, diagnostics: {} };
222
+ let outputMsg = { payload: 0 };
223
+ outputMsg.diagnostics = {
224
+ setpoint: node.runtime.setpoint,
225
+ interval,
226
+ lastOutput,
227
+ run: node.runtime.run,
228
+ directAction: node.runtime.directAction,
229
+ kp: node.runtime.kp,
230
+ ki: node.runtime.ki,
231
+ kd: node.runtime.kd
232
+ };
233
+
177
234
  if (!node.runtime.run || interval <= 0 || node.runtime.kp === 0) {
178
235
  if (lastOutput !== 0) {
179
236
  lastOutput = 0;
@@ -182,7 +239,6 @@ module.exports = function(RED) {
182
239
  shape: "dot",
183
240
  text: `in: ${input.toFixed(2)}, out: 0.00, setpoint: ${node.runtime.setpoint.toFixed(2)}`
184
241
  });
185
- send(outputMsg);
186
242
  } else {
187
243
  node.status({
188
244
  fill: "blue",
@@ -190,6 +246,7 @@ module.exports = function(RED) {
190
246
  text: `in: ${input.toFixed(2)}, out: 0.00, setpoint: ${node.runtime.setpoint.toFixed(2)}`
191
247
  });
192
248
  }
249
+ send(outputMsg);
193
250
  if (done) done();
194
251
  return;
195
252
  }
@@ -218,7 +275,7 @@ module.exports = function(RED) {
218
275
  }
219
276
 
220
277
  // Update internal constraints
221
- if (node.runtime.kp !== storekp || node.runtime.ki !== storeki || node.runtime.outMin !== storemin || node.runtime.outMax !== storemax) {
278
+ if (node.runtime.kp !== storekp || node.runtime.ki !== storeki || node.runtime.outMin !== storeOutMin || node.runtime.outMax !== storeOutMax) {
222
279
  if (node.runtime.kp !== storekp && node.runtime.kp !== 0 && storekp !== 0) {
223
280
  node.runtime.errorSum = node.runtime.errorSum * storekp / node.runtime.kp;
224
281
  }
@@ -230,8 +287,8 @@ module.exports = function(RED) {
230
287
  maxInt = kpkiConst === 0 ? 0 : (node.runtime.outMax || Infinity) * kpkiConst;
231
288
  storekp = node.runtime.kp;
232
289
  storeki = node.runtime.ki;
233
- storemin = node.runtime.outMin;
234
- storemax = node.runtime.outMax;
290
+ storeOutMin = node.runtime.outMin;
291
+ storeOutMax = node.runtime.outMax;
235
292
  }
236
293
 
237
294
  // Calculate error
@@ -272,6 +329,7 @@ module.exports = function(RED) {
272
329
  shape: "dot",
273
330
  text: `tune: completed, Kp=${node.runtime.kp.toFixed(2)}, Ki=${node.runtime.ki.toFixed(2)}, Kd=${node.runtime.kd.toFixed(2)}`
274
331
  });
332
+
275
333
  send(outputMsg);
276
334
  if (done) done();
277
335
  return;
@@ -302,7 +360,7 @@ module.exports = function(RED) {
302
360
  // Output calculation
303
361
  let pv = pGain + intGain + dGain;
304
362
  //if (node.runtime.directAction) pv = -pv;
305
- pv = Math.min(Math.max(pv, node.runtime.outMin || -Infinity), node.runtime.outMax || Infinity);
363
+ pv = Math.min(Math.max(pv, node.runtime.outMin), node.runtime.outMax);
306
364
 
307
365
  // Rate of change limit
308
366
  if (node.runtime.maxChange !== 0) {
@@ -316,7 +374,18 @@ module.exports = function(RED) {
316
374
  }
317
375
 
318
376
  outputMsg.payload = node.runtime.result;
319
- outputMsg.diagnostics = { pGain, intGain, dGain, error, errorSum: node.runtime.errorSum };
377
+ outputMsg.diagnostics = {
378
+ pGain,
379
+ intGain,
380
+ dGain,
381
+ error,
382
+ errorSum: node.runtime.errorSum,
383
+ run: node.runtime.run,
384
+ directAction: node.runtime.directAction,
385
+ kp: node.runtime.kp,
386
+ ki: node.runtime.ki,
387
+ kd: node.runtime.kd
388
+ };
320
389
 
321
390
  const outputChanged = !lastOutput || lastOutput !== outputMsg.payload;
322
391
  if (outputChanged) {
@@ -326,7 +395,6 @@ module.exports = function(RED) {
326
395
  shape: "dot",
327
396
  text: `in: ${input.toFixed(2)}, out: ${node.runtime.result.toFixed(2)}, setpoint: ${node.runtime.setpoint.toFixed(2)}`
328
397
  });
329
- send(outputMsg);
330
398
  } else {
331
399
  node.status({
332
400
  fill: "blue",
@@ -335,73 +403,15 @@ module.exports = function(RED) {
335
403
  });
336
404
  }
337
405
 
406
+ send(outputMsg);
407
+
338
408
  if (done) done();
339
409
  });
340
410
 
341
411
  node.on("close", function(done) {
342
- node.runtime = {
343
- name: config.name || "",
344
- kp: parseFloat(config.kp) || 0,
345
- ki: parseFloat(config.ki) || 0,
346
- kd: parseFloat(config.kd) || 0,
347
- setpoint: parseFloat(config.setpoint) || 0,
348
- deadband: parseFloat(config.deadband) || 0,
349
- dbBehavior: config.dbBehavior || "ReturnToZero",
350
- outMin: config.outMin ? parseFloat(config.outMin) : null,
351
- outMax: config.outMax ? parseFloat(config.outMax) : null,
352
- maxChange: parseFloat(config.maxChange) || 0,
353
- directAction: !!config.directAction,
354
- run: config.run !== false,
355
- errorSum: 0,
356
- lastError: 0,
357
- lastDError: 0,
358
- result: 0,
359
- lastTime: Date.now(),
360
- tuneMode: false,
361
- tuneData: { oscillations: [], lastPeak: null, lastTrough: null, Ku: 0, Tu: 0 }
362
- };
363
- if (isNaN(node.runtime.kp) || isNaN(node.runtime.ki) || isNaN(node.runtime.kd) ||
364
- isNaN(node.runtime.setpoint) || isNaN(node.runtime.deadband) || isNaN(node.runtime.maxChange) ||
365
- !isFinite(node.runtime.kp) || !isFinite(node.runtime.ki) || !isFinite(node.runtime.kd) ||
366
- !isFinite(node.runtime.setpoint) || !isFinite(node.runtime.deadband) || !isFinite(node.runtime.maxChange)) {
367
- node.runtime.kp = node.runtime.ki = node.runtime.kd = node.runtime.setpoint = node.runtime.deadband = node.runtime.maxChange = 0;
368
- }
369
- if (node.runtime.deadband < 0 || node.runtime.maxChange < 0) {
370
- node.runtime.deadband = node.runtime.maxChange = 0;
371
- }
372
- if (node.runtime.outMin != null && node.runtime.outMax != null && node.runtime.outMax <= node.runtime.outMin) {
373
- node.runtime.outMin = node.runtime.outMax = null;
374
- }
375
- if (!["ReturnToZero", "HoldLastResult"].includes(node.runtime.dbBehavior)) {
376
- node.runtime.dbBehavior = "ReturnToZero";
377
- }
378
- node.status({});
379
412
  done();
380
413
  });
381
414
  }
382
415
 
383
416
  RED.nodes.registerType("pid-block", PIDBlockNode);
384
-
385
- // Serve runtime state for editor
386
- RED.httpAdmin.get("/pid-block-runtime/:id", RED.auth.needsPermission("pid-block.read"), function(req, res) {
387
- const node = RED.nodes.getNode(req.params.id);
388
- if (node && node.type === "pid-block") {
389
- res.json({
390
- name: node.runtime.name,
391
- kp: node.runtime.kp,
392
- ki: node.runtime.ki,
393
- kd: node.runtime.kd,
394
- setpoint: node.runtime.setpoint,
395
- deadband: node.runtime.deadband,
396
- dbBehavior: node.runtime.dbBehavior,
397
- outMin: node.runtime.outMin,
398
- outMax: node.runtime.outMax,
399
- maxChange: node.runtime.maxChange,
400
- directAction: node.runtime.directAction,
401
- run: node.runtime.run
402
- });
403
- } else {
404
- res.status(404).json({ error: "Node not found" });
405
- }
406
- });
407
417
  };