@bldgblocks/node-red-contrib-control 0.1.4
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/README.md +43 -0
- package/nodes/accumulate-block.html +71 -0
- package/nodes/accumulate-block.js +104 -0
- package/nodes/add-block.html +67 -0
- package/nodes/add-block.js +97 -0
- package/nodes/analog-switch-block.html +65 -0
- package/nodes/analog-switch-block.js +129 -0
- package/nodes/and-block.html +64 -0
- package/nodes/and-block.js +73 -0
- package/nodes/average-block.html +97 -0
- package/nodes/average-block.js +137 -0
- package/nodes/boolean-switch-block.html +59 -0
- package/nodes/boolean-switch-block.js +88 -0
- package/nodes/boolean-to-number-block.html +59 -0
- package/nodes/boolean-to-number-block.js +45 -0
- package/nodes/cache-block.html +69 -0
- package/nodes/cache-block.js +106 -0
- package/nodes/call-status-block.html +111 -0
- package/nodes/call-status-block.js +274 -0
- package/nodes/changeover-block.html +234 -0
- package/nodes/changeover-block.js +392 -0
- package/nodes/comment-block.html +83 -0
- package/nodes/comment-block.js +53 -0
- package/nodes/compare-block.html +64 -0
- package/nodes/compare-block.js +84 -0
- package/nodes/contextual-label-block.html +67 -0
- package/nodes/contextual-label-block.js +52 -0
- package/nodes/convert-block.html +179 -0
- package/nodes/convert-block.js +289 -0
- package/nodes/count-block.html +57 -0
- package/nodes/count-block.js +92 -0
- package/nodes/debounce-block.html +64 -0
- package/nodes/debounce-block.js +140 -0
- package/nodes/delay-block.html +104 -0
- package/nodes/delay-block.js +180 -0
- package/nodes/divide-block.html +65 -0
- package/nodes/divide-block.js +123 -0
- package/nodes/edge-block.html +71 -0
- package/nodes/edge-block.js +120 -0
- package/nodes/frequency-block.html +55 -0
- package/nodes/frequency-block.js +140 -0
- package/nodes/hysteresis-block.html +131 -0
- package/nodes/hysteresis-block.js +142 -0
- package/nodes/interpolate-block.html +74 -0
- package/nodes/interpolate-block.js +141 -0
- package/nodes/load-sequence-block.html +134 -0
- package/nodes/load-sequence-block.js +272 -0
- package/nodes/max-block.html +76 -0
- package/nodes/max-block.js +103 -0
- package/nodes/memory-block.html +90 -0
- package/nodes/memory-block.js +241 -0
- package/nodes/min-block.html +77 -0
- package/nodes/min-block.js +106 -0
- package/nodes/minmax-block.html +89 -0
- package/nodes/minmax-block.js +119 -0
- package/nodes/modulo-block.html +73 -0
- package/nodes/modulo-block.js +126 -0
- package/nodes/multiply-block.html +63 -0
- package/nodes/multiply-block.js +115 -0
- package/nodes/negate-block.html +55 -0
- package/nodes/negate-block.js +91 -0
- package/nodes/nullify-block.html +111 -0
- package/nodes/nullify-block.js +78 -0
- package/nodes/on-change-block.html +79 -0
- package/nodes/on-change-block.js +191 -0
- package/nodes/oneshot-block.html +96 -0
- package/nodes/oneshot-block.js +169 -0
- package/nodes/or-block.html +64 -0
- package/nodes/or-block.js +73 -0
- package/nodes/pid-block.html +205 -0
- package/nodes/pid-block.js +407 -0
- package/nodes/priority-block.html +66 -0
- package/nodes/priority-block.js +239 -0
- package/nodes/rate-limit-block.html +99 -0
- package/nodes/rate-limit-block.js +221 -0
- package/nodes/round-block.html +73 -0
- package/nodes/round-block.js +89 -0
- package/nodes/saw-tooth-wave-block.html +87 -0
- package/nodes/saw-tooth-wave-block.js +161 -0
- package/nodes/scale-range-block.html +90 -0
- package/nodes/scale-range-block.js +137 -0
- package/nodes/sine-wave-block.html +88 -0
- package/nodes/sine-wave-block.js +142 -0
- package/nodes/subtract-block.html +64 -0
- package/nodes/subtract-block.js +103 -0
- package/nodes/thermistor-block.html +81 -0
- package/nodes/thermistor-block.js +146 -0
- package/nodes/tick-tock-block.html +66 -0
- package/nodes/tick-tock-block.js +110 -0
- package/nodes/time-sequence-block.html +67 -0
- package/nodes/time-sequence-block.js +144 -0
- package/nodes/triangle-wave-block.html +86 -0
- package/nodes/triangle-wave-block.js +154 -0
- package/nodes/tstat-block.html +311 -0
- package/nodes/tstat-block.js +499 -0
- package/nodes/units-block.html +150 -0
- package/nodes/units-block.js +106 -0
- package/package.json +73 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function PriorityBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
const context = this.context();
|
|
6
|
+
|
|
7
|
+
// Initialize runtime state
|
|
8
|
+
node.runtime = {
|
|
9
|
+
name: config.name
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Initialize state from context or defaults
|
|
13
|
+
let priorities = context.get("priorities") || {
|
|
14
|
+
priority1: null, priority2: null, priority3: null, priority4: null,
|
|
15
|
+
priority5: null, priority6: null, priority7: null, priority8: null,
|
|
16
|
+
priority9: null, priority10: null, priority11: null, priority12: null,
|
|
17
|
+
priority13: null, priority14: null, priority15: null, priority16: null
|
|
18
|
+
};
|
|
19
|
+
let defaultValue = context.get("defaultValue") || null;
|
|
20
|
+
let fallbackValue = context.get("fallbackValue") || null;
|
|
21
|
+
let messages = context.get("messages") || {
|
|
22
|
+
priority1: null, priority2: null, priority3: null, priority4: null,
|
|
23
|
+
priority5: null, priority6: null, priority7: null, priority8: null,
|
|
24
|
+
priority9: null, priority10: null, priority11: null, priority12: null,
|
|
25
|
+
priority13: null, priority14: null, priority15: null, priority16: null,
|
|
26
|
+
default: null, fallback: null
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Save initial state to context
|
|
30
|
+
context.set("priorities", priorities);
|
|
31
|
+
context.set("defaultValue", defaultValue);
|
|
32
|
+
context.set("fallbackValue", fallbackValue);
|
|
33
|
+
context.set("messages", messages);
|
|
34
|
+
|
|
35
|
+
node.on("input", function(msg, send, done) {
|
|
36
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
37
|
+
|
|
38
|
+
// Guard against invalid message
|
|
39
|
+
if (!msg) {
|
|
40
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
41
|
+
if (done) done();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Validate payload
|
|
46
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
47
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
48
|
+
if (done) done();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle keyed object payload for clearing
|
|
53
|
+
if (typeof msg.payload === "object" && msg.payload !== null && msg.payload.hasOwnProperty("clear")) {
|
|
54
|
+
const clear = msg.payload.clear;
|
|
55
|
+
if (clear === "all") {
|
|
56
|
+
priorities = {
|
|
57
|
+
priority1: null, priority2: null, priority3: null, priority4: null,
|
|
58
|
+
priority5: null, priority6: null, priority7: null, priority8: null,
|
|
59
|
+
priority9: null, priority10: null, priority11: null, priority12: null,
|
|
60
|
+
priority13: null, priority14: null, priority15: null, priority16: null
|
|
61
|
+
};
|
|
62
|
+
defaultValue = null;
|
|
63
|
+
fallbackValue = null;
|
|
64
|
+
messages = {
|
|
65
|
+
priority1: null, priority2: null, priority3: null, priority4: null,
|
|
66
|
+
priority5: null, priority6: null, priority7: null, priority8: null,
|
|
67
|
+
priority9: null, priority10: null, priority11: null, priority12: null,
|
|
68
|
+
priority13: null, priority14: null, priority15: null, priority16: null,
|
|
69
|
+
default: null, fallback: null
|
|
70
|
+
};
|
|
71
|
+
context.set("priorities", priorities);
|
|
72
|
+
context.set("defaultValue", defaultValue);
|
|
73
|
+
context.set("fallbackValue", fallbackValue);
|
|
74
|
+
context.set("messages", messages);
|
|
75
|
+
node.status({ fill: "green", shape: "dot", text: "all slots cleared" });
|
|
76
|
+
} else if (typeof clear === "string" && isValidSlot(clear)) {
|
|
77
|
+
if (clear.startsWith("priority")) priorities[clear] = null;
|
|
78
|
+
else if (clear === "default") defaultValue = null;
|
|
79
|
+
else if (clear === "fallback") fallbackValue = null;
|
|
80
|
+
messages[clear] = null;
|
|
81
|
+
context.set("priorities", priorities);
|
|
82
|
+
context.set("defaultValue", defaultValue);
|
|
83
|
+
context.set("fallbackValue", fallbackValue);
|
|
84
|
+
context.set("messages", messages);
|
|
85
|
+
node.status({ fill: "green", shape: "dot", text: `${clear} cleared` });
|
|
86
|
+
} else if (Array.isArray(clear) && clear.every(isValidSlot)) {
|
|
87
|
+
clear.forEach(slot => {
|
|
88
|
+
if (slot.startsWith("priority")) priorities[slot] = null;
|
|
89
|
+
else if (slot === "default") defaultValue = null;
|
|
90
|
+
else if (slot === "fallback") fallbackValue = null;
|
|
91
|
+
messages[slot] = null;
|
|
92
|
+
});
|
|
93
|
+
context.set("priorities", priorities);
|
|
94
|
+
context.set("defaultValue", defaultValue);
|
|
95
|
+
context.set("fallbackValue", fallbackValue);
|
|
96
|
+
context.set("messages", messages);
|
|
97
|
+
node.status({ fill: "green", shape: "dot", text: `${clear.join(", ")} cleared` });
|
|
98
|
+
} else {
|
|
99
|
+
node.status({ fill: "red", shape: "ring", text: "invalid clear" });
|
|
100
|
+
if (done) done();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
} else if (msg.payload === "clear") {
|
|
104
|
+
// Handle string "clear" with msg.context
|
|
105
|
+
if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
|
|
106
|
+
node.status({ fill: "red", shape: "ring", text: "missing or invalid context for clear" });
|
|
107
|
+
if (done) done();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const contextMsg = msg.context;
|
|
111
|
+
if (isValidSlot(contextMsg)) {
|
|
112
|
+
if (contextMsg.startsWith("priority")) priorities[contextMsg] = null;
|
|
113
|
+
else if (contextMsg === "default") defaultValue = null;
|
|
114
|
+
else if (contextMsg === "fallback") fallbackValue = null;
|
|
115
|
+
messages[contextMsg] = null;
|
|
116
|
+
context.set("priorities", priorities);
|
|
117
|
+
context.set("defaultValue", defaultValue);
|
|
118
|
+
context.set("fallbackValue", fallbackValue);
|
|
119
|
+
context.set("messages", messages);
|
|
120
|
+
node.status({ fill: "green", shape: "dot", text: `${contextMsg} cleared` });
|
|
121
|
+
} else {
|
|
122
|
+
node.status({ fill: "red", shape: "ring", text: "invalid clear context" });
|
|
123
|
+
if (done) done();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Handle non-object, non-"clear" payloads
|
|
128
|
+
if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
|
|
129
|
+
node.status({ fill: "red", shape: "ring", text: "missing or invalid context" });
|
|
130
|
+
if (done) done();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const contextMsg = msg.context;
|
|
135
|
+
const value = msg.payload === null ? null : typeof msg.payload === "number" ? parseFloat(msg.payload) : typeof msg.payload === "boolean" ? msg.payload : null;
|
|
136
|
+
|
|
137
|
+
if (value === null && msg.payload !== null) {
|
|
138
|
+
node.status({ fill: "red", shape: "ring", text: `invalid ${contextMsg}` });
|
|
139
|
+
if (done) done();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (/^priority([1-9]|1[0-6])$/.test(contextMsg)) {
|
|
144
|
+
priorities[contextMsg] = value;
|
|
145
|
+
messages[contextMsg] = RED.util.cloneMessage(msg);
|
|
146
|
+
context.set("priorities", priorities);
|
|
147
|
+
context.set("messages", messages);
|
|
148
|
+
node.status({
|
|
149
|
+
fill: "green",
|
|
150
|
+
shape: "dot",
|
|
151
|
+
text: value === null ? `${contextMsg} relinquished` : `${contextMsg}: ${typeof value === "number" ? value.toFixed(2) : value}`
|
|
152
|
+
});
|
|
153
|
+
} else if (contextMsg === "default") {
|
|
154
|
+
defaultValue = value;
|
|
155
|
+
messages[contextMsg] = RED.util.cloneMessage(msg);
|
|
156
|
+
context.set("defaultValue", defaultValue);
|
|
157
|
+
context.set("messages", messages);
|
|
158
|
+
node.status({
|
|
159
|
+
fill: "green",
|
|
160
|
+
shape: "dot",
|
|
161
|
+
text: value === null ? "default relinquished" : `default: ${typeof value === "number" ? value.toFixed(2) : value}`
|
|
162
|
+
});
|
|
163
|
+
} else if (contextMsg === "fallback") {
|
|
164
|
+
fallbackValue = value;
|
|
165
|
+
messages[contextMsg] = RED.util.cloneMessage(msg);
|
|
166
|
+
context.set("fallbackValue", fallbackValue);
|
|
167
|
+
context.set("messages", messages);
|
|
168
|
+
node.status({
|
|
169
|
+
fill: "green",
|
|
170
|
+
shape: "dot",
|
|
171
|
+
text: value === null ? "fallback relinquished" : `fallback: ${typeof value === "number" ? value.toFixed(2) : value}`
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
175
|
+
if (done) done("Unknown context");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Output highest priority message
|
|
181
|
+
const currentOutput = evaluatePriority();
|
|
182
|
+
send(currentOutput);
|
|
183
|
+
const inDisplay = typeof msg.payload === "number" ? msg.payload.toFixed(2) : typeof msg.payload === "object" ? JSON.stringify(msg.payload).slice(0, 20) : msg.payload;
|
|
184
|
+
const outDisplay = currentOutput.payload === null ? "null" : typeof currentOutput.payload === "number" ? currentOutput.payload.toFixed(2) : currentOutput.payload;
|
|
185
|
+
node.status({
|
|
186
|
+
fill: "blue",
|
|
187
|
+
shape: "dot",
|
|
188
|
+
text: `in: ${inDisplay}, out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (done) done();
|
|
192
|
+
|
|
193
|
+
function isValidSlot(slot) {
|
|
194
|
+
return /^priority([1-9]|1[0-6])$/.test(slot) || slot === "default" || slot === "fallback";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function evaluatePriority() {
|
|
198
|
+
let selectedValue = null;
|
|
199
|
+
let activePriority = null;
|
|
200
|
+
let selectedMessage = null;
|
|
201
|
+
|
|
202
|
+
// Check priorities from 1 to 16
|
|
203
|
+
for (let i = 1; i <= 16; i++) {
|
|
204
|
+
const key = `priority${i}`;
|
|
205
|
+
if (priorities[key] !== null) {
|
|
206
|
+
selectedValue = priorities[key];
|
|
207
|
+
activePriority = key;
|
|
208
|
+
selectedMessage = messages[key];
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fall back to default or fallback
|
|
214
|
+
if (selectedValue === null) {
|
|
215
|
+
if (defaultValue !== null) {
|
|
216
|
+
selectedValue = defaultValue;
|
|
217
|
+
activePriority = "default";
|
|
218
|
+
selectedMessage = messages.default;
|
|
219
|
+
} else if (fallbackValue !== null) {
|
|
220
|
+
selectedValue = fallbackValue;
|
|
221
|
+
activePriority = "fallback";
|
|
222
|
+
selectedMessage = messages.fallback;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Return the original message if available, otherwise a new message
|
|
227
|
+
const output = selectedMessage ? RED.util.cloneMessage(selectedMessage) : { payload: selectedValue };
|
|
228
|
+
output.diagnostics = { activePriority };
|
|
229
|
+
return output;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
node.on("close", function(done) {
|
|
234
|
+
done();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
RED.nodes.registerType("priority-block", PriorityBlockNode);
|
|
239
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="rate-limit-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
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-mode" title="Processing mode"><i class="fa fa-cog"></i> Mode</label>
|
|
9
|
+
<select id="node-input-mode">
|
|
10
|
+
<option value="rate-limit">Rate Limit</option>
|
|
11
|
+
<option value="threshold">Threshold</option>
|
|
12
|
+
<option value="full-value">Full Value</option>
|
|
13
|
+
</select>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="form-row rate-limit-field">
|
|
16
|
+
<label for="node-input-rate" title="Rate of change (units per second, > 0)"><i class="fa fa-tachometer"></i> Rate (units/s)</label>
|
|
17
|
+
<input type="number" id="node-input-rate" placeholder="1" min="0.01" step="0.01">
|
|
18
|
+
</div>
|
|
19
|
+
<div class="form-row rate-limit-field">
|
|
20
|
+
<label for="node-input-interval" title="Update interval (milliseconds, ≥ 10, integer)"><i class="fa fa-clock-o"></i> Interval (ms)</label>
|
|
21
|
+
<input type="number" id="node-input-interval" placeholder="100" min="10" step="10">
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-row threshold-field">
|
|
24
|
+
<label for="node-input-threshold" title="Threshold for output (units, ≥ 0)"><i class="fa fa-filter"></i> Threshold (units)</label>
|
|
25
|
+
<input type="number" id="node-input-threshold" placeholder="5" min="0" step="0.01">
|
|
26
|
+
</div>
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<!-- JavaScript Section -->
|
|
30
|
+
<script type="text/javascript">
|
|
31
|
+
RED.nodes.registerType("rate-limit-block", {
|
|
32
|
+
category: "control",
|
|
33
|
+
color: "#301934",
|
|
34
|
+
defaults: {
|
|
35
|
+
name: { value: "" },
|
|
36
|
+
mode: { value: "rate-limit", required: true, validate: function(v) { return ["rate-limit", "threshold", "full-value"].includes(v); } },
|
|
37
|
+
rate: { value: 1, required: false, validate: function(v) { return !v || (!isNaN(parseFloat(v)) && parseFloat(v) > 0 && isFinite(parseFloat(v))); } },
|
|
38
|
+
interval: { value: 100, required: false, validate: function(v) { return !v || (Number.isInteger(parseInt(v)) && parseInt(v) >= 10); } },
|
|
39
|
+
threshold: { value: 5, required: false, validate: function(v) { return !v || (!isNaN(parseFloat(v)) && parseFloat(v) >= 0 && isFinite(parseFloat(v))); } }
|
|
40
|
+
},
|
|
41
|
+
inputs: 1,
|
|
42
|
+
outputs: 1,
|
|
43
|
+
inputLabels: ["value"],
|
|
44
|
+
outputLabels: ["processed value"],
|
|
45
|
+
icon: "font-awesome/fa-tachometer-alt",
|
|
46
|
+
paletteLabel: "rate limit",
|
|
47
|
+
label: function() {
|
|
48
|
+
return this.name || this.mode || "rate limit";
|
|
49
|
+
},
|
|
50
|
+
oneditprepare: function() {
|
|
51
|
+
const node = this;
|
|
52
|
+
|
|
53
|
+
// Field visibility logic
|
|
54
|
+
function updateFieldVisibility() {
|
|
55
|
+
const mode = $("#node-input-mode").val();
|
|
56
|
+
$(".rate-limit-field").toggle(mode === "rate-limit");
|
|
57
|
+
$(".threshold-field").toggle(mode === "threshold");
|
|
58
|
+
}
|
|
59
|
+
$("#node-input-mode").on("change", updateFieldVisibility);
|
|
60
|
+
updateFieldVisibility();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<!-- Help Section -->
|
|
66
|
+
<script type="text/markdown" data-help-name="rate-limit-block">
|
|
67
|
+
Processes a numeric input value based on a selected mode and passes the original message.
|
|
68
|
+
|
|
69
|
+
### Inputs
|
|
70
|
+
: context (string) : Configures settings (`"mode"`, `"rate"`, `"interval"`, `"threshold"`). Unmatched values trigger error.
|
|
71
|
+
: payload (number | string | number) : Number to process, or value for configuration (string for mode, number for others).
|
|
72
|
+
|
|
73
|
+
### Outputs
|
|
74
|
+
: payload (number) : Processed value, depending on mode.
|
|
75
|
+
: Other properties (e.g., `msg.topic`) from the input message are preserved.
|
|
76
|
+
|
|
77
|
+
### Properties
|
|
78
|
+
: mode (string) : Processing mode.
|
|
79
|
+
: rate (number) : Rate of change for rate-limit mode.
|
|
80
|
+
: interval (number) : Update interval for rate-limit mode.
|
|
81
|
+
: threshold (number) : Threshold for output in threshold mode.
|
|
82
|
+
|
|
83
|
+
### Details
|
|
84
|
+
Processes `msg.payload` (number) in one of three modes:
|
|
85
|
+
- `Rate Limit` Limits rate of change to `rate` (units/s), updating every `interval` (ms) toward the input value.
|
|
86
|
+
- `Threshold` Outputs when input differs from last output by more than `threshold` (units).
|
|
87
|
+
- `Full Value` Passes input unchanged.
|
|
88
|
+
|
|
89
|
+
### Status
|
|
90
|
+
- Green (dot): Configuration update
|
|
91
|
+
- Blue (dot): State changed
|
|
92
|
+
- Blue (ring): State unchanged
|
|
93
|
+
- Red (ring): Error
|
|
94
|
+
- Yellow (ring): Warning
|
|
95
|
+
|
|
96
|
+
### References
|
|
97
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
98
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
99
|
+
</script>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function RateLimitBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
mode: config.mode,
|
|
10
|
+
rate: parseFloat(config.rate),
|
|
11
|
+
interval: parseInt(config.interval),
|
|
12
|
+
threshold: parseFloat(config.threshold),
|
|
13
|
+
currentValue: 0,
|
|
14
|
+
targetValue: 0,
|
|
15
|
+
lastUpdate: Date.now(),
|
|
16
|
+
lastInputMsg: null
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Validate initial config
|
|
20
|
+
if (isNaN(node.runtime.rate) || node.runtime.rate <= 0 || !isFinite(node.runtime.rate)) {
|
|
21
|
+
node.runtime.rate = 1.0;
|
|
22
|
+
node.status({ fill: "red", shape: "ring", text: "invalid rate" });
|
|
23
|
+
}
|
|
24
|
+
if (isNaN(node.runtime.interval) || node.runtime.interval < 10 || !Number.isInteger(node.runtime.interval)) {
|
|
25
|
+
node.runtime.interval = 100;
|
|
26
|
+
node.status({ fill: "red", shape: "ring", text: "invalid interval" });
|
|
27
|
+
}
|
|
28
|
+
if (isNaN(node.runtime.threshold) || node.runtime.threshold < 0 || !isFinite(node.runtime.threshold)) {
|
|
29
|
+
node.runtime.threshold = 5.0;
|
|
30
|
+
node.status({ fill: "red", shape: "ring", text: "invalid threshold" });
|
|
31
|
+
}
|
|
32
|
+
if (!["rate-limit", "threshold", "full-value"].includes(node.runtime.mode)) {
|
|
33
|
+
node.runtime.mode = "rate-limit";
|
|
34
|
+
node.status({ fill: "red", shape: "ring", text: "invalid mode" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Set initial status
|
|
38
|
+
node.status({ fill: "blue", shape: "dot", text: `mode: ${node.runtime.mode}, out: ${node.runtime.currentValue.toFixed(2)}` });
|
|
39
|
+
|
|
40
|
+
let updateTimer = null;
|
|
41
|
+
|
|
42
|
+
// Function to update output for rate-limit mode
|
|
43
|
+
function updateRateLimitOutput() {
|
|
44
|
+
if (!node.runtime.lastInputMsg) return;
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const elapsed = (now - node.runtime.lastUpdate) / 1000; // Seconds
|
|
47
|
+
const maxChange = node.runtime.rate * elapsed;
|
|
48
|
+
let newValue = node.runtime.currentValue;
|
|
49
|
+
|
|
50
|
+
if (node.runtime.currentValue < node.runtime.targetValue) {
|
|
51
|
+
newValue = Math.min(node.runtime.currentValue + maxChange, node.runtime.targetValue);
|
|
52
|
+
} else if (node.runtime.currentValue > node.runtime.targetValue) {
|
|
53
|
+
newValue = Math.max(node.runtime.currentValue - maxChange, node.runtime.targetValue);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (newValue !== node.runtime.currentValue) {
|
|
57
|
+
node.runtime.currentValue = newValue;
|
|
58
|
+
node.runtime.lastUpdate = now;
|
|
59
|
+
const msg = RED.util.cloneMessage(node.runtime.lastInputMsg);
|
|
60
|
+
msg.payload = node.runtime.currentValue;
|
|
61
|
+
node.status({
|
|
62
|
+
fill: "blue",
|
|
63
|
+
shape: "dot",
|
|
64
|
+
text: `mode: rate-limit, out: ${node.runtime.currentValue.toFixed(2)}`
|
|
65
|
+
});
|
|
66
|
+
node.send(msg);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Start update timer for rate-limit mode
|
|
71
|
+
function startTimer() {
|
|
72
|
+
if (updateTimer) clearInterval(updateTimer);
|
|
73
|
+
if (node.runtime.mode === "rate-limit") {
|
|
74
|
+
updateTimer = setInterval(updateRateLimitOutput, node.runtime.interval);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
node.on("input", function(msg, send, done) {
|
|
79
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
80
|
+
|
|
81
|
+
// Guard against invalid message
|
|
82
|
+
if (!msg) {
|
|
83
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
84
|
+
if (done) done();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle context updates
|
|
89
|
+
if (msg.hasOwnProperty("context")) {
|
|
90
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
91
|
+
node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
|
|
92
|
+
if (done) done();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
switch (msg.context) {
|
|
96
|
+
case "mode":
|
|
97
|
+
if (!["rate-limit", "threshold", "full-value"].includes(msg.payload)) {
|
|
98
|
+
node.status({ fill: "red", shape: "ring", text: "invalid mode" });
|
|
99
|
+
if (done) done();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
node.runtime.mode = msg.payload;
|
|
103
|
+
startTimer();
|
|
104
|
+
node.status({
|
|
105
|
+
fill: "green",
|
|
106
|
+
shape: "dot",
|
|
107
|
+
text: `mode: ${node.runtime.mode}`
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
case "rate":
|
|
111
|
+
const rate = parseFloat(msg.payload);
|
|
112
|
+
if (isNaN(rate) || rate <= 0 || !isFinite(rate)) {
|
|
113
|
+
node.status({ fill: "red", shape: "ring", text: "invalid rate" });
|
|
114
|
+
if (done) done();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
node.runtime.rate = rate;
|
|
118
|
+
node.status({
|
|
119
|
+
fill: "green",
|
|
120
|
+
shape: "dot",
|
|
121
|
+
text: `rate: ${node.runtime.rate.toFixed(2)}`
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
case "interval":
|
|
125
|
+
const interval = parseInt(msg.payload);
|
|
126
|
+
if (isNaN(interval) || interval < 10 || !Number.isInteger(interval)) {
|
|
127
|
+
node.status({ fill: "red", shape: "ring", text: "invalid interval" });
|
|
128
|
+
if (done) done();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
node.runtime.interval = interval;
|
|
132
|
+
startTimer();
|
|
133
|
+
node.status({
|
|
134
|
+
fill: "green",
|
|
135
|
+
shape: "dot",
|
|
136
|
+
text: `interval: ${node.runtime.interval}`
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
case "threshold":
|
|
140
|
+
const threshold = parseFloat(msg.payload);
|
|
141
|
+
if (isNaN(threshold) || threshold < 0 || !isFinite(threshold)) {
|
|
142
|
+
node.status({ fill: "red", shape: "ring", text: "invalid threshold" });
|
|
143
|
+
if (done) done();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
node.runtime.threshold = threshold;
|
|
147
|
+
node.status({
|
|
148
|
+
fill: "green",
|
|
149
|
+
shape: "dot",
|
|
150
|
+
text: `threshold: ${node.runtime.threshold.toFixed(2)}`
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
155
|
+
if (done) done("Unknown context");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (done) done();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate input
|
|
163
|
+
if (typeof msg.payload !== "number" || isNaN(msg.payload) || !isFinite(msg.payload)) {
|
|
164
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
165
|
+
if (done) done();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const inputValue = msg.payload;
|
|
170
|
+
node.runtime.lastInputMsg = RED.util.cloneMessage(msg);
|
|
171
|
+
|
|
172
|
+
if (node.runtime.mode === "rate-limit") {
|
|
173
|
+
node.runtime.targetValue = inputValue;
|
|
174
|
+
node.status({
|
|
175
|
+
fill: "green",
|
|
176
|
+
shape: "dot",
|
|
177
|
+
text: `mode: rate-limit, target: ${node.runtime.targetValue.toFixed(2)}`
|
|
178
|
+
});
|
|
179
|
+
updateRateLimitOutput();
|
|
180
|
+
startTimer();
|
|
181
|
+
} else if (node.runtime.mode === "threshold") {
|
|
182
|
+
const diff = Math.abs(inputValue - node.runtime.currentValue);
|
|
183
|
+
node.runtime.currentValue = inputValue;
|
|
184
|
+
if (diff > node.runtime.threshold) {
|
|
185
|
+
msg.payload = inputValue;
|
|
186
|
+
node.status({
|
|
187
|
+
fill: "blue",
|
|
188
|
+
shape: "dot",
|
|
189
|
+
text: `mode: threshold, out: ${node.runtime.currentValue.toFixed(2)}`
|
|
190
|
+
});
|
|
191
|
+
send(msg);
|
|
192
|
+
} else {
|
|
193
|
+
node.status({
|
|
194
|
+
fill: "blue",
|
|
195
|
+
shape: "ring",
|
|
196
|
+
text: `mode: threshold, out: ${node.runtime.currentValue.toFixed(2)}`
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} else if (node.runtime.mode === "full-value") {
|
|
200
|
+
node.runtime.currentValue = inputValue;
|
|
201
|
+
msg.payload = inputValue;
|
|
202
|
+
node.status({
|
|
203
|
+
fill: "blue",
|
|
204
|
+
shape: "dot",
|
|
205
|
+
text: `mode: full-value, out: ${node.runtime.currentValue.toFixed(2)}`
|
|
206
|
+
});
|
|
207
|
+
send(msg);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (done) done();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
node.on("close", function(done) {
|
|
214
|
+
if (updateTimer) clearInterval(updateTimer);
|
|
215
|
+
updateTimer = null;
|
|
216
|
+
done();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
RED.nodes.registerType("rate-limit-block", RateLimitBlockNode);
|
|
221
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="round-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
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-precision" title="Rounding precision (0.1 for tenths, 0.5 for halves, 1.0 for whole numbers)"><i class="fa fa-ruler"></i> Precision</label>
|
|
9
|
+
<select id="node-input-precision">
|
|
10
|
+
<option value="0.1">0.1 (tenths)</option>
|
|
11
|
+
<option value="0.5">0.5 (halves)</option>
|
|
12
|
+
<option value="1.0">1.0 (whole)</option>
|
|
13
|
+
</select>
|
|
14
|
+
</div>
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- JavaScript Section -->
|
|
18
|
+
<script type="text/javascript">
|
|
19
|
+
RED.nodes.registerType("round-block", {
|
|
20
|
+
category: "control",
|
|
21
|
+
color: "#301934",
|
|
22
|
+
defaults: {
|
|
23
|
+
name: { value: "" },
|
|
24
|
+
precision: { value: "1.0", required: true }
|
|
25
|
+
},
|
|
26
|
+
inputs: 1,
|
|
27
|
+
outputs: 1,
|
|
28
|
+
inputLabels: ["input"],
|
|
29
|
+
outputLabels: ["rounded"],
|
|
30
|
+
icon: "font-awesome/fa-circle-o-notch",
|
|
31
|
+
paletteLabel: "round",
|
|
32
|
+
label: function() {
|
|
33
|
+
return this.name || "";
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!-- Help Section -->
|
|
39
|
+
<script type="text/markdown" data-help-name="round-block">
|
|
40
|
+
Rounds a float in `msg.payload` to the nearest configurable precision (tenth, half, or whole number).
|
|
41
|
+
|
|
42
|
+
### Inputs
|
|
43
|
+
: payload (number) : Float to round.
|
|
44
|
+
: context (string, optional) : Action (`"precision"` to set precision). Unknown `msg.context` values are ignored.
|
|
45
|
+
: payload (string, for `"precision"`) : Precision value (`"0.1"`, `"0.5"`, `"1.0"`).
|
|
46
|
+
|
|
47
|
+
### Outputs
|
|
48
|
+
: msg : Original message with `msg.payload` rounded to configured precision.
|
|
49
|
+
|
|
50
|
+
### Properties
|
|
51
|
+
: name (string) : Display name in editor.
|
|
52
|
+
: precision (string) : Rounding precision (`"0.1"`, `"0.5"`, `"1.0"`).
|
|
53
|
+
|
|
54
|
+
### Details
|
|
55
|
+
Rounds a float in `msg.payload` to the nearest tenth (0.1), half (0.5), or whole number (1.0), set via editor or `msg.context = "precision"`.
|
|
56
|
+
|
|
57
|
+
Operates as a passthrough node, modifying `msg.payload` and forwarding the original message.
|
|
58
|
+
|
|
59
|
+
If `msg.payload` is not a finite number, the message is passed unchanged.
|
|
60
|
+
|
|
61
|
+
Unknown `msg.context` values are ignored.
|
|
62
|
+
|
|
63
|
+
### Status
|
|
64
|
+
- Green (dot): Configuration update
|
|
65
|
+
- Blue (dot): State changed
|
|
66
|
+
- Blue (ring): State unchanged
|
|
67
|
+
- Red (ring): Error
|
|
68
|
+
- Yellow (ring): Warning
|
|
69
|
+
|
|
70
|
+
### References
|
|
71
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
72
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
73
|
+
</script>
|