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

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.
@@ -0,0 +1,55 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="latch-block">
3
+ <div class="form-row">
4
+ <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
5
+ <input type="text" id="node-input-name" placeholder="Name">
6
+ </div>
7
+ </script>
8
+
9
+ <!-- JavaScript Section -->
10
+ <script type="text/javascript">
11
+ RED.nodes.registerType("latch-block", {
12
+ category: "control",
13
+ color: "#301934",
14
+ defaults: {
15
+ name: { value: "" },
16
+ state: { value: false }
17
+ },
18
+ inputs: 1,
19
+ outputs: 1,
20
+ inputLabels: ["input"],
21
+ outputLabels: ["output"],
22
+ icon: "font-awesome/fa-toggle-on",
23
+ paletteLabel: "latch",
24
+ label: function() {
25
+ return this.name || "latch";
26
+ }
27
+ });
28
+ </script>
29
+
30
+ <!-- Help Section -->
31
+ <script type="text/markdown" data-help-name="latch-block">
32
+ Latch output based on control messages.
33
+
34
+ ### Inputs
35
+ : context (string) : Configuration commands (`set`, `reset`).
36
+ : payload (boolean) : `true` to set latch, `false` to reset latch when paired with appropriate `context`.
37
+
38
+ ### Outputs
39
+ : output (msg) : `msg.payload` `true` or `false` based on latch state.
40
+
41
+ ### Details
42
+ Set or reset the latch state based on input messages. `msg.context` = `"set"` with `msg.payload` = `true`
43
+ sets the latch `true`, while `msg.context` = `"reset"` with `msg.payload` = `true` resets it to `false`.
44
+
45
+ ### Status
46
+ - Green (dot): Configuration update
47
+ - Blue (dot): State changed
48
+ - Blue (ring): State unchanged
49
+ - Red (ring): Error
50
+ - Yellow (ring): Warning
51
+
52
+ ### References
53
+ - [Node-RED Documentation](https://nodered.org/docs/)
54
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
55
+ </script>
@@ -0,0 +1,77 @@
1
+ module.exports = function(RED) {
2
+ function LatchBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+
5
+ const node = this;
6
+
7
+ // Initialize state from config
8
+ node.state = config.state;
9
+
10
+ // Set initial status
11
+ node.status({
12
+ fill: "green",
13
+ shape: "dot",
14
+ text: `state: ${node.state}`
15
+ });
16
+
17
+ node.on("input", function(msg, send, done) {
18
+ send = send || function() { node.send.apply(node, arguments); };
19
+
20
+ // Guard against invalid message
21
+ if (!msg) {
22
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
23
+ if (done) done();
24
+ return;
25
+ }
26
+
27
+ // Validate context
28
+ if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
29
+ node.status({ fill: "red", shape: "ring", text: "missing or invalid context" });
30
+ if (done) done();
31
+ return;
32
+ }
33
+
34
+ // Handle context commands
35
+ switch (msg.context) {
36
+ case "set":
37
+ if (node.state) {
38
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
39
+ } else {
40
+ if (msg.payload) {
41
+ node.state = true;
42
+ node.status({ fill: "blue", shape: "dot", text: `state: ${node.state}` });
43
+ } else {
44
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
45
+ }
46
+ }
47
+ // Output latch value regardless
48
+ send({ payload: node.state });
49
+ break;
50
+ case "reset":
51
+ if (node.state === false) {
52
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
53
+ } else {
54
+ if (msg.payload) {
55
+ node.state = false;
56
+ node.status({ fill: "blue", shape: "dot", text: `state: ${node.state}` });
57
+ } else {
58
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
59
+ }
60
+ }
61
+ send({ payload: node.state });
62
+ break;
63
+ default:
64
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
65
+ if (done) done("Unknown context");
66
+ return;
67
+ }
68
+ if (done) done();
69
+ });
70
+
71
+ node.on("close", function(done) {
72
+ done();
73
+ });
74
+ }
75
+
76
+ RED.nodes.registerType("latch-block", LatchBlockNode);
77
+ };
@@ -64,7 +64,6 @@ differs from the last output value. When `period > 0`, outputs the first message
64
64
  Supports complex payloads (objects, arrays) via deep comparison.
65
65
  Configuration
66
66
  - `msg.context = "period"` Sets period (ms), no output.
67
- - `msg.context = "status"` Outputs `{ period, periodType }`.
68
67
 
69
68
  ### Status
70
69
  - Green (dot): Configuration update
@@ -70,16 +70,6 @@ module.exports = function(RED) {
70
70
  if (done) done();
71
71
  return;
72
72
  }
73
- if (msg.context === "status") {
74
- send({
75
- payload: {
76
- period: node.runtime.period,
77
- periodType: node.runtime.periodType
78
- }
79
- });
80
- if (done) done();
81
- return;
82
- }
83
73
  // Ignore unknown context
84
74
  }
85
75
 
@@ -115,7 +105,7 @@ module.exports = function(RED) {
115
105
  node.status({
116
106
  fill: "blue",
117
107
  shape: "ring",
118
- text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}`
108
+ text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)} |`
119
109
  });
120
110
  if (done) done();
121
111
  return;
@@ -139,7 +129,7 @@ module.exports = function(RED) {
139
129
  node.status({
140
130
  fill: "blue",
141
131
  shape: "ring",
142
- text: `Filter period expired`
132
+ text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}` // remove ' |' to indicate end of filter period
143
133
  });
144
134
  }, node.runtime.period);
145
135
  }
@@ -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/)