@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
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# node-red-contrib-buildingblocks-control
|
|
2
|
+
Sedona-inspired control nodes for stateful logic.
|
|
3
|
+
|
|
4
|
+
This is a rather large node collection. Contributions are appreciated.
|
|
5
|
+
|
|
6
|
+
## Intro
|
|
7
|
+
This is intended for HVAC usage but the logic applies to anything.
|
|
8
|
+
|
|
9
|
+
Industry visual scripting tools, with Sedona being the original open source solution, use a stateful multi-in/out architecture. Any paradigm can be used in NodeRED but you need to standardize on how things are done.
|
|
10
|
+
|
|
11
|
+
Logic should be general, combinable, reusable and easily updated. NodeRED is like a blank canvas and I see this project as a way to provide some standardization with the logic. Seemingly simple, foundational, logic functions standardized. It helps me wrap my head around how to get things done and follow flows when I can wire things in branches that store an outcome and the ability to track which inputs are doing what...
|
|
12
|
+
|
|
13
|
+
There are TONS of fantastic node libraries out there but usually focusing on one area, or even one node. For my purposes, I want a core set of nodes all in one library that work in a certain, stateful way.
|
|
14
|
+
|
|
15
|
+
- Full status display of many states
|
|
16
|
+
- Full help sections
|
|
17
|
+
- Stateful node operation
|
|
18
|
+
- Multiple inputs through data tagging
|
|
19
|
+
- Many node types. Math, logic, test functions (sqaure wave, sine wave, tick tock, ...), specialized
|
|
20
|
+
- Most nodes utilize the Typed Input type to assign global variables
|
|
21
|
+
- Validation. Runtime validation is relied on in most cases to evaluate Typed Inputs and provides a status message to indicate errors encountered.
|
|
22
|
+
- Node commands, such as 'reset' or 'mode' or changing setpoints via messages.
|
|
23
|
+
|
|
24
|
+
## How To Use
|
|
25
|
+
Pictures to come.
|
|
26
|
+
|
|
27
|
+
The help section of every node describes the expected msg.context (data tag) for the intended msg.payload incoming. You can of course do this as you process data through a 'change' block, or use the provided 'contextual label' block which makes it easier to add and remove tags, more compact (especially if label hidden), and more transparent of the data flowing (ALL nodes contain complete status usage). Most nodes use a simple in1, in2, and so on.
|
|
28
|
+
|
|
29
|
+
##### Example
|
|
30
|
+
An 'and' block set to 4 slots must recieve `true` values on each inX at some point to evaluate to a `true` output. Where as, an 'or' block set to 4 inputs could have any input trigger a `true` evaluation. However, a remaining `true` would prevent evaluating to `false`. So the flow may look like 4 small tagging nodes configured in1,in2,in3,in4 connecting to the 'and' block and just wiring your branches of logic to those inputs. You can also negate or have multiple connected to an input and you can watch as each comes in to evaluate. Just try to keep it clean.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
##### Via NodeRED Palette Manager (Not Yet Available)
|
|
34
|
+
|
|
35
|
+
Search for the package name and add to your project.
|
|
36
|
+
|
|
37
|
+
##### Via NPM
|
|
38
|
+
```
|
|
39
|
+
# Navigate to Node-RED user directory (varies by installation)
|
|
40
|
+
- $ cd ~/.node-red
|
|
41
|
+
- $ npm install node-red-contrib-buildingblocks-control
|
|
42
|
+
# then restart node-red
|
|
43
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="accumulate-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-mode" title="Select accumulation mode"><i class="fa fa-cog"></i> Mode</label>
|
|
8
|
+
<select id="node-input-mode">
|
|
9
|
+
<option value="true">Accumulate True</option>
|
|
10
|
+
<option value="false">Accumulate False</option>
|
|
11
|
+
<option value="flows">Accumulate Flows</option>
|
|
12
|
+
</select>
|
|
13
|
+
</div>
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script type="text/javascript">
|
|
17
|
+
RED.nodes.registerType("accumulate-block", {
|
|
18
|
+
category: "control",
|
|
19
|
+
color: "#301934",
|
|
20
|
+
defaults: {
|
|
21
|
+
name: { value: "" },
|
|
22
|
+
mode: { value: "true", required: true }
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
inputLabels: ["input"],
|
|
27
|
+
outputLabels: ["count"],
|
|
28
|
+
icon: "font-awesome/fa-sort-numeric-asc",
|
|
29
|
+
paletteLabel: "accumulate",
|
|
30
|
+
label: function() {
|
|
31
|
+
if (this.name) return this.name;
|
|
32
|
+
const modeLabels = {
|
|
33
|
+
"true": "true accumulate",
|
|
34
|
+
"false": "false accumulate",
|
|
35
|
+
"flows": "flows accumulate"
|
|
36
|
+
};
|
|
37
|
+
return modeLabels[this.mode] || "accumulate";
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<script type="text/markdown" data-help-name="accumulate-block">
|
|
43
|
+
Counts consecutive inputs based on the selected mode, resetting on specific conditions.
|
|
44
|
+
|
|
45
|
+
### Inputs
|
|
46
|
+
: context (string) : Configuration commands = reset (`"reset"` to clear count).
|
|
47
|
+
: payload (boolean) : For `Accumulate True`, `true` increments count, `false` resets; for `Accumulate False`, `false` increments count, `true` resets; ignored in `Accumulate Flows`.
|
|
48
|
+
|
|
49
|
+
### Outputs
|
|
50
|
+
: payload (number) : Current count based on mode.
|
|
51
|
+
|
|
52
|
+
### Details
|
|
53
|
+
Counts inputs according to the selected mode
|
|
54
|
+
- `Accumulate True` counts consecutive `true` `msg.payload` inputs (reset on `false` or explicit reset)
|
|
55
|
+
- `Accumulate False` counts consecutive `false` `msg.payload` inputs (reset on `true` or explicit reset)
|
|
56
|
+
- `Accumulate Flows` counts all valid input messages (reset only on explicit reset).
|
|
57
|
+
|
|
58
|
+
Reset via `msg.context = "reset"` with `msg.payload = true` applies immediately;
|
|
59
|
+
|
|
60
|
+
Used to track sustained events or message frequency.
|
|
61
|
+
|
|
62
|
+
### Status
|
|
63
|
+
- Green (dot): Configuration
|
|
64
|
+
- Blue (dot): Output sent
|
|
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,104 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function AccumulateBlockNode(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
|
+
count: 0,
|
|
11
|
+
lastCount: null
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Set initial status
|
|
15
|
+
node.status({
|
|
16
|
+
fill: "green",
|
|
17
|
+
shape: "dot",
|
|
18
|
+
text: `mode: ${node.runtime.mode}, name: ${node.runtime.name || node.runtime.mode + " accumulate"}`
|
|
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: "missing message" });
|
|
27
|
+
node.warn("Missing message");
|
|
28
|
+
if (done) done();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle reset command with intentional payload requirement
|
|
33
|
+
if (msg.context === "reset") {
|
|
34
|
+
if (msg.payload === true) {
|
|
35
|
+
count = 0;
|
|
36
|
+
updateStatus();
|
|
37
|
+
if (done) done();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// payload !== true: treat as normal message, don't reset
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Process input based on mode
|
|
44
|
+
if (node.runtime.mode !== "flows") {
|
|
45
|
+
// Check for missing payload
|
|
46
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
47
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
48
|
+
node.warn("Missing payload");
|
|
49
|
+
if (done) done();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate input
|
|
54
|
+
const inputValue = msg.payload;
|
|
55
|
+
if (typeof inputValue !== "boolean") {
|
|
56
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
57
|
+
node.warn("Invalid input: non-boolean payload");
|
|
58
|
+
if (done) done();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Prevent extended time running isues
|
|
63
|
+
if (node.runtime.count > 9999) {
|
|
64
|
+
node.runtime.count = 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Accumulate or reset count
|
|
68
|
+
if (node.runtime.mode === "true") {
|
|
69
|
+
if (inputValue === true) {
|
|
70
|
+
node.runtime.count++;
|
|
71
|
+
} else {
|
|
72
|
+
node.runtime.count = 0;
|
|
73
|
+
}
|
|
74
|
+
} else if (node.runtime.mode === "false") {
|
|
75
|
+
if (inputValue === false) {
|
|
76
|
+
node.runtime.count++;
|
|
77
|
+
} else {
|
|
78
|
+
node.runtime.count = 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// flows mode: count all valid messages
|
|
83
|
+
node.runtime.count++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Output only if count changed
|
|
87
|
+
if (node.runtime.lastCount !== node.runtime.count) {
|
|
88
|
+
node.runtime.lastCount = node.runtime.count;
|
|
89
|
+
node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.count}` });
|
|
90
|
+
send({ payload: node.runtime.count });
|
|
91
|
+
} else {
|
|
92
|
+
node.status({ fill: "blue", shape: "ring", text: `out: ${node.runtime.count}` });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (done) done();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
node.on("close", function(done) {
|
|
99
|
+
done();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
RED.nodes.registerType("accumulate-block", AccumulateBlockNode);
|
|
104
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="add-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="2" step="1">
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script type="text/javascript">
|
|
13
|
+
RED.nodes.registerType("add-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) >= 2; }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
inputLabels: ["input"],
|
|
27
|
+
outputLabels: ["sum"],
|
|
28
|
+
icon: "font-awesome/fa-plus",
|
|
29
|
+
paletteLabel: "add",
|
|
30
|
+
label: function() {
|
|
31
|
+
return this.name ? `${this.name} (${this.slots})` : `add (${this.slots})`;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script type="text/markdown" data-help-name="add-block">
|
|
37
|
+
Sums numeric inputs from multiple slots.
|
|
38
|
+
|
|
39
|
+
### Inputs
|
|
40
|
+
: context (string) : Configuration commands - `"reset"`, `"slots"` active slots count, `"in<x>"` identifies input slot (e.g., `"in1"`, `"in2"`).
|
|
41
|
+
: payload (number | boolean) : Number for slot input or slots configuration, boolean for reset.
|
|
42
|
+
|
|
43
|
+
### Outputs
|
|
44
|
+
: payload (number) : Sum of all slot values.
|
|
45
|
+
|
|
46
|
+
### Details
|
|
47
|
+
Sums numeric `msg.payload` values from slots identified by `msg.context` (e.g., `"in1"`, `"in2"`).
|
|
48
|
+
|
|
49
|
+
Values persist in context per slot. They may arrive and be set at different times.
|
|
50
|
+
|
|
51
|
+
Slots (default: 2) are set via editor or `msg.context = "slots"` with positive integer greater than 2.
|
|
52
|
+
|
|
53
|
+
Inputs default to 0, updated via `msg.context = "inX"`. Resets inputs to 0 via `msg.context = "reset"` with `msg.payload = true`.
|
|
54
|
+
|
|
55
|
+
Outputs sum for each valid slot update.
|
|
56
|
+
|
|
57
|
+
### Status
|
|
58
|
+
- Green (dot): Configuration update
|
|
59
|
+
- Blue (dot): State changed
|
|
60
|
+
- Blue (ring): State unchanged
|
|
61
|
+
- Red (ring): Error
|
|
62
|
+
- Yellow: Warning
|
|
63
|
+
|
|
64
|
+
### References
|
|
65
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
66
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
67
|
+
</script>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function AddBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize state
|
|
7
|
+
node.slots = parseInt(config.slots) || 2;
|
|
8
|
+
node.inputs = Array(parseInt(config.slots) || 2).fill(0);
|
|
9
|
+
|
|
10
|
+
let lastSum = null;
|
|
11
|
+
|
|
12
|
+
node.on("input", function(msg, send, done) {
|
|
13
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
14
|
+
|
|
15
|
+
// Guard against invalid msg
|
|
16
|
+
if (!msg) {
|
|
17
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
18
|
+
if (done) done();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check for required properties
|
|
23
|
+
if (!msg.hasOwnProperty("context")) {
|
|
24
|
+
node.status({ fill: "red", shape: "ring", text: "missing context" });
|
|
25
|
+
if (done) done();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
30
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
31
|
+
if (done) done();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle configuration messages
|
|
36
|
+
if (msg.context === "reset") {
|
|
37
|
+
if (typeof msg.payload !== "boolean") {
|
|
38
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
39
|
+
if (done) done();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (msg.payload === true) {
|
|
43
|
+
node.inputs = Array(node.slots).fill(0);
|
|
44
|
+
lastSum = null;
|
|
45
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
46
|
+
if (done) done();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
} else if (msg.context === "slots") {
|
|
50
|
+
let newSlots = parseInt(msg.payload);
|
|
51
|
+
if (isNaN(newSlots) || newSlots < 1) {
|
|
52
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots" });
|
|
53
|
+
if (done) done();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
node.slots = newSlots;
|
|
57
|
+
node.inputs = Array(newSlots).fill(0);
|
|
58
|
+
lastSum = null;
|
|
59
|
+
node.status({ fill: "green", shape: "dot", text: `slots: ${node.slots}` });
|
|
60
|
+
if (done) done();
|
|
61
|
+
return;
|
|
62
|
+
} else if (msg.context.startsWith("in")) {
|
|
63
|
+
let slotIndex = parseInt(msg.context.slice(2)) - 1;
|
|
64
|
+
if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.slots) {
|
|
65
|
+
node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
|
|
66
|
+
if (done) done();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let newValue = parseFloat(msg.payload);
|
|
70
|
+
if (isNaN(newValue)) {
|
|
71
|
+
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
72
|
+
if (done) done();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
node.inputs[slotIndex] = newValue;
|
|
76
|
+
// Calculate sum
|
|
77
|
+
const sum = node.inputs.reduce((acc, val) => acc + val, 0);
|
|
78
|
+
const isUnchanged = sum === lastSum;
|
|
79
|
+
node.status({ fill: "blue", shape: isUnchanged ? "ring" : "dot", text: `${msg.context}: ${newValue.toFixed(2)}, sum: ${sum.toFixed(2)}` });
|
|
80
|
+
lastSum = sum;
|
|
81
|
+
send({ payload: sum });
|
|
82
|
+
if (done) done();
|
|
83
|
+
return;
|
|
84
|
+
} else {
|
|
85
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
86
|
+
if (done) done();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
node.on("close", function(done) {
|
|
92
|
+
done();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
RED.nodes.registerType("add-block", AddBlockNode);
|
|
97
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="analog-switch-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 (integer >= 1)"><i class="fa fa-th-list"></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: Registers the node and handles editor logic -->
|
|
14
|
+
<script type="text/javascript">
|
|
15
|
+
RED.nodes.registerType("analog-switch-block", {
|
|
16
|
+
category: "control",
|
|
17
|
+
color: "#301934",
|
|
18
|
+
defaults: {
|
|
19
|
+
name: { value: "" },
|
|
20
|
+
slots: { value: 2, required: true, validate: function(v) { return Number.isInteger(Number(v)) && Number(v) >= 1; } }
|
|
21
|
+
},
|
|
22
|
+
inputs: 1,
|
|
23
|
+
outputs: 1,
|
|
24
|
+
inputLabels: ["input"],
|
|
25
|
+
outputLabels: ["output"],
|
|
26
|
+
icon: "font-awesome/fa-exchange",
|
|
27
|
+
paletteLabel: "analog switch",
|
|
28
|
+
label: function() {
|
|
29
|
+
return this.name ? `${this.name} (${this.slots})` : `analog switch (${this.slots})`;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<!-- Help Section -->
|
|
35
|
+
<script type="text/markdown" data-help-name="analog-switch-block">
|
|
36
|
+
Selects one numeric input from multiple slots based on a switch value.
|
|
37
|
+
|
|
38
|
+
### Inputs
|
|
39
|
+
: context (string) : Configures slots (`"slots"`), switch (`"switch"`), or input slot (`"inX"`, e.g., `in1`). Unmatched values trigger error.
|
|
40
|
+
: payload (number | integer) : Number for input slot, integer for slots or switch configuration.
|
|
41
|
+
|
|
42
|
+
### Outputs
|
|
43
|
+
: payload (number) : Selected input value based on switch setting.
|
|
44
|
+
|
|
45
|
+
### Properties
|
|
46
|
+
: slots (integer) : Number of input slots (≥ 1). Default 2.
|
|
47
|
+
|
|
48
|
+
### Details
|
|
49
|
+
Selects one numeric input from multiple slots (`in1`, `in2`, etc.) based on a switch value (1 to slots)
|
|
50
|
+
set via `msg.context = "switch"` with integer `msg.payload`. Slots configurable via editor or `msg.context = "slots"` with
|
|
51
|
+
integer `msg.payload` (≥ 1). Inputs default to 0, updated via `msg.context = "inX"` with numeric `msg.payload`.
|
|
52
|
+
|
|
53
|
+
Outputs a new `msg.payload` when the active slot is updated, switch changes to a valid slot, or slots change affects the output.
|
|
54
|
+
|
|
55
|
+
### Status
|
|
56
|
+
- Green (dot): Configuration update
|
|
57
|
+
- Blue (dot): State changed
|
|
58
|
+
- Blue (ring): State unchanged
|
|
59
|
+
- Red (ring): Error
|
|
60
|
+
- Yellow (ring): Warning
|
|
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,129 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function AnalogSwitchBlockNode(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),
|
|
10
|
+
inputs: Array(parseInt(config.slots, 10) || 2).fill(0),
|
|
11
|
+
switch: 1
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
node.on("input", function(msg, send, done) {
|
|
15
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
16
|
+
|
|
17
|
+
// Guard against invalid message
|
|
18
|
+
if (!msg) {
|
|
19
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
20
|
+
if (done) done();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate context
|
|
25
|
+
if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
|
|
26
|
+
node.status({ fill: "red", shape: "ring", text: "missing context" });
|
|
27
|
+
if (done) done();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate payload
|
|
32
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
33
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
34
|
+
if (done) done();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let shouldOutput = false;
|
|
39
|
+
const prevSwitch = node.runtime.switch;
|
|
40
|
+
|
|
41
|
+
switch (msg.context) {
|
|
42
|
+
case "slots":
|
|
43
|
+
const slotValue = parseInt(msg.payload, 10);
|
|
44
|
+
if (isNaN(slotValue) || slotValue < 1) {
|
|
45
|
+
node.status({ fill: "red", shape: "ring", text: "invalid slots" });
|
|
46
|
+
if (done) done();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
node.runtime.slots = slotValue;
|
|
50
|
+
const newInputs = Array(node.runtime.slots).fill(0);
|
|
51
|
+
for (let i = 0; i < Math.min(node.runtime.inputs.length, node.runtime.slots); i++) {
|
|
52
|
+
newInputs[i] = node.runtime.inputs[i];
|
|
53
|
+
}
|
|
54
|
+
node.runtime.inputs = newInputs;
|
|
55
|
+
if (node.runtime.switch > node.runtime.slots) {
|
|
56
|
+
node.runtime.switch = 1;
|
|
57
|
+
shouldOutput = true;
|
|
58
|
+
}
|
|
59
|
+
node.status({
|
|
60
|
+
fill: "green",
|
|
61
|
+
shape: "dot",
|
|
62
|
+
text: `slots: ${node.runtime.slots}`
|
|
63
|
+
});
|
|
64
|
+
break;
|
|
65
|
+
case "switch":
|
|
66
|
+
const switchValue = parseInt(msg.payload, 10);
|
|
67
|
+
if (isNaN(switchValue) || switchValue < 1 || switchValue > node.runtime.slots) {
|
|
68
|
+
node.status({ fill: "red", shape: "ring", text: "invalid switch" });
|
|
69
|
+
if (done) done();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
node.runtime.switch = switchValue;
|
|
73
|
+
shouldOutput = prevSwitch !== node.runtime.switch;
|
|
74
|
+
node.status({
|
|
75
|
+
fill: "green",
|
|
76
|
+
shape: "dot",
|
|
77
|
+
text: `switch: ${node.runtime.switch}`
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
if (msg.context.startsWith("in")) {
|
|
82
|
+
const index = parseInt(msg.context.slice(2), 10);
|
|
83
|
+
if (isNaN(index) || index < 1 || index > node.runtime.slots) {
|
|
84
|
+
node.status({ fill: "red", shape: "ring", text: `invalid input index ${index}` });
|
|
85
|
+
if (done) done();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const value = parseFloat(msg.payload);
|
|
89
|
+
if (isNaN(value)) {
|
|
90
|
+
node.status({ fill: "red", shape: "ring", text: `invalid in${index}` });
|
|
91
|
+
if (done) done();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
node.runtime.inputs[index - 1] = value;
|
|
95
|
+
shouldOutput = index === node.runtime.switch;
|
|
96
|
+
node.status({
|
|
97
|
+
fill: "green",
|
|
98
|
+
shape: "dot",
|
|
99
|
+
text: `in${index}: ${value.toFixed(2)}`
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
103
|
+
if (done) done("Unknown context");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Output new message if the active slot is updated or switch/slots change affects output
|
|
110
|
+
if (shouldOutput) {
|
|
111
|
+
const out = node.runtime.inputs[node.runtime.switch - 1] ?? node.runtime.inputs[0];
|
|
112
|
+
node.status({
|
|
113
|
+
fill: "blue",
|
|
114
|
+
shape: "dot",
|
|
115
|
+
text: `slots: ${node.runtime.slots}, switch: ${node.runtime.switch}, out: ${out.toFixed(2)}`
|
|
116
|
+
});
|
|
117
|
+
send({ payload: out });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (done) done();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
node.on("close", function(done) {
|
|
124
|
+
done();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
RED.nodes.registerType("analog-switch-block", AnalogSwitchBlockNode);
|
|
129
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="and-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 boolean inputs (integer ≥ 2)"><i class="fa fa-list"></i> Slots</label>
|
|
8
|
+
<input type="number" id="node-input-slots" placeholder="2" min="2" step="1">
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script type="text/javascript">
|
|
13
|
+
RED.nodes.registerType("and-block", {
|
|
14
|
+
category: "control",
|
|
15
|
+
color: "#301934",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: "" },
|
|
18
|
+
slots: {
|
|
19
|
+
value: 2,
|
|
20
|
+
required: true,
|
|
21
|
+
validate: function(v) {
|
|
22
|
+
const num = parseInt(v, 10);
|
|
23
|
+
return !isNaN(num) && num >= 2;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
inputs: 1,
|
|
28
|
+
outputs: 1,
|
|
29
|
+
inputLabels: ["input"],
|
|
30
|
+
outputLabels: ["output"],
|
|
31
|
+
icon: "font-awesome/fa-check-square-o",
|
|
32
|
+
paletteLabel: "and",
|
|
33
|
+
label: function() {
|
|
34
|
+
return this.name ? `${this.name} (${this.slots})` : `and (${this.slots})`;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script type="text/markdown" data-help-name="and-block">
|
|
40
|
+
Computes the logical AND of multiple boolean inputs, outputting a new message with the result.
|
|
41
|
+
|
|
42
|
+
### Inputs
|
|
43
|
+
: context (string) : Configuration commands - Identifies the input slot (e.g., `"in1"`, `"in2"`).
|
|
44
|
+
: payload (any) : Value for the slot, converted to boolean (`true`, `1` → `true`; `false`, `0`, `null` → `false`).
|
|
45
|
+
|
|
46
|
+
### Outputs
|
|
47
|
+
: payload (boolean) : `true` if all slots are `true`, `false` otherwise.
|
|
48
|
+
|
|
49
|
+
### Details
|
|
50
|
+
Evaluates the logical AND of a fixed number of boolean inputs (`slots` ≥ 2, set in editor).
|
|
51
|
+
|
|
52
|
+
Each slot is updated via `msg.context = "inX"` (e.g., `"in1"`, `"in2"`) with `msg.payload` converted to boolean.
|
|
53
|
+
|
|
54
|
+
### Status
|
|
55
|
+
- Green (dot): Configuration update
|
|
56
|
+
- Blue (dot): State changed
|
|
57
|
+
- Blue (ring): State unchanged
|
|
58
|
+
- Red (ring): Error
|
|
59
|
+
- Yellow (ring): Warning
|
|
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>
|