@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,119 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function MinMaxBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Store last output value for status
|
|
12
|
+
let lastOutput = null;
|
|
13
|
+
|
|
14
|
+
node.on("input", function(msg, send, done) {
|
|
15
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
16
|
+
|
|
17
|
+
// Evaluate typed-inputs
|
|
18
|
+
try {
|
|
19
|
+
node.runtime.min = RED.util.evaluateNodeProperty(
|
|
20
|
+
config.min, config.minType, node, msg
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
node.runtime.max = RED.util.evaluateNodeProperty(
|
|
24
|
+
config.max, config.maxType, node, msg
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// Validate min and max at startup
|
|
29
|
+
if (isNaN(node.runtime.min) || isNaN(node.runtime.max) || node.runtime.min > node.runtime.max) {
|
|
30
|
+
node.status({ fill: "red", shape: "dot", text: `invalid min/max` });
|
|
31
|
+
if (done) done();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
} catch(err) {
|
|
35
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
36
|
+
if (done) done(err);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Guard against invalid message
|
|
41
|
+
if (!msg) {
|
|
42
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
43
|
+
if (done) done();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle context updates
|
|
48
|
+
if (msg.hasOwnProperty("context")) {
|
|
49
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
50
|
+
node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
|
|
51
|
+
if (done) done();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const value = parseFloat(msg.payload);
|
|
55
|
+
if (isNaN(value) || value < 0) {
|
|
56
|
+
node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
|
|
57
|
+
if (done) done();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (msg.context === "min") {
|
|
61
|
+
if (value < node.runtime.max) {
|
|
62
|
+
node.runtime.min = value;
|
|
63
|
+
node.status({ fill: "green", shape: "dot", text: `min: ${node.runtime.min}` });
|
|
64
|
+
} else {
|
|
65
|
+
node.status({ fill: "yellow", shape: "dot", text: `Context update aborted. Payload more than max` });
|
|
66
|
+
}
|
|
67
|
+
} else if (msg.context === "max") {
|
|
68
|
+
if (value > node.runtime.max) {
|
|
69
|
+
node.runtime.max = value;
|
|
70
|
+
node.status({ fill: "green", shape: "dot", text: `max: ${node.runtime.max}` });
|
|
71
|
+
} else {
|
|
72
|
+
node.status({ fill: "yellow", shape: "dot", text: `Context update aborted. Payload less than min` });
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
76
|
+
if (done) done();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (done) done();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate input payload
|
|
84
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
85
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
86
|
+
if (done) done();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const inputValue = parseFloat(msg.payload);
|
|
91
|
+
if (isNaN(inputValue)) {
|
|
92
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
93
|
+
if (done) done();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Clamp input to [min, max]
|
|
98
|
+
const outputValue = Math.min(Math.max(inputValue, node.runtime.min), node.runtime.max);
|
|
99
|
+
|
|
100
|
+
// Update status and send output
|
|
101
|
+
msg.payload = outputValue;
|
|
102
|
+
node.status({
|
|
103
|
+
fill: "blue",
|
|
104
|
+
shape: lastOutput === outputValue ? "ring" : "dot",
|
|
105
|
+
text: `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`
|
|
106
|
+
});
|
|
107
|
+
lastOutput = outputValue;
|
|
108
|
+
send(msg);
|
|
109
|
+
|
|
110
|
+
if (done) done();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
node.on("close", function(done) {
|
|
114
|
+
done();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
RED.nodes.registerType("minmax-block", MinMaxBlockNode);
|
|
119
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="modulo-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-slots" title="Number of input slots (positive integer, e.g., 2)"><i class="fa fa-list-ol"></i> Slots</label>
|
|
9
|
+
<input type="number" id="node-input-slots" placeholder="2" min="1" step="1">
|
|
10
|
+
</div>
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<!-- JavaScript Section -->
|
|
14
|
+
<script type="text/javascript">
|
|
15
|
+
RED.nodes.registerType("modulo-block", {
|
|
16
|
+
category: "control",
|
|
17
|
+
color: "#301934",
|
|
18
|
+
defaults: {
|
|
19
|
+
name: { value: "" },
|
|
20
|
+
slots: {
|
|
21
|
+
value: 2,
|
|
22
|
+
required: true,
|
|
23
|
+
validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) >= 1; }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
inputs: 1,
|
|
27
|
+
outputs: 1,
|
|
28
|
+
inputLabels: ["input"],
|
|
29
|
+
outputLabels: ["remainder"],
|
|
30
|
+
icon: "font-awesome/fa-percent",
|
|
31
|
+
paletteLabel: "modulo",
|
|
32
|
+
label: function() {
|
|
33
|
+
return this.name ? `${this.name} (${this.slots})` : `modulo (${this.slots})`;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!-- Help Section -->
|
|
39
|
+
<script type="text/markdown" data-help-name="modulo-block">
|
|
40
|
+
Computes the modulo (remainder) of numeric inputs from multiple slots in sequence.
|
|
41
|
+
|
|
42
|
+
### Inputs
|
|
43
|
+
: context (string) : Configures reset (`"reset"`), slots (`"slots"`), or identifies input slot (e.g., `"in1"`, `"in2"`).
|
|
44
|
+
: payload (number | boolean | integer) : Number for slot input, boolean for reset, integer for slots configuration.
|
|
45
|
+
|
|
46
|
+
### Outputs
|
|
47
|
+
: payload (number) : Result of modulo operation (in1 % in2 % ...).
|
|
48
|
+
|
|
49
|
+
### Properties
|
|
50
|
+
: slots (number) : Number of input slots (positive integer).
|
|
51
|
+
|
|
52
|
+
### Details
|
|
53
|
+
Computes the modulo of numeric `msg.payload` values from slots identified by `msg.context` (e.g., `"in1"`, `"in2"`) in sequence (in1 % in2 % ...).
|
|
54
|
+
|
|
55
|
+
Slots are set via editor or `msg.context = "slots"` with positive integer.
|
|
56
|
+
|
|
57
|
+
Inputs default to 1, updated via `msg.context = "inX"`.
|
|
58
|
+
Inputs for `in2` onward must be non-zero to avoid modulo by zero.
|
|
59
|
+
Resets inputs to 1 via `msg.context = "reset"` with `msg.payload = true`.
|
|
60
|
+
|
|
61
|
+
Outputs only when the result changes.
|
|
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>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function ModuloBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
slots: parseInt(config.slots),
|
|
10
|
+
inputs: Array(parseInt(config.slots) || 2).fill(1),
|
|
11
|
+
lastResult: null
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Validate initial config
|
|
15
|
+
if (isNaN(node.runtime.slots) || node.runtime.slots < 1) {
|
|
16
|
+
node.runtime.slots = 2;
|
|
17
|
+
node.runtime.inputs = Array(2).fill(1);
|
|
18
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots, using 2" });
|
|
19
|
+
} else {
|
|
20
|
+
node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name || "modulo"}, slots: ${node.runtime.slots}` });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
node.on("input", function(msg, send, done) {
|
|
24
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
25
|
+
|
|
26
|
+
// Guard against invalid message
|
|
27
|
+
if (!msg) {
|
|
28
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
29
|
+
if (done) done();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check for missing context or payload
|
|
34
|
+
if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
|
|
35
|
+
node.status({ fill: "red", shape: "ring", text: "missing context" });
|
|
36
|
+
if (done) done();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
41
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
42
|
+
if (done) done();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle configuration messages
|
|
47
|
+
if (msg.context === "reset") {
|
|
48
|
+
if (typeof msg.payload !== "boolean") {
|
|
49
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
50
|
+
if (done) done();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (msg.payload === true) {
|
|
54
|
+
node.runtime.inputs = Array(node.runtime.slots).fill(1);
|
|
55
|
+
node.runtime.lastResult = null;
|
|
56
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
57
|
+
if (done) done();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (done) done();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (msg.context === "slots") {
|
|
65
|
+
const newSlots = parseInt(msg.payload);
|
|
66
|
+
if (isNaN(newSlots) || newSlots < 1) {
|
|
67
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots" });
|
|
68
|
+
if (done) done();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
node.runtime.slots = newSlots;
|
|
72
|
+
node.runtime.inputs = Array(newSlots).fill(1);
|
|
73
|
+
node.runtime.lastResult = null;
|
|
74
|
+
node.status({ fill: "green", shape: "dot", text: `slots: ${newSlots}` });
|
|
75
|
+
if (done) done();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (msg.context.startsWith("in")) {
|
|
80
|
+
const slotIndex = parseInt(msg.context.slice(2)) - 1;
|
|
81
|
+
if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
|
|
82
|
+
node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
|
|
83
|
+
if (done) done();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const newValue = parseFloat(msg.payload);
|
|
87
|
+
if (isNaN(newValue) || !isFinite(newValue)) {
|
|
88
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
89
|
+
if (done) done();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (slotIndex > 0 && newValue === 0) {
|
|
93
|
+
node.status({ fill: "red", shape: "ring", text: "modulo by zero" });
|
|
94
|
+
if (done) done();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
node.runtime.inputs[slotIndex] = newValue;
|
|
98
|
+
|
|
99
|
+
// Calculate modulo
|
|
100
|
+
const result = node.runtime.inputs.reduce((acc, val, idx) => idx === 0 ? val : acc % val, node.runtime.inputs[0]);
|
|
101
|
+
const isUnchanged = result === node.runtime.lastResult;
|
|
102
|
+
node.status({
|
|
103
|
+
fill: "blue",
|
|
104
|
+
shape: isUnchanged ? "ring" : "dot",
|
|
105
|
+
text: `in: ${msg.context}=${newValue.toFixed(2)}, out: ${result.toFixed(2)}`
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!isUnchanged) {
|
|
109
|
+
node.runtime.lastResult = result;
|
|
110
|
+
send({ payload: result });
|
|
111
|
+
}
|
|
112
|
+
if (done) done();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
117
|
+
if (done) done("Unknown context");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
node.on("close", function(done) {
|
|
121
|
+
done();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
RED.nodes.registerType("modulo-block", ModuloBlockNode);
|
|
126
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="multiply-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("multiply-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: ["product"],
|
|
28
|
+
icon: "font-awesome/fa-times",
|
|
29
|
+
paletteLabel: "multiply",
|
|
30
|
+
label: function() {
|
|
31
|
+
return this.name ? `${this.name} (${this.slots})` : `multiply (${this.slots})`;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script type="text/markdown" data-help-name="multiply-block">
|
|
37
|
+
Multiplies numeric inputs from multiple slots.
|
|
38
|
+
|
|
39
|
+
### Inputs
|
|
40
|
+
: context (string) : Configures reset (`"reset"`), slots (`"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) : Product of all slot values.
|
|
45
|
+
|
|
46
|
+
### Details
|
|
47
|
+
Multiplies numeric `msg.payload` values from slots identified by `msg.context` (e.g., `"in1"`, `"in2"`).
|
|
48
|
+
|
|
49
|
+
Slots are set via editor or `msg.context = "slots"` with positive integer. Inputs default to 1, updated via `msg.context = "inX"`.
|
|
50
|
+
|
|
51
|
+
Resets inputs to 1 via `msg.context = "reset"` with `msg.payload = true`. Outputs only when the product changes.
|
|
52
|
+
|
|
53
|
+
### Status
|
|
54
|
+
- Green (dot): Configuration update
|
|
55
|
+
- Blue (dot): State changed
|
|
56
|
+
- Blue (ring): State unchanged
|
|
57
|
+
- Red (ring): Error
|
|
58
|
+
- Yellow (ring): Warning
|
|
59
|
+
|
|
60
|
+
### References
|
|
61
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
62
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
63
|
+
</script>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function MultiplyBlockNode(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(parseInt(config.slots) || 2).fill(1),
|
|
12
|
+
lastResult: null
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Validate initial config
|
|
16
|
+
if (isNaN(node.runtime.slots) || node.runtime.slots < 1) {
|
|
17
|
+
node.runtime.slots = 2;
|
|
18
|
+
node.runtime.inputs = Array(2).fill(1);
|
|
19
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots, using 2" });
|
|
20
|
+
} else {
|
|
21
|
+
node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name}, slots: ${node.runtime.slots}` });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
node.on("input", function(msg, send, done) {
|
|
25
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
26
|
+
|
|
27
|
+
// Guard against invalid msg
|
|
28
|
+
if (!msg) {
|
|
29
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
30
|
+
if (done) done();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for missing context or payload
|
|
35
|
+
if (!msg.hasOwnProperty("context")) {
|
|
36
|
+
node.status({ fill: "red", shape: "ring", text: "missing context" });
|
|
37
|
+
if (done) done();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
42
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
43
|
+
if (done) done();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle configuration messages
|
|
48
|
+
if (msg.context === "reset") {
|
|
49
|
+
if (typeof msg.payload !== "boolean") {
|
|
50
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
51
|
+
if (done) done();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (msg.payload === true) {
|
|
55
|
+
node.runtime.inputs = Array(node.runtime.slots).fill(1);
|
|
56
|
+
node.runtime.lastResult = null;
|
|
57
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
58
|
+
if (done) done();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
} else if (msg.context === "slots") {
|
|
62
|
+
let newSlots = parseInt(msg.payload);
|
|
63
|
+
if (isNaN(newSlots) || newSlots < 1) {
|
|
64
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots" });
|
|
65
|
+
if (done) done();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
node.runtime.slots = newSlots;
|
|
69
|
+
node.runtime.inputs = Array(newSlots).fill(1);
|
|
70
|
+
node.runtime.lastResult = null;
|
|
71
|
+
node.status({ fill: "green", shape: "dot", text: `slots: ${node.runtime.slots}` });
|
|
72
|
+
if (done) done();
|
|
73
|
+
return;
|
|
74
|
+
} else if (msg.context.startsWith("in")) {
|
|
75
|
+
let slotIndex = parseInt(msg.context.slice(2)) - 1;
|
|
76
|
+
if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
|
|
77
|
+
node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
|
|
78
|
+
if (done) done();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let newValue = parseFloat(msg.payload);
|
|
82
|
+
if (isNaN(newValue)) {
|
|
83
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
84
|
+
if (done) done();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
node.runtime.inputs[slotIndex] = newValue;
|
|
88
|
+
// Calculate product
|
|
89
|
+
const product = node.runtime.inputs.reduce((acc, val) => acc * val, 1);
|
|
90
|
+
const isUnchanged = product === node.runtime.lastResult;
|
|
91
|
+
node.status({
|
|
92
|
+
fill: "blue",
|
|
93
|
+
shape: isUnchanged ? "ring" : "dot",
|
|
94
|
+
text: `in: ${msg.context}=${newValue.toFixed(2)}, out: ${product.toFixed(2)}`
|
|
95
|
+
});
|
|
96
|
+
if (!isUnchanged) {
|
|
97
|
+
node.runtime.lastResult = product;
|
|
98
|
+
send({ payload: product });
|
|
99
|
+
}
|
|
100
|
+
if (done) done();
|
|
101
|
+
return;
|
|
102
|
+
} else {
|
|
103
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
104
|
+
if (done) done();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
node.on("close", function(done) {
|
|
110
|
+
done();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
RED.nodes.registerType("multiply-block", MultiplyBlockNode);
|
|
115
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="negate-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
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script type="text/javascript">
|
|
9
|
+
RED.nodes.registerType("negate-block", {
|
|
10
|
+
category: "control",
|
|
11
|
+
color: "#301934",
|
|
12
|
+
defaults: {
|
|
13
|
+
name: { value: "" }
|
|
14
|
+
},
|
|
15
|
+
inputs: 1,
|
|
16
|
+
outputs: 1,
|
|
17
|
+
inputLabels: ["input"],
|
|
18
|
+
outputLabels: ["output"],
|
|
19
|
+
icon: "font-awesome/fa-minus",
|
|
20
|
+
paletteLabel: "negate",
|
|
21
|
+
label: function() {
|
|
22
|
+
return this.name || "negate";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script type="text/markdown" data-help-name="negate-block">
|
|
28
|
+
Negates a number or boolean input value.
|
|
29
|
+
|
|
30
|
+
### Inputs
|
|
31
|
+
: payload (number | boolean) : Value to negate (number or boolean).
|
|
32
|
+
|
|
33
|
+
### Outputs
|
|
34
|
+
: payload (number | boolean) : Negated value (-number or !boolean).
|
|
35
|
+
|
|
36
|
+
### Details
|
|
37
|
+
Negates `msg.payload` in a passthrough manner, updating the original message.
|
|
38
|
+
- For numbers, outputs the negative (e.g., 5 becomes -5).
|
|
39
|
+
- For booleans, outputs the inverse (e.g., true becomes false).
|
|
40
|
+
|
|
41
|
+
Preserves other message properties (e.g., `msg.topic`, `msg.context`).
|
|
42
|
+
|
|
43
|
+
Outputs only when the negated value changes.
|
|
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,91 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function NegateBlockNode(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
|
+
lastOutput: null
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Set initial status
|
|
14
|
+
node.status({
|
|
15
|
+
fill: "green",
|
|
16
|
+
shape: "dot",
|
|
17
|
+
text: `name: ${node.runtime.name}`
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
node.on("input", function(msg, send, done) {
|
|
21
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
22
|
+
|
|
23
|
+
// Guard against invalid msg
|
|
24
|
+
if (!msg) {
|
|
25
|
+
node.status({ fill: "red", shape: "ring", text: "missing message" });
|
|
26
|
+
if (done) done();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for missing payload
|
|
31
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
32
|
+
node.status({ fill: "red", shape: "ring", text: "missing input" });
|
|
33
|
+
if (done) done();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const inputValue = msg.payload;
|
|
38
|
+
let outputValue;
|
|
39
|
+
let statusText;
|
|
40
|
+
|
|
41
|
+
if (typeof inputValue === "number") {
|
|
42
|
+
if (isNaN(inputValue)) {
|
|
43
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input: NaN" });
|
|
44
|
+
if (done) done();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
outputValue = -inputValue;
|
|
48
|
+
statusText = `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`;
|
|
49
|
+
} else if (typeof inputValue === "boolean") {
|
|
50
|
+
outputValue = !inputValue;
|
|
51
|
+
statusText = `in: ${inputValue}, out: ${outputValue}`;
|
|
52
|
+
} else {
|
|
53
|
+
let errorText;
|
|
54
|
+
if (inputValue === null) {
|
|
55
|
+
errorText = "invalid input: null";
|
|
56
|
+
} else if (Array.isArray(inputValue)) {
|
|
57
|
+
errorText = "invalid input: array";
|
|
58
|
+
} else if (typeof inputValue === "string") {
|
|
59
|
+
errorText = "invalid input: string";
|
|
60
|
+
} else {
|
|
61
|
+
errorText = "invalid input type";
|
|
62
|
+
}
|
|
63
|
+
node.status({ fill: "red", shape: "ring", text: errorText });
|
|
64
|
+
if (done) done();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for unchanged output
|
|
69
|
+
const isUnchanged = outputValue === node.runtime.lastOutput;
|
|
70
|
+
node.status({
|
|
71
|
+
fill: "blue",
|
|
72
|
+
shape: isUnchanged ? "ring" : "dot",
|
|
73
|
+
text: statusText
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!isUnchanged) {
|
|
77
|
+
node.runtime.lastOutput = outputValue;
|
|
78
|
+
msg.payload = outputValue;
|
|
79
|
+
send(msg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (done) done();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
node.on("close", function(done) {
|
|
86
|
+
done();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
RED.nodes.registerType("negate-block", NegateBlockNode);
|
|
91
|
+
};
|