@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,289 @@
|
|
|
1
|
+
const validConversions = [
|
|
2
|
+
"F to C",
|
|
3
|
+
"C to F",
|
|
4
|
+
"K to C",
|
|
5
|
+
"C to K",
|
|
6
|
+
"K to F",
|
|
7
|
+
"F to K",
|
|
8
|
+
"R to F",
|
|
9
|
+
"F to R",
|
|
10
|
+
"decimal to %",
|
|
11
|
+
"% to decimal",
|
|
12
|
+
"Pa to inH₂O",
|
|
13
|
+
"inH₂O to Pa",
|
|
14
|
+
"Pa to inHg",
|
|
15
|
+
"inHg to Pa",
|
|
16
|
+
"Pa to bar",
|
|
17
|
+
"bar to Pa",
|
|
18
|
+
"Pa to psi",
|
|
19
|
+
"psi to Pa",
|
|
20
|
+
"m to ft",
|
|
21
|
+
"ft to m",
|
|
22
|
+
"m to in",
|
|
23
|
+
"in to m",
|
|
24
|
+
"mm to in",
|
|
25
|
+
"in to mm",
|
|
26
|
+
"kg to lb",
|
|
27
|
+
"lb to kg",
|
|
28
|
+
"L to gal",
|
|
29
|
+
"gal to L",
|
|
30
|
+
"kW to hp",
|
|
31
|
+
"hp to kW",
|
|
32
|
+
"rad to deg",
|
|
33
|
+
"deg to rad",
|
|
34
|
+
"s to min",
|
|
35
|
+
"min to s"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
module.exports = function(RED) {
|
|
39
|
+
function ConvertBlockNode(config) {
|
|
40
|
+
RED.nodes.createNode(this, config);
|
|
41
|
+
const node = this;
|
|
42
|
+
|
|
43
|
+
// Initialize runtime state
|
|
44
|
+
node.runtime = {
|
|
45
|
+
conversion: validConversions.includes(config.conversion) ? config.conversion : "C to F"
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
node.on("input", function(msg, send, done) {
|
|
49
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
50
|
+
|
|
51
|
+
// Guard against invalid message
|
|
52
|
+
if (!msg) {
|
|
53
|
+
node.status({ fill: "red", shape: "ring", text: "missing message" });
|
|
54
|
+
if (done) done();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle configuration messages
|
|
59
|
+
if (msg.hasOwnProperty("context")) {
|
|
60
|
+
if (typeof msg.context !== "string") {
|
|
61
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
62
|
+
if (done) done();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (msg.context === "conversion") {
|
|
66
|
+
if (!msg.hasOwnProperty("payload") || !validConversions.includes(msg.payload)) {
|
|
67
|
+
node.status({ fill: "red", shape: "ring", text: "invalid conversion" });
|
|
68
|
+
if (done) done();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
node.runtime.conversion = msg.payload;
|
|
72
|
+
node.status({ fill: "green", shape: "dot", text: `conversion: ${node.runtime.conversion}` });
|
|
73
|
+
if (done) done();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (done) done();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate input payload
|
|
82
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
83
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
84
|
+
if (done) done();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const inputValue = parseFloat(msg.payload);
|
|
89
|
+
if (isNaN(inputValue) || !isFinite(inputValue)) {
|
|
90
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
91
|
+
if (done) done();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Perform conversion
|
|
96
|
+
let output, inUnit, outUnit;
|
|
97
|
+
switch (node.runtime.conversion) {
|
|
98
|
+
case "F to C":
|
|
99
|
+
output = (msg.payload - 32) * 5 / 9;
|
|
100
|
+
inUnit = "°F";
|
|
101
|
+
outUnit = "°C";
|
|
102
|
+
break;
|
|
103
|
+
case "C to F":
|
|
104
|
+
output = (msg.payload * 9 / 5) + 32;
|
|
105
|
+
inUnit = "°C";
|
|
106
|
+
outUnit = "°F";
|
|
107
|
+
break;
|
|
108
|
+
case "decimal to %":
|
|
109
|
+
output = msg.payload * 100;
|
|
110
|
+
inUnit = "";
|
|
111
|
+
outUnit = "%";
|
|
112
|
+
break;
|
|
113
|
+
case "% to decimal":
|
|
114
|
+
output = msg.payload / 100;
|
|
115
|
+
inUnit = "%";
|
|
116
|
+
outUnit = "";
|
|
117
|
+
break;
|
|
118
|
+
case "Pa to inH₂O":
|
|
119
|
+
output = msg.payload * 0.00401463;
|
|
120
|
+
inUnit = "Pa";
|
|
121
|
+
outUnit = "inH₂O";
|
|
122
|
+
break;
|
|
123
|
+
case "inH₂O to Pa":
|
|
124
|
+
output = msg.payload / 0.00401463;
|
|
125
|
+
inUnit = "inH₂O";
|
|
126
|
+
outUnit = "Pa";
|
|
127
|
+
break;
|
|
128
|
+
case "Pa to inHg":
|
|
129
|
+
output = msg.payload * 0.0002953;
|
|
130
|
+
inUnit = "Pa";
|
|
131
|
+
outUnit = "inHg";
|
|
132
|
+
break;
|
|
133
|
+
case "inHg to Pa":
|
|
134
|
+
output = msg.payload / 0.0002953;
|
|
135
|
+
inUnit = "inHg";
|
|
136
|
+
outUnit = "Pa";
|
|
137
|
+
break;
|
|
138
|
+
case "Pa to bar":
|
|
139
|
+
output = msg.payload * 0.00001;
|
|
140
|
+
inUnit = "Pa";
|
|
141
|
+
outUnit = "bar";
|
|
142
|
+
break;
|
|
143
|
+
case "bar to Pa":
|
|
144
|
+
output = msg.payload / 0.00001;
|
|
145
|
+
inUnit = "bar";
|
|
146
|
+
outUnit = "Pa";
|
|
147
|
+
break;
|
|
148
|
+
case "Pa to psi":
|
|
149
|
+
output = msg.payload * 0.000145038;
|
|
150
|
+
inUnit = "Pa";
|
|
151
|
+
outUnit = "psi";
|
|
152
|
+
break;
|
|
153
|
+
case "psi to Pa":
|
|
154
|
+
output = msg.payload / 0.000145038;
|
|
155
|
+
inUnit = "psi";
|
|
156
|
+
outUnit = "Pa";
|
|
157
|
+
break;
|
|
158
|
+
case "K to C":
|
|
159
|
+
output = inputValue - 273.15;
|
|
160
|
+
inUnit = "K";
|
|
161
|
+
outUnit = "°C";
|
|
162
|
+
break;
|
|
163
|
+
case "C to K":
|
|
164
|
+
output = inputValue + 273.15;
|
|
165
|
+
inUnit = "°C";
|
|
166
|
+
outUnit = "K";
|
|
167
|
+
break;
|
|
168
|
+
case "K to F":
|
|
169
|
+
output = (inputValue * 9/5) - 459.67;
|
|
170
|
+
inUnit = "K";
|
|
171
|
+
outUnit = "°F";
|
|
172
|
+
break;
|
|
173
|
+
case "F to K":
|
|
174
|
+
output = (inputValue + 459.67) * 5/9;
|
|
175
|
+
inUnit = "°F";
|
|
176
|
+
outUnit = "K";
|
|
177
|
+
break;
|
|
178
|
+
case "R to F":
|
|
179
|
+
output = inputValue - 459.67;
|
|
180
|
+
inUnit = "°R";
|
|
181
|
+
outUnit = "°F";
|
|
182
|
+
break;
|
|
183
|
+
case "F to R":
|
|
184
|
+
output = inputValue + 459.67;
|
|
185
|
+
inUnit = "°F";
|
|
186
|
+
outUnit = "°R";
|
|
187
|
+
break;
|
|
188
|
+
case "m to ft":
|
|
189
|
+
output = inputValue * 3.28084;
|
|
190
|
+
inUnit = "m";
|
|
191
|
+
outUnit = "ft";
|
|
192
|
+
break;
|
|
193
|
+
case "ft to m":
|
|
194
|
+
output = inputValue / 3.28084;
|
|
195
|
+
inUnit = "ft";
|
|
196
|
+
outUnit = "m";
|
|
197
|
+
break;
|
|
198
|
+
case "m to in":
|
|
199
|
+
output = inputValue * 39.3701;
|
|
200
|
+
inUnit = "m";
|
|
201
|
+
outUnit = "in";
|
|
202
|
+
break;
|
|
203
|
+
case "in to m":
|
|
204
|
+
output = inputValue / 39.3701;
|
|
205
|
+
inUnit = "in";
|
|
206
|
+
outUnit = "m";
|
|
207
|
+
break;
|
|
208
|
+
case "mm to in":
|
|
209
|
+
output = inputValue / 25.4;
|
|
210
|
+
inUnit = "mm";
|
|
211
|
+
outUnit = "in";
|
|
212
|
+
break;
|
|
213
|
+
case "in to mm":
|
|
214
|
+
output = inputValue * 25.4;
|
|
215
|
+
inUnit = "in";
|
|
216
|
+
outUnit = "mm";
|
|
217
|
+
break;
|
|
218
|
+
case "kg to lb":
|
|
219
|
+
output = inputValue * 2.20462;
|
|
220
|
+
inUnit = "kg";
|
|
221
|
+
outUnit = "lb";
|
|
222
|
+
break;
|
|
223
|
+
case "lb to kg":
|
|
224
|
+
output = inputValue / 2.20462;
|
|
225
|
+
inUnit = "lb";
|
|
226
|
+
outUnit = "kg";
|
|
227
|
+
break;
|
|
228
|
+
case "L to gal":
|
|
229
|
+
output = inputValue * 0.264172;
|
|
230
|
+
inUnit = "L";
|
|
231
|
+
outUnit = "gal";
|
|
232
|
+
break;
|
|
233
|
+
case "gal to L":
|
|
234
|
+
output = inputValue / 0.264172;
|
|
235
|
+
inUnit = "gal";
|
|
236
|
+
outUnit = "L";
|
|
237
|
+
break;
|
|
238
|
+
case "kW to hp":
|
|
239
|
+
output = inputValue * 1.34102;
|
|
240
|
+
inUnit = "kW";
|
|
241
|
+
outUnit = "hp";
|
|
242
|
+
break;
|
|
243
|
+
case "hp to kW":
|
|
244
|
+
output = inputValue / 1.34102;
|
|
245
|
+
inUnit = "hp";
|
|
246
|
+
outUnit = "kW";
|
|
247
|
+
break;
|
|
248
|
+
case "rad to deg":
|
|
249
|
+
output = inputValue * (180 / Math.PI);
|
|
250
|
+
inUnit = "rad";
|
|
251
|
+
outUnit = "°";
|
|
252
|
+
break;
|
|
253
|
+
case "deg to rad":
|
|
254
|
+
output = inputValue * (Math.PI / 180);
|
|
255
|
+
inUnit = "°";
|
|
256
|
+
outUnit = "rad";
|
|
257
|
+
break;
|
|
258
|
+
case "s to min":
|
|
259
|
+
output = inputValue / 60;
|
|
260
|
+
inUnit = "s";
|
|
261
|
+
outUnit = "min";
|
|
262
|
+
break;
|
|
263
|
+
case "min to s":
|
|
264
|
+
output = inputValue * 60;
|
|
265
|
+
inUnit = "min";
|
|
266
|
+
outUnit = "s";
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Format status numbers
|
|
271
|
+
const inDisplay = msg.payload % 1 === 0 ? msg.payload : msg.payload.toFixed(2);
|
|
272
|
+
const outDisplay = output % 1 === 0 ? output : output.toFixed(2);
|
|
273
|
+
|
|
274
|
+
// Update status and send output
|
|
275
|
+
node.status({ fill: "blue", shape: "dot", text: `${inDisplay} ${inUnit} → ${outDisplay} ${outUnit}` });
|
|
276
|
+
|
|
277
|
+
msg.payload = output;
|
|
278
|
+
send(msg);
|
|
279
|
+
|
|
280
|
+
if (done) done();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
node.on("close", function(done) {
|
|
284
|
+
done();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
RED.nodes.registerType("convert-block", ConvertBlockNode);
|
|
289
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="count-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: Registers the node and handles editor logic -->
|
|
10
|
+
<script type="text/javascript">
|
|
11
|
+
RED.nodes.registerType("count-block", {
|
|
12
|
+
category: "control",
|
|
13
|
+
color: "#301934",
|
|
14
|
+
defaults: {
|
|
15
|
+
name: { value: "" }
|
|
16
|
+
},
|
|
17
|
+
inputs: 1,
|
|
18
|
+
outputs: 1,
|
|
19
|
+
inputLabels: ["input"],
|
|
20
|
+
outputLabels: ["count"],
|
|
21
|
+
icon: "font-awesome/fa-tally",
|
|
22
|
+
paletteLabel: "count",
|
|
23
|
+
label: function() {
|
|
24
|
+
return this.name || "count";
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<!-- Help Section -->
|
|
30
|
+
<script type="text/markdown" data-help-name="count-block">
|
|
31
|
+
Counts boolean rising edges in the input.
|
|
32
|
+
|
|
33
|
+
### Inputs
|
|
34
|
+
: context (string) : Resets count (`"reset"`). Unmatched values trigger error.
|
|
35
|
+
: payload (boolean) : Input boolean to detect rising edges.
|
|
36
|
+
|
|
37
|
+
### Outputs
|
|
38
|
+
: payload (number) : Current count of rising edges.
|
|
39
|
+
|
|
40
|
+
### Properties
|
|
41
|
+
: name (string) : Display name in editor.
|
|
42
|
+
|
|
43
|
+
### Details
|
|
44
|
+
Counts rising edges (false-to-true transitions) in `msg.payload` (boolean), incrementing the count on each transition.
|
|
45
|
+
Resets count to 0 via `msg.context = "reset"` with `msg.payload = true`.
|
|
46
|
+
|
|
47
|
+
### Status
|
|
48
|
+
- Green (dot): Configuration
|
|
49
|
+
- Blue (dot): Output, no alarm
|
|
50
|
+
- Red (dot): Output with alarm
|
|
51
|
+
- Red (ring): Errors
|
|
52
|
+
- Yellow (ring): Unknown context
|
|
53
|
+
|
|
54
|
+
### References
|
|
55
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
56
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function CountBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
count: 0,
|
|
10
|
+
prevState: false
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
node.on("input", function(msg, send, done) {
|
|
14
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
15
|
+
|
|
16
|
+
// Guard against invalid message
|
|
17
|
+
if (!msg) {
|
|
18
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
19
|
+
if (done) done();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle context updates
|
|
24
|
+
if (msg.hasOwnProperty("context")) {
|
|
25
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
26
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload for reset" });
|
|
27
|
+
if (done) done();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (msg.context === "reset") {
|
|
31
|
+
if (typeof msg.payload !== "boolean") {
|
|
32
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
33
|
+
if (done) done();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (msg.payload === true) {
|
|
37
|
+
node.runtime.count = 0;
|
|
38
|
+
node.runtime.prevState = false;
|
|
39
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
40
|
+
send({ payload: node.runtime.count });
|
|
41
|
+
}
|
|
42
|
+
if (done) done();
|
|
43
|
+
return;
|
|
44
|
+
} else {
|
|
45
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
46
|
+
if (done) done("Unknown context");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate input
|
|
52
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
53
|
+
node.status({ fill: "red", shape: "ring", text: "missing input" });
|
|
54
|
+
if (done) done();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const inputValue = msg.payload;
|
|
59
|
+
if (typeof inputValue !== "boolean") {
|
|
60
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
61
|
+
if (done) done();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Prevent integer overflow
|
|
66
|
+
if (node.runtime.count > Number.MAX_SAFE_INTEGER - 100000) {
|
|
67
|
+
node.runtime.count = 0;
|
|
68
|
+
node.status({ fill: "yellow", shape: "ring", text: "count overflow reset" });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Increment on false → true transition
|
|
72
|
+
if (!node.runtime.prevState && inputValue === true) {
|
|
73
|
+
node.runtime.count++;
|
|
74
|
+
node.status({ fill: "blue", shape: "dot", text: `count: ${node.runtime.count}` });
|
|
75
|
+
send({ payload: node.runtime.count });
|
|
76
|
+
} else {
|
|
77
|
+
node.status({ fill: "blue", shape: "ring", text: `count: ${node.runtime.count}` });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Update prevState
|
|
81
|
+
node.runtime.prevState = inputValue;
|
|
82
|
+
|
|
83
|
+
if (done) done();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
node.on("close", function(done) {
|
|
87
|
+
done();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
RED.nodes.registerType("count-block", CountBlockNode);
|
|
92
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="debounce-block">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-period" title="Filter period in milliseconds (positive number, e.g., 1000)"><i class="fa fa-clock-o"></i> Filter Period (ms)</label>
|
|
4
|
+
<input type="text" id="node-input-period" placeholder="1000" min="0.001" step="any">
|
|
5
|
+
<input type="hidden" id="node-input-periodType">
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script type="text/javascript">
|
|
11
|
+
RED.nodes.registerType("debounce-block", {
|
|
12
|
+
category: "control",
|
|
13
|
+
color: "#301934",
|
|
14
|
+
defaults: {
|
|
15
|
+
name: { value: "" },
|
|
16
|
+
period: {
|
|
17
|
+
value: 1000,
|
|
18
|
+
required: true,
|
|
19
|
+
validate: function() { return true; }
|
|
20
|
+
},
|
|
21
|
+
periodType: { value: "num" }
|
|
22
|
+
},
|
|
23
|
+
inputs: 1,
|
|
24
|
+
outputs: 1,
|
|
25
|
+
inputLabels: ["input"],
|
|
26
|
+
outputLabels: ["output"],
|
|
27
|
+
icon: "font-awesome/fa-hourglass-half",
|
|
28
|
+
paletteLabel: "debounce",
|
|
29
|
+
label: function() {
|
|
30
|
+
return this.name || "debounce";
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<script type="text/markdown" data-help-name="debounce-block">
|
|
36
|
+
Debounces consecutive `true` payloads and passes `false` payloads immediately with a configurable filter period.
|
|
37
|
+
|
|
38
|
+
### Inputs
|
|
39
|
+
: context (string) : Configures filter period (`"period"`).
|
|
40
|
+
: payload (boolean | number) : `true` for debounced output, `false` for immediate output.
|
|
41
|
+
|
|
42
|
+
### Outputs
|
|
43
|
+
: payload (boolean) : `true` after filter period elapses without new `true` payloads, `false` immediately. `msg.context` is consumed.
|
|
44
|
+
|
|
45
|
+
### Details
|
|
46
|
+
Filters consecutive `true` `msg.payload` inputs, outputting `true` after the filter period elapses without new `true` payloads.
|
|
47
|
+
|
|
48
|
+
Consecutive `true` payloads within the period reset the timer, delaying output. Outputs `false` `msg.payload` inputs immediately without debouncing.
|
|
49
|
+
|
|
50
|
+
Non-boolean payloads (e.g., strings, numbers) are ignored with an error status.
|
|
51
|
+
|
|
52
|
+
Tracks ignored `true` payloads, which increments for each `true` payload resetting an active timer, capped at 9999, resetting to 0 if exceeded or on redeployment.
|
|
53
|
+
|
|
54
|
+
### Status
|
|
55
|
+
- Green (dot): Configuration
|
|
56
|
+
- Blue (dot): Output, no alarm
|
|
57
|
+
- Red (dot): Output with alarm
|
|
58
|
+
- Red (ring): Errors
|
|
59
|
+
- Yellow (ring): Unknown context
|
|
60
|
+
|
|
61
|
+
### References
|
|
62
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
63
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
64
|
+
</script>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function DebounceBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime for editor display
|
|
7
|
+
node.runtime = {
|
|
8
|
+
debounceCount: 0
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Initialize state
|
|
12
|
+
let debounceTimer = null;
|
|
13
|
+
let lastOutput = null;
|
|
14
|
+
|
|
15
|
+
node.on("input", function(msg, send, done) {
|
|
16
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
17
|
+
|
|
18
|
+
// Evaluate all properties
|
|
19
|
+
try {
|
|
20
|
+
node.runtime.period = RED.util.evaluateNodeProperty(
|
|
21
|
+
config.period, config.periodType, node, msg
|
|
22
|
+
);
|
|
23
|
+
node.runtime.period = parseFloat(node.runtime.period);
|
|
24
|
+
|
|
25
|
+
node.period = parseFloat(node.period);
|
|
26
|
+
if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
|
|
27
|
+
node.period = 1000;
|
|
28
|
+
node.status({ fill: "yellow", shape: "ring", text: "invalid period, using 1000ms" });
|
|
29
|
+
}
|
|
30
|
+
} catch(err) {
|
|
31
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
32
|
+
if (done) done(err);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Guard against invalid msg
|
|
37
|
+
if (!msg) {
|
|
38
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
39
|
+
if (done) done();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Handle msg.context
|
|
44
|
+
if (msg.hasOwnProperty("context")) {
|
|
45
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
46
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
47
|
+
if (done) done();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (msg.context === "period") {
|
|
52
|
+
const newPeriod = parseFloat(msg.payload);
|
|
53
|
+
if (isNaN(newPeriod) || newPeriod <= 0) {
|
|
54
|
+
node.status({ fill: "red", shape: "ring", text: "invalid period" });
|
|
55
|
+
if (done) done();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
node.runtime.period = newPeriod;
|
|
59
|
+
node.status({
|
|
60
|
+
fill: "green",
|
|
61
|
+
shape: "dot",
|
|
62
|
+
text: `period: ${newPeriod.toFixed(0)} ms, bounced: ${node.runtime.debounceCount}`
|
|
63
|
+
});
|
|
64
|
+
if (done) done();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
69
|
+
if (done) done();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for missing payload
|
|
74
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
75
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
76
|
+
if (done) done();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Process false payloads immediately
|
|
81
|
+
if (msg.payload === false) {
|
|
82
|
+
const statusText = `in: false, out: false, bounced: ${node.runtime.debounceCount}`;
|
|
83
|
+
if (lastOutput === false) {
|
|
84
|
+
node.status({ fill: "blue", shape: "ring", text: statusText });
|
|
85
|
+
} else {
|
|
86
|
+
node.status({ fill: "blue", shape: "dot", text: statusText });
|
|
87
|
+
}
|
|
88
|
+
lastOutput = false;
|
|
89
|
+
delete msg.context;
|
|
90
|
+
send(msg);
|
|
91
|
+
if (done) done();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Process true payloads with debouncing
|
|
96
|
+
if (msg.payload === true) {
|
|
97
|
+
// Increment debounce counter if resetting an active timer
|
|
98
|
+
if (debounceTimer) {
|
|
99
|
+
node.runtime.debounceCount++;
|
|
100
|
+
if (node.runtime.debounceCount > 9999) {
|
|
101
|
+
node.runtime.debounceCount = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Clear existing timer
|
|
106
|
+
if (debounceTimer) {
|
|
107
|
+
clearTimeout(debounceTimer);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Set new debounce timer
|
|
111
|
+
debounceTimer = setTimeout(() => {
|
|
112
|
+
debounceTimer = null;
|
|
113
|
+
const statusText = `in: true, out: true, bounced: ${node.runtime.debounceCount}`;
|
|
114
|
+
node.status({ fill: "blue", shape: "dot", text: statusText });
|
|
115
|
+
lastOutput = true;
|
|
116
|
+
delete msg.context;
|
|
117
|
+
send(msg);
|
|
118
|
+
}, node.runtime.period);
|
|
119
|
+
|
|
120
|
+
if (done) done();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ignore non-boolean payloads
|
|
125
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
126
|
+
if (done) done();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
node.on("close", function(done) {
|
|
130
|
+
if (debounceTimer) {
|
|
131
|
+
clearTimeout(debounceTimer);
|
|
132
|
+
debounceTimer = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
done();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
RED.nodes.registerType("debounce-block", DebounceBlockNode);
|
|
140
|
+
};
|