@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
@@ -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
+ };