@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,104 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="delay-block">
|
|
3
|
+
<div class="form-row">
|
|
4
|
+
<label for="node-input-name"><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-delayOn"><i class="fa fa-clock-o"></i> On Delay</label>
|
|
9
|
+
<input type="number" id="node-input-delayOn" placeholder="1000" min="0" step="1">
|
|
10
|
+
<select id="node-input-delayOnUnits">
|
|
11
|
+
<option value="milliseconds">Milliseconds</option>
|
|
12
|
+
<option value="seconds">Seconds</option>
|
|
13
|
+
<option value="minutes">Minutes</option>
|
|
14
|
+
</select>
|
|
15
|
+
<input type="hidden" id="node-input-delayOnType">
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-row">
|
|
19
|
+
<label for="node-input-delayOff"><i class="fa fa-clock-o"></i> Off Delay</label>
|
|
20
|
+
<input type="number" id="node-input-delayOff" placeholder="1000" min="0" step="1">
|
|
21
|
+
<select id="node-input-delayOffUnits">
|
|
22
|
+
<option value="milliseconds">Milliseconds</option>
|
|
23
|
+
<option value="seconds">Seconds</option>
|
|
24
|
+
<option value="minutes">Minutes</option>
|
|
25
|
+
</select>
|
|
26
|
+
<input type="hidden" id="node-input-delayOffType">
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
32
|
+
<script type="text/javascript">
|
|
33
|
+
RED.nodes.registerType("delay-block", {
|
|
34
|
+
category: "control",
|
|
35
|
+
color: "#301934",
|
|
36
|
+
defaults: {
|
|
37
|
+
name: { value: "" },
|
|
38
|
+
delayOn: { value: 1000, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
39
|
+
delayOnUnits: { value: "milliseconds" },
|
|
40
|
+
delayOnType: { value: "num" },
|
|
41
|
+
delayOff: { value: 1000, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
42
|
+
delayOffUnits: { value: "milliseconds" },
|
|
43
|
+
delayOffType: { value: "num" }
|
|
44
|
+
},
|
|
45
|
+
inputs: 1,
|
|
46
|
+
outputs: 1,
|
|
47
|
+
inputLabels: ["input"],
|
|
48
|
+
outputLabels: ["output"],
|
|
49
|
+
icon: "font-awesome/fa-clock-o",
|
|
50
|
+
paletteLabel: "delay",
|
|
51
|
+
label: function() {
|
|
52
|
+
return this.name || "delay";
|
|
53
|
+
},
|
|
54
|
+
oneditprepare: function() {
|
|
55
|
+
try {
|
|
56
|
+
// Initialize typed inputs
|
|
57
|
+
$("#node-input-delayOn").typedInput({
|
|
58
|
+
default: "num",
|
|
59
|
+
types: ["num", "msg", "flow", "global"],
|
|
60
|
+
typeField: "#node-input-delayOnType"
|
|
61
|
+
}).typedInput("type", node.delayOnType || "num").typedInput("value", node.delayOn);
|
|
62
|
+
|
|
63
|
+
$("#node-input-delayOff").typedInput({
|
|
64
|
+
default: "num",
|
|
65
|
+
types: ["num", "msg", "flow", "global"],
|
|
66
|
+
typeField: "#node-input-delayOffType"
|
|
67
|
+
}).typedInput("type", node.delayOffType || "num").typedInput("value", node.delayOff);
|
|
68
|
+
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("Error in hysteresis-block oneditprepare:", err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<!-- Help Section: -->
|
|
77
|
+
<script type="text/markdown" data-help-name="delay-block">
|
|
78
|
+
Delays boolean state transitions with configurable on/off delays.
|
|
79
|
+
|
|
80
|
+
### Inputs
|
|
81
|
+
: context (string) : Configures node (`"reset"`, `"delayOn"`, `"delayOff"`). Unmatched values ignored silently.
|
|
82
|
+
: payload (boolean | number) : Boolean for state change; number for delay config with `msg.context`.
|
|
83
|
+
: *units* (string) : Units for delay context config (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
84
|
+
|
|
85
|
+
### Outputs
|
|
86
|
+
: payload (boolean) : `true` after `delayOn` ms or `false` after `delayOff` ms if state persists. `msg.context` is consumed.
|
|
87
|
+
: *other* (any) : Other input properties preserved, except `msg.context`.
|
|
88
|
+
|
|
89
|
+
### Details
|
|
90
|
+
Delays `msg.payload` boolean transitions, outputting `true` after `delayOn` ms for false-to-true or `false` after `delayOff` ms for true-to-false,
|
|
91
|
+
if the input state persists. Forwards the input message with updated `msg.payload`, removing `msg.context`.
|
|
92
|
+
Non-transition inputs (e.g., `true` when `state=true`) or state reversions cancel pending delays without output.
|
|
93
|
+
|
|
94
|
+
### Status
|
|
95
|
+
- Green (dot): Configuration
|
|
96
|
+
- Blue (dot): Output, no alarm
|
|
97
|
+
- Red (dot): Output with alarm
|
|
98
|
+
- Red (ring): Errors
|
|
99
|
+
- Yellow (ring): Unknown context
|
|
100
|
+
|
|
101
|
+
### References
|
|
102
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
103
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
104
|
+
</script>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function DelayBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
node.runtime = {
|
|
7
|
+
name: config.name || "",
|
|
8
|
+
state: false
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (isNaN(node.runtime.delayOn) || node.runtime.delayOn < 0) {
|
|
12
|
+
node.runtime.delayOn = 1000;
|
|
13
|
+
node.status({ fill: "red", shape: "ring", text: "invalid delayOn" });
|
|
14
|
+
}
|
|
15
|
+
if (isNaN(node.runtime.delayOff) || node.runtime.delayOff < 0) {
|
|
16
|
+
node.runtime.delayOff = 1000;
|
|
17
|
+
node.status({ fill: "red", shape: "ring", text: "invalid delayOff" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Set initial status
|
|
21
|
+
node.status({
|
|
22
|
+
fill: "green",
|
|
23
|
+
shape: "dot",
|
|
24
|
+
text: `On: ${node.runtime.delayOn}ms, Off: ${node.runtime.delayOff}ms`
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let timeoutId = null;
|
|
28
|
+
|
|
29
|
+
node.on("input", function(msg, send, done) {
|
|
30
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
31
|
+
if (!msg) {
|
|
32
|
+
if (done) done();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Evaluate typed-inputs
|
|
37
|
+
try {
|
|
38
|
+
node.runtime.delayOn = RED.util.evaluateNodeProperty(
|
|
39
|
+
config.delayOn, config.delayOnType, node, msg
|
|
40
|
+
);
|
|
41
|
+
node.runtime.delayOn = (parseFloat(node.runtime.delayOn)) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
|
|
42
|
+
|
|
43
|
+
node.runtime.delayOff = RED.util.evaluateNodeProperty(
|
|
44
|
+
config.delayOff, config.delayOffType, node, msg
|
|
45
|
+
);
|
|
46
|
+
node.runtime.delayOff = (parseFloat(node.runtime.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
|
|
47
|
+
|
|
48
|
+
node.period = parseFloat(node.period);
|
|
49
|
+
if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
|
|
50
|
+
node.period = 1000;
|
|
51
|
+
node.status({ fill: "yellow", shape: "ring", text: "invalid period, using 1000ms" });
|
|
52
|
+
}
|
|
53
|
+
} catch(err) {
|
|
54
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
55
|
+
if (done) done(err);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (msg.hasOwnProperty("context")) {
|
|
60
|
+
if (msg.context === "reset") {
|
|
61
|
+
if (!msg.hasOwnProperty("payload") || typeof msg.payload !== "boolean") {
|
|
62
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
63
|
+
if (done) done();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (msg.payload === true) {
|
|
67
|
+
if (timeoutId) {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
timeoutId = null;
|
|
70
|
+
}
|
|
71
|
+
node.runtime.state = false;
|
|
72
|
+
node.status({ fill: "green", shape: "dot", text: "reset" });
|
|
73
|
+
}
|
|
74
|
+
if (done) done();
|
|
75
|
+
return;
|
|
76
|
+
} else if (msg.context === "delayOn") {
|
|
77
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
78
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload for delayOn" });
|
|
79
|
+
if (done) done();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let newDelayOn = parseFloat(msg.payload);
|
|
83
|
+
const newDelayOnMultiplier = msg.units === "seconds" ? 1000 : msg.units === "minutes" ? 60000 : 1;
|
|
84
|
+
newDelayOn *= newDelayOnMultiplier;
|
|
85
|
+
if (isNaN(newDelayOn) || newDelayOn < 0) {
|
|
86
|
+
node.status({ fill: "red", shape: "ring", text: "invalid delayOn" });
|
|
87
|
+
if (done) done();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
node.runtime.delayOn = newDelayOn;
|
|
91
|
+
node.status({ fill: "green", shape: "dot", text: `delayOn: ${newDelayOn.toFixed(0)} ms` });
|
|
92
|
+
if (done) done();
|
|
93
|
+
return;
|
|
94
|
+
} else if (msg.context === "delayOff") {
|
|
95
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
96
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload for delayOff" });
|
|
97
|
+
if (done) done();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let newDelayOff = parseFloat(msg.payload);
|
|
101
|
+
const newDelayOffMultiplier = msg.units === "seconds" ? 1000 : msg.units === "minutes" ? 60000 : 1;
|
|
102
|
+
newDelayOff *= newDelayOffMultiplier;
|
|
103
|
+
if (isNaN(newDelayOff) || newDelayOff < 0) {
|
|
104
|
+
node.status({ fill: "red", shape: "ring", text: "invalid delayOff" });
|
|
105
|
+
if (done) done();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
node.runtime.delayOff = newDelayOff;
|
|
109
|
+
node.status({ fill: "green", shape: "dot", text: `delayOff: ${newDelayOff.toFixed(0)} ms` });
|
|
110
|
+
if (done) done();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
114
|
+
if (done) done();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
119
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
120
|
+
if (done) done();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const inputValue = msg.payload;
|
|
125
|
+
if (typeof inputValue !== "boolean") {
|
|
126
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
127
|
+
if (done) done();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!node.runtime.state && inputValue === true) {
|
|
132
|
+
if (timeoutId) {
|
|
133
|
+
clearTimeout(timeoutId);
|
|
134
|
+
}
|
|
135
|
+
node.status({ fill: "blue", shape: "ring", text: `awaiting true` });
|
|
136
|
+
timeoutId = setTimeout(() => {
|
|
137
|
+
node.runtime.state = true;
|
|
138
|
+
msg.payload = true;
|
|
139
|
+
delete msg.context;
|
|
140
|
+
node.status({ fill: "blue", shape: "dot", text: `in: true, out: true` });
|
|
141
|
+
send(msg);
|
|
142
|
+
timeoutId = null;
|
|
143
|
+
}, node.runtime.delayOn);
|
|
144
|
+
} else if (node.runtime.state && inputValue === false) {
|
|
145
|
+
if (timeoutId) {
|
|
146
|
+
clearTimeout(timeoutId);
|
|
147
|
+
}
|
|
148
|
+
node.status({ fill: "blue", shape: "ring", text: `awaiting false` });
|
|
149
|
+
timeoutId = setTimeout(() => {
|
|
150
|
+
node.runtime.state = false;
|
|
151
|
+
msg.payload = false;
|
|
152
|
+
delete msg.context;
|
|
153
|
+
node.status({ fill: "blue", shape: "dot", text: `in: false, out: false` });
|
|
154
|
+
send(msg);
|
|
155
|
+
timeoutId = null;
|
|
156
|
+
}, node.runtime.delayOff);
|
|
157
|
+
} else {
|
|
158
|
+
if (timeoutId) {
|
|
159
|
+
clearTimeout(timeoutId);
|
|
160
|
+
timeoutId = null;
|
|
161
|
+
node.status({ fill: "blue", shape: "ring", text: `canceled awaiting ${node.runtime.state}` });
|
|
162
|
+
} else {
|
|
163
|
+
node.status({ fill: "blue", shape: "ring", text: `awaiting ${inputValue}` });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (done) done();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
node.on("close", function(done) {
|
|
171
|
+
if (timeoutId) {
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
timeoutId = null;
|
|
174
|
+
}
|
|
175
|
+
done();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
RED.nodes.registerType("delay-block", DelayBlockNode);
|
|
180
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="divide-block">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-slots" title="Number of input slots (positive integer, e.g., 2)"><i class="fa fa-list-ol"></i> Slots</label>
|
|
8
|
+
<input type="number" id="node-input-slots" placeholder="2" min="1" step="1">
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script type="text/javascript">
|
|
13
|
+
RED.nodes.registerType("divide-block", {
|
|
14
|
+
category: "control",
|
|
15
|
+
color: "#301934",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: "" },
|
|
18
|
+
slots: {
|
|
19
|
+
value: 2,
|
|
20
|
+
required: true,
|
|
21
|
+
validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) > 1; }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
inputLabels: ["input"],
|
|
27
|
+
outputLabels: ["quotient"],
|
|
28
|
+
icon: "split.svg",
|
|
29
|
+
paletteLabel: "divide",
|
|
30
|
+
label: function() {
|
|
31
|
+
return this.name ? `${this.name} (${this.slots})` : `divide (${this.slots})`;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script type="text/markdown" data-help-name="divide-block">
|
|
37
|
+
Divides numeric inputs from multiple slots in sequence.
|
|
38
|
+
|
|
39
|
+
### Inputs
|
|
40
|
+
: context (string) : Configuration commands - `"reset"`, `"slots"`, or identifies input slot (e.g., `"in1"`, `"in2"`).
|
|
41
|
+
: payload (number | boolean) : Number for slot input, boolean for reset, integer for slots configuration.
|
|
42
|
+
|
|
43
|
+
### Outputs
|
|
44
|
+
: payload (number) : Result of dividing slots in sequence (in1 / in2 / ...).
|
|
45
|
+
|
|
46
|
+
### Details
|
|
47
|
+
Divides numeric `msg.payload` values from slots identified by `msg.context` (e.g., `"in1"`, `"in2"`) in sequence.
|
|
48
|
+
|
|
49
|
+
Slots are set via editor or `msg.context = "slots"` with positive integer.
|
|
50
|
+
|
|
51
|
+
Inputs for `in2` onward must be non-zero to avoid division by zero. Resets inputs to 1 via `msg.context = "reset"` with `msg.payload = true`.
|
|
52
|
+
|
|
53
|
+
Outputs a number only when the result changes.
|
|
54
|
+
|
|
55
|
+
### Status
|
|
56
|
+
- Green (dot): Configuration
|
|
57
|
+
- Blue (dot): Output, no alarm
|
|
58
|
+
- Red (dot): Output with alarm
|
|
59
|
+
- Red (ring): Errors
|
|
60
|
+
- Yellow (ring): Unknown context
|
|
61
|
+
|
|
62
|
+
### References
|
|
63
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
64
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function DivideBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
|
|
5
|
+
const node = this;
|
|
6
|
+
|
|
7
|
+
// Initialize runtime state
|
|
8
|
+
node.runtime = {
|
|
9
|
+
name: config.name,
|
|
10
|
+
slots: parseInt(config.slots),
|
|
11
|
+
inputs: Array(node.runtime.slots).fill(1).map(x => parseFloat(x)),
|
|
12
|
+
lastResult: null
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
node.status({
|
|
16
|
+
fill: "green",
|
|
17
|
+
shape: "dot",
|
|
18
|
+
text: `name: ${node.runtime.name}, slots: ${node.runtime.slots}`
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
node.on("input", function(msg, send, done) {
|
|
22
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
23
|
+
|
|
24
|
+
// Guard against invalid msg
|
|
25
|
+
if (!msg) {
|
|
26
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
27
|
+
if (done) done();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for missing context or payload
|
|
32
|
+
if (!msg.hasOwnProperty("context")) {
|
|
33
|
+
node.status({ fill: "red", shape: "ring", text: "missing context" });
|
|
34
|
+
if (done) done();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
39
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
40
|
+
if (done) done();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle configuration messages
|
|
45
|
+
if (msg.context === "reset") {
|
|
46
|
+
if (typeof msg.payload !== "boolean") {
|
|
47
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
48
|
+
if (done) done();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (msg.payload === true) {
|
|
52
|
+
node.runtime.inputs = Array(node.runtime.slots).fill(1);
|
|
53
|
+
node.runtime.lastResult = null;
|
|
54
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
55
|
+
if (done) done();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
} else if (msg.context === "slots") {
|
|
59
|
+
let newSlots = parseInt(msg.payload);
|
|
60
|
+
if (isNaN(newSlots) || newSlots < 1) {
|
|
61
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots" });
|
|
62
|
+
if (done) done();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
node.runtime.slots = newSlots;
|
|
66
|
+
node.runtime.inputs = Array(newSlots).fill(1);
|
|
67
|
+
node.runtime.lastResult = null;
|
|
68
|
+
node.status({ fill: "green", shape: "dot", text: `slots: ${node.runtime.slots}` });
|
|
69
|
+
if (done) done();
|
|
70
|
+
return;
|
|
71
|
+
} else if (msg.context.startsWith("in")) {
|
|
72
|
+
let slotIndex = parseInt(msg.context.slice(2)) - 1;
|
|
73
|
+
if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
|
|
74
|
+
node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
|
|
75
|
+
if (done) done();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let newValue = parseFloat(msg.payload);
|
|
79
|
+
if (isNaN(newValue)) {
|
|
80
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
81
|
+
if (done) done();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (slotIndex > 0 && newValue === 0) {
|
|
85
|
+
node.status({ fill: "red", shape: "ring", text: "divide by zero" });
|
|
86
|
+
if (done) done();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Handle division by very small numbers approaching zero
|
|
90
|
+
if (slotIndex > 0 && Math.abs(newValue) < 1e-10) { // Near-zero check
|
|
91
|
+
node.status({ fill: "red", shape: "ring", text: "divide by near-zero" });
|
|
92
|
+
if (done) done();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
node.runtime.inputs[slotIndex] = newValue;
|
|
96
|
+
// Calculate division
|
|
97
|
+
const result = node.runtime.inputs.reduce((acc, val, idx) => idx === 0 ? val : acc / val, 1);
|
|
98
|
+
const isUnchanged = result === node.runtime.lastResult;
|
|
99
|
+
node.status({
|
|
100
|
+
fill: "blue",
|
|
101
|
+
shape: isUnchanged ? "ring" : "dot",
|
|
102
|
+
text: `in: ${msg.context}=${newValue.toFixed(2)}, out: ${result.toFixed(2)}`
|
|
103
|
+
});
|
|
104
|
+
if (!isUnchanged) {
|
|
105
|
+
node.runtime.lastResult = result;
|
|
106
|
+
send({ payload: result });
|
|
107
|
+
}
|
|
108
|
+
if (done) done();
|
|
109
|
+
return;
|
|
110
|
+
} else {
|
|
111
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
112
|
+
if (done) done();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
node.on("close", function(done) {
|
|
118
|
+
done();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
RED.nodes.registerType("divide-block", DivideBlockNode);
|
|
123
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="edge-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-algorithm" title="Transition to detect (true-to-false or false-to-true)"><i class="fa fa-exchange"></i> Algorithm</label>
|
|
9
|
+
<select id="node-input-algorithm">
|
|
10
|
+
<option value="true-to-false">True to False</option>
|
|
11
|
+
<option value="false-to-true">False to True</option>
|
|
12
|
+
</select>
|
|
13
|
+
</div>
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!-- JavaScript Section -->
|
|
17
|
+
<script type="text/javascript">
|
|
18
|
+
RED.nodes.registerType("edge-block", {
|
|
19
|
+
category: "control",
|
|
20
|
+
color: "#301934",
|
|
21
|
+
defaults: {
|
|
22
|
+
name: { value: "" },
|
|
23
|
+
algorithm: { value: "true-to-false", required: true }
|
|
24
|
+
},
|
|
25
|
+
inputs: 1,
|
|
26
|
+
outputs: 1,
|
|
27
|
+
inputLabels: ["input"],
|
|
28
|
+
outputLabels: ["transition"],
|
|
29
|
+
icon: "font-awesome/fa-exchange",
|
|
30
|
+
paletteLabel: "edge",
|
|
31
|
+
label: function() {
|
|
32
|
+
return this.name || "edge";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<!-- Help Section -->
|
|
38
|
+
<script type="text/markdown" data-help-name="edge-block">
|
|
39
|
+
Detects configured boolean transitions (true-to-false or false-to-true) in `msg.payload`.
|
|
40
|
+
|
|
41
|
+
### Inputs
|
|
42
|
+
: payload (boolean) : Boolean to monitor for transitions.
|
|
43
|
+
: context (string) : Action (`"algorithm"` to set transition type, `"reset"` to clear state). Unknown `msg.context` is ignored.
|
|
44
|
+
: payload (string | boolean) : Transition type (`"true-to-false"`, `"false-to-true"`) for `"algorithm"`, true for `"reset"`.
|
|
45
|
+
|
|
46
|
+
### Outputs
|
|
47
|
+
: payload (boolean) : `true` when the specified transition occurs (true-to-false or false-to-true).
|
|
48
|
+
|
|
49
|
+
### Properties
|
|
50
|
+
: name (string) : Display name in editor.
|
|
51
|
+
: algorithm (string) : Transition to detect (`"true-to-false"`, `"false-to-true"`).
|
|
52
|
+
|
|
53
|
+
### Details
|
|
54
|
+
Detects transitions in boolean payloads based on the configured `algorithm`.
|
|
55
|
+
Outputs `msg.payload` as true only when the specified transition occurs (true-to-false or false-to-true).
|
|
56
|
+
No output on first input after reset, if no transition occurs, or for non-boolean inputs.
|
|
57
|
+
Configuration via `msg.context`
|
|
58
|
+
- `"algorithm"` Sets transition type, no output.
|
|
59
|
+
- `"reset"` Clears state, no output.
|
|
60
|
+
|
|
61
|
+
### Status
|
|
62
|
+
- Green (dot): Configuration
|
|
63
|
+
- Blue (dot): Output, no alarm
|
|
64
|
+
- Red (dot): Output with alarm
|
|
65
|
+
- Red (ring): Errors
|
|
66
|
+
- Yellow (ring): Unknown context
|
|
67
|
+
|
|
68
|
+
### References
|
|
69
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
70
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function EdgeBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
algorithm: config.algorithm,
|
|
10
|
+
lastValue: null
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
node.status({
|
|
14
|
+
fill: "green",
|
|
15
|
+
shape: "dot",
|
|
16
|
+
text: `name: ${node.runtime.name || "edge"}, algorithm: ${node.runtime.algorithm}`
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
node.on("input", function(msg, send, done) {
|
|
20
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
21
|
+
|
|
22
|
+
const validAlgorithms = ["true-to-false", "false-to-true"];
|
|
23
|
+
|
|
24
|
+
// Guard against invalid message
|
|
25
|
+
if (!msg) {
|
|
26
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
27
|
+
if (done) done();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle configuration messages
|
|
32
|
+
if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
|
|
33
|
+
if (msg.context === "algorithm") {
|
|
34
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
35
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
36
|
+
if (done) done();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const newAlgorithm = String(msg.payload);
|
|
40
|
+
if (!validAlgorithms.includes(newAlgorithm)) {
|
|
41
|
+
node.status({ fill: "red", shape: "ring", text: "invalid algorithm" });
|
|
42
|
+
if (done) done();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
node.runtime.algorithm = newAlgorithm;
|
|
46
|
+
node.status({ fill: "green", shape: "dot", text: `algorithm: ${newAlgorithm}` });
|
|
47
|
+
if (done) done();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (msg.context === "reset") {
|
|
52
|
+
if (!msg.hasOwnProperty("payload") || typeof msg.payload !== "boolean") {
|
|
53
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
54
|
+
if (done) done();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (msg.payload === true) {
|
|
58
|
+
node.runtime.lastValue = null;
|
|
59
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
60
|
+
if (done) done();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (done) done();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Ignore unknown context, process payload
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate payload
|
|
70
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
71
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
72
|
+
if (done) done();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof msg.payload !== "boolean") {
|
|
77
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
78
|
+
if (done) done();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const currentValue = msg.payload;
|
|
83
|
+
const lastValue = node.runtime.lastValue;
|
|
84
|
+
|
|
85
|
+
// Check for transition
|
|
86
|
+
let isTransition = false;
|
|
87
|
+
if (lastValue !== null && lastValue !== undefined) {
|
|
88
|
+
if (node.runtime.algorithm === "true-to-false" && lastValue === true && currentValue === false) {
|
|
89
|
+
isTransition = true;
|
|
90
|
+
} else if (node.runtime.algorithm === "false-to-true" && lastValue === false && currentValue === true) {
|
|
91
|
+
isTransition = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isTransition) {
|
|
96
|
+
node.status({
|
|
97
|
+
fill: "blue",
|
|
98
|
+
shape: "dot",
|
|
99
|
+
text: `in: ${currentValue}, out: true`
|
|
100
|
+
});
|
|
101
|
+
send({ payload: true });
|
|
102
|
+
} else {
|
|
103
|
+
node.status({
|
|
104
|
+
fill: "blue",
|
|
105
|
+
shape: "ring",
|
|
106
|
+
text: `in: ${currentValue}, out: none`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
node.runtime.lastValue = currentValue;
|
|
111
|
+
if (done) done();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
node.on("close", function(done) {
|
|
115
|
+
done();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
RED.nodes.registerType("edge-block", EdgeBlockNode);
|
|
120
|
+
};
|