@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.
Files changed (98) hide show
  1. package/README.md +43 -0
  2. package/nodes/accumulate-block.html +71 -0
  3. package/nodes/accumulate-block.js +104 -0
  4. package/nodes/add-block.html +67 -0
  5. package/nodes/add-block.js +97 -0
  6. package/nodes/analog-switch-block.html +65 -0
  7. package/nodes/analog-switch-block.js +129 -0
  8. package/nodes/and-block.html +64 -0
  9. package/nodes/and-block.js +73 -0
  10. package/nodes/average-block.html +97 -0
  11. package/nodes/average-block.js +137 -0
  12. package/nodes/boolean-switch-block.html +59 -0
  13. package/nodes/boolean-switch-block.js +88 -0
  14. package/nodes/boolean-to-number-block.html +59 -0
  15. package/nodes/boolean-to-number-block.js +45 -0
  16. package/nodes/cache-block.html +69 -0
  17. package/nodes/cache-block.js +106 -0
  18. package/nodes/call-status-block.html +111 -0
  19. package/nodes/call-status-block.js +274 -0
  20. package/nodes/changeover-block.html +234 -0
  21. package/nodes/changeover-block.js +392 -0
  22. package/nodes/comment-block.html +83 -0
  23. package/nodes/comment-block.js +53 -0
  24. package/nodes/compare-block.html +64 -0
  25. package/nodes/compare-block.js +84 -0
  26. package/nodes/contextual-label-block.html +67 -0
  27. package/nodes/contextual-label-block.js +52 -0
  28. package/nodes/convert-block.html +179 -0
  29. package/nodes/convert-block.js +289 -0
  30. package/nodes/count-block.html +57 -0
  31. package/nodes/count-block.js +92 -0
  32. package/nodes/debounce-block.html +64 -0
  33. package/nodes/debounce-block.js +140 -0
  34. package/nodes/delay-block.html +104 -0
  35. package/nodes/delay-block.js +180 -0
  36. package/nodes/divide-block.html +65 -0
  37. package/nodes/divide-block.js +123 -0
  38. package/nodes/edge-block.html +71 -0
  39. package/nodes/edge-block.js +120 -0
  40. package/nodes/frequency-block.html +55 -0
  41. package/nodes/frequency-block.js +140 -0
  42. package/nodes/hysteresis-block.html +131 -0
  43. package/nodes/hysteresis-block.js +142 -0
  44. package/nodes/interpolate-block.html +74 -0
  45. package/nodes/interpolate-block.js +141 -0
  46. package/nodes/load-sequence-block.html +134 -0
  47. package/nodes/load-sequence-block.js +272 -0
  48. package/nodes/max-block.html +76 -0
  49. package/nodes/max-block.js +103 -0
  50. package/nodes/memory-block.html +90 -0
  51. package/nodes/memory-block.js +241 -0
  52. package/nodes/min-block.html +77 -0
  53. package/nodes/min-block.js +106 -0
  54. package/nodes/minmax-block.html +89 -0
  55. package/nodes/minmax-block.js +119 -0
  56. package/nodes/modulo-block.html +73 -0
  57. package/nodes/modulo-block.js +126 -0
  58. package/nodes/multiply-block.html +63 -0
  59. package/nodes/multiply-block.js +115 -0
  60. package/nodes/negate-block.html +55 -0
  61. package/nodes/negate-block.js +91 -0
  62. package/nodes/nullify-block.html +111 -0
  63. package/nodes/nullify-block.js +78 -0
  64. package/nodes/on-change-block.html +79 -0
  65. package/nodes/on-change-block.js +191 -0
  66. package/nodes/oneshot-block.html +96 -0
  67. package/nodes/oneshot-block.js +169 -0
  68. package/nodes/or-block.html +64 -0
  69. package/nodes/or-block.js +73 -0
  70. package/nodes/pid-block.html +205 -0
  71. package/nodes/pid-block.js +407 -0
  72. package/nodes/priority-block.html +66 -0
  73. package/nodes/priority-block.js +239 -0
  74. package/nodes/rate-limit-block.html +99 -0
  75. package/nodes/rate-limit-block.js +221 -0
  76. package/nodes/round-block.html +73 -0
  77. package/nodes/round-block.js +89 -0
  78. package/nodes/saw-tooth-wave-block.html +87 -0
  79. package/nodes/saw-tooth-wave-block.js +161 -0
  80. package/nodes/scale-range-block.html +90 -0
  81. package/nodes/scale-range-block.js +137 -0
  82. package/nodes/sine-wave-block.html +88 -0
  83. package/nodes/sine-wave-block.js +142 -0
  84. package/nodes/subtract-block.html +64 -0
  85. package/nodes/subtract-block.js +103 -0
  86. package/nodes/thermistor-block.html +81 -0
  87. package/nodes/thermistor-block.js +146 -0
  88. package/nodes/tick-tock-block.html +66 -0
  89. package/nodes/tick-tock-block.js +110 -0
  90. package/nodes/time-sequence-block.html +67 -0
  91. package/nodes/time-sequence-block.js +144 -0
  92. package/nodes/triangle-wave-block.html +86 -0
  93. package/nodes/triangle-wave-block.js +154 -0
  94. package/nodes/tstat-block.html +311 -0
  95. package/nodes/tstat-block.js +499 -0
  96. package/nodes/units-block.html +150 -0
  97. package/nodes/units-block.js +106 -0
  98. 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>