@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.
- package/nodes/average-block.js +1 -1
- package/nodes/boolean-switch-block.html +2 -1
- package/nodes/boolean-switch-block.js +1 -2
- package/nodes/call-status-block.html +30 -45
- package/nodes/call-status-block.js +81 -195
- package/nodes/changeover-block.html +76 -12
- package/nodes/changeover-block.js +24 -8
- package/nodes/delay-block.html +1 -1
- package/nodes/hysteresis-block.js +1 -1
- package/nodes/latch-block.html +55 -0
- package/nodes/latch-block.js +77 -0
- package/nodes/on-change-block.html +0 -1
- package/nodes/on-change-block.js +2 -12
- package/nodes/pid-block.html +102 -80
- package/nodes/pid-block.js +120 -110
- package/nodes/rate-of-change-block.html +110 -0
- package/nodes/rate-of-change-block.js +233 -0
- package/nodes/string-builder-block.html +112 -0
- package/nodes/string-builder-block.js +89 -0
- package/nodes/tstat-block.html +85 -39
- package/nodes/tstat-block.js +66 -30
- package/package.json +4 -1
package/nodes/pid-block.html
CHANGED
|
@@ -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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
53
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
$("#node-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
197
|
-
- Blue (dot): Output change
|
|
198
|
-
- Blue (ring): Output unchanged
|
|
199
|
-
- Red (ring): Errors
|
|
200
|
-
- Yellow (ring): 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/)
|
package/nodes/pid-block.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
node.
|
|
35
|
-
node.runtime.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
node.
|
|
39
|
-
|
|
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 =
|
|
52
|
-
let storeki =
|
|
53
|
-
let
|
|
54
|
-
let
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
let
|
|
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
|
|
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
|
|
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
|
|
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 !==
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
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 = {
|
|
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
|
};
|