@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,110 @@
1
+ module.exports = function(RED) {
2
+ function TickTockBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ period: parseFloat(config.period),
10
+ state: true
11
+ };
12
+
13
+ // Validate initial config
14
+ if (isNaN(node.runtime.period) || node.runtime.period <= 0 || !isFinite(node.runtime.period)) {
15
+ node.runtime.period = 10;
16
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
17
+ }
18
+
19
+ let intervalId = null;
20
+
21
+ node.on("input", function(msg, send, done) {
22
+ send = send || function() { node.send.apply(node, arguments); };
23
+
24
+ // Guard against invalid message
25
+ if (!msg) {
26
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
27
+ if (done) done();
28
+ return;
29
+ }
30
+
31
+ // Handle context updates
32
+ if (msg.hasOwnProperty("context")) {
33
+ if (!msg.hasOwnProperty("payload")) {
34
+ node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
35
+ if (done) done();
36
+ return;
37
+ }
38
+ if (typeof msg.context !== "string") {
39
+ node.status({ fill: "red", shape: "ring", text: "invalid context" });
40
+ if (done) done();
41
+ return;
42
+ }
43
+ switch (msg.context) {
44
+ case "period":
45
+ const value = parseFloat(msg.payload);
46
+ if (isNaN(value) || value <= 0 || !isFinite(value)) {
47
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
48
+ if (done) done();
49
+ return;
50
+ }
51
+ node.runtime.period = value;
52
+ node.status({ fill: "green", shape: "dot", text: `period: ${node.runtime.period.toFixed(2)}` });
53
+ if (intervalId) {
54
+ clearInterval(intervalId);
55
+ node.runtime.state = true;
56
+ const halfPeriodMs = (node.runtime.period * 1000) / 2;
57
+ send({ payload: node.runtime.state });
58
+ node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.state}` });
59
+ intervalId = setInterval(() => {
60
+ node.runtime.state = !node.runtime.state;
61
+ send({ payload: node.runtime.state });
62
+ node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.state}` });
63
+ }, halfPeriodMs);
64
+ }
65
+ break;
66
+ case "command":
67
+ if (typeof msg.payload !== "string" || !["start", "stop"].includes(msg.payload)) {
68
+ node.status({ fill: "red", shape: "ring", text: "invalid command" });
69
+ if (done) done();
70
+ return;
71
+ }
72
+ if (msg.payload === "start" && !intervalId) {
73
+ node.runtime.state = true;
74
+ const halfPeriodMs = (node.runtime.period * 1000) / 2;
75
+ send({ payload: node.runtime.state });
76
+ node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.state}` });
77
+ intervalId = setInterval(() => {
78
+ node.runtime.state = !node.runtime.state;
79
+ send({ payload: node.runtime.state });
80
+ node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.state}` });
81
+ }, halfPeriodMs);
82
+ node.status({ fill: "green", shape: "dot", text: `started, period: ${node.runtime.period.toFixed(2)}` });
83
+ } else if (msg.payload === "stop" && intervalId) {
84
+ clearInterval(intervalId);
85
+ intervalId = null;
86
+ node.status({ fill: "yellow", shape: "dot", text: "stopped" });
87
+ }
88
+ break;
89
+ default:
90
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
91
+ if (done) done("Unknown context");
92
+ return;
93
+ }
94
+ if (done) done();
95
+ return;
96
+ }
97
+ if (done) done();
98
+ });
99
+
100
+ node.on("close", function(done) {
101
+ if (intervalId) {
102
+ clearInterval(intervalId);
103
+ intervalId = null;
104
+ }
105
+ done();
106
+ });
107
+ }
108
+
109
+ RED.nodes.registerType("tick-tock-block", TickTockBlockNode);
110
+ };
@@ -0,0 +1,67 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="time-sequence-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-delay" title="Delay between sequence stages (milliseconds, non-negative number)"><i class="fa fa-clock-o"></i> Delay (ms)</label>
9
+ <input type="number" id="node-input-delay" placeholder="5000" min="0" step="any">
10
+ </div>
11
+ </script>
12
+
13
+ <!-- JavaScript Section -->
14
+ <script type="text/javascript">
15
+ RED.nodes.registerType("time-sequence-block", {
16
+ category: "control",
17
+ color: "#301934",
18
+ defaults: {
19
+ name: { value: "" },
20
+ delay: { value: 5000, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0 && isFinite(parseFloat(v)); } }
21
+ },
22
+ inputs: 1,
23
+ outputs: 4,
24
+ inputLabels: ["input"],
25
+ outputLabels: ["stage1", "stage2", "stage3", "stage4"],
26
+ icon: "font-awesome/fa-hourglass-half",
27
+ paletteLabel: "time sequence",
28
+ label: function() {
29
+ return this.name || "time sequence";
30
+ }
31
+ });
32
+ </script>
33
+
34
+ <!-- Help Section -->
35
+ <script type="text/markdown" data-help-name="time-sequence-block">
36
+ Triggers a sequence of outputs with configurable delays, ignoring inputs during active sequence.
37
+
38
+ ### Inputs
39
+ : context (string) : Configures settings (`"delay"`, `"reset"`). Unmatched values trigger error.
40
+ : payload (any | number | boolean) : Value to pass through outputs, number for delay, boolean (true) for reset.
41
+
42
+ ### Outputs
43
+ : stage1 (any) : The input message with `msg.stage = 1` (immediate).
44
+ : stage2 (any) : The input message with `msg.stage = 2` (after delay).
45
+ : stage3 (any) : The input message with `msg.stage = 3` (after 2x delay).
46
+ : stage4 (any) : The input message with `msg.stage = 4` (after 2x delay).
47
+ : Other properties (e.g., `msg.topic`) from the input message are preserved.
48
+
49
+ ### Details
50
+ Triggers a sequence of four outputs (`stage1`, `stage2`, `stage3`, `reset`) with the input message, adding `msg.stage`
51
+ (1 to 4). Outputs are delayed by `delay` (ms), configurable via editor or `msg.context = "delay"` with numeric `msg.payload`.
52
+
53
+ Resets sequence via `msg.context = "reset"` with `msg.payload = true`, outputting `{ payload false }` to all ports.
54
+
55
+ Ignores inputs during active sequence.
56
+
57
+ ### Status
58
+ - Green (dot): Configuration update
59
+ - Blue (dot): State changed
60
+ - Blue (ring): State unchanged
61
+ - Red (ring): Error
62
+ - Yellow (ring): 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,144 @@
1
+ module.exports = function(RED) {
2
+ function TimeSequenceBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ delay: parseFloat(config.delay),
10
+ stage: 0
11
+ };
12
+
13
+ // Validate initial config
14
+ if (isNaN(node.runtime.delay) || node.runtime.delay < 0 || !isFinite(node.runtime.delay)) {
15
+ node.runtime.delay = 5000;
16
+ node.status({ fill: "red", shape: "ring", text: "invalid delay" });
17
+ }
18
+
19
+ let timer = null;
20
+
21
+ node.on("input", function(msg, send, done) {
22
+ send = send || function() { node.send.apply(node, arguments); };
23
+
24
+ // Guard against invalid message
25
+ if (!msg) {
26
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
27
+ if (done) done();
28
+ return;
29
+ }
30
+
31
+ // Handle context updates
32
+ if (msg.hasOwnProperty("context")) {
33
+ if (typeof msg.context !== "string") {
34
+ node.status({ fill: "red", shape: "ring", text: "invalid context" });
35
+ if (done) done();
36
+ return;
37
+ }
38
+ switch (msg.context) {
39
+ case "delay":
40
+ if (!msg.hasOwnProperty("payload")) {
41
+ node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
42
+ if (done) done();
43
+ return;
44
+ }
45
+ const delayValue = parseFloat(msg.payload);
46
+ if (isNaN(delayValue) || delayValue < 0 || !isFinite(delayValue)) {
47
+ node.status({ fill: "red", shape: "ring", text: "invalid delay" });
48
+ if (done) done();
49
+ return;
50
+ }
51
+ node.runtime.delay = delayValue;
52
+ node.status({
53
+ fill: "green",
54
+ shape: "dot",
55
+ text: `delay: ${node.runtime.delay.toFixed(2)} ms`
56
+ });
57
+ if (done) done();
58
+ return;
59
+ case "reset":
60
+ if (!msg.hasOwnProperty("payload")) {
61
+ node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
62
+ if (done) done();
63
+ return;
64
+ }
65
+ if (typeof msg.payload !== "boolean" || !msg.payload) {
66
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
67
+ if (done) done();
68
+ return;
69
+ }
70
+ if (timer) {
71
+ clearTimeout(timer);
72
+ timer = null;
73
+ }
74
+ node.runtime.stage = 0;
75
+ const resetMsg = { payload: false };
76
+ node.status({
77
+ fill: "green",
78
+ shape: "dot",
79
+ text: "state reset"
80
+ });
81
+ send([resetMsg, resetMsg, resetMsg, resetMsg]);
82
+ if (done) done();
83
+ return;
84
+ default:
85
+ break;
86
+ }
87
+ }
88
+
89
+ // Validate input
90
+ if (!msg.hasOwnProperty("payload")) {
91
+ node.status({ fill: "red", shape: "ring", text: "missing input" });
92
+ if (done) done();
93
+ return;
94
+ }
95
+
96
+ // Process input
97
+ if (node.runtime.stage !== 0) {
98
+ node.status({ fill: "yellow", shape: "ring", text: "sequence already running" });
99
+ if (done) done();
100
+ return;
101
+ }
102
+
103
+ // Start new sequence
104
+ node.runtime.stage = 1;
105
+ const cloneMsg = RED.util.cloneMessage(msg);
106
+
107
+ // Output sequence
108
+ const sendNextOutput = () => {
109
+ if (node.runtime.stage === 0) return;
110
+ const stageLabels = ["stage 1", "stage 2", "stage 3", "stage 4"];
111
+ const outputs = [null, null, null, null];
112
+ cloneMsg.stage = node.runtime.stage;
113
+ outputs[node.runtime.stage - 1] = cloneMsg;
114
+ node.status({
115
+ fill: "blue",
116
+ shape: "dot",
117
+ text: `stage: ${stageLabels[node.runtime.stage - 1]}, in: ${JSON.stringify(cloneMsg.payload).slice(0, 20)}`
118
+ });
119
+ send(outputs);
120
+
121
+ node.runtime.stage++;
122
+ if (node.runtime.stage <= 4) {
123
+ timer = setTimeout(sendNextOutput, node.runtime.delay);
124
+ } else {
125
+ node.runtime.stage = 0;
126
+ timer = null;
127
+ }
128
+ };
129
+
130
+ // Start sequence
131
+ sendNextOutput();
132
+
133
+ if (done) done();
134
+ });
135
+
136
+ node.on("close", function(done) {
137
+ if (timer) clearTimeout(timer);
138
+ timer = null;
139
+ done();
140
+ });
141
+ }
142
+
143
+ RED.nodes.registerType("time-sequence-block", TimeSequenceBlockNode);
144
+ };
@@ -0,0 +1,86 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="triangle-wave-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-lowerLimit" title="Minimum output value"><i class="fa fa-arrow-down"></i> Lower Limit</label>
9
+ <input type="number" id="node-input-lowerLimit" placeholder="0" step="any">
10
+ </div>
11
+ <div class="form-row">
12
+ <label for="node-input-upperLimit" title="Maximum output value (≥ lowerLimit)"><i class="fa fa-arrow-up"></i> Upper Limit</label>
13
+ <input type="number" id="node-input-upperLimit" placeholder="100" step="any">
14
+ </div>
15
+ <div class="form-row">
16
+ <label for="node-input-period" title="Wave period (positive number, e.g., 10)"><i class="fa fa-clock-o"></i> Period</label>
17
+ <input type="number" id="node-input-period" placeholder="10" min="0.001" step="any">
18
+ <select id="node-input-periodUnits">
19
+ <option value="milliseconds">Milliseconds</option>
20
+ <option value="seconds">Seconds</option>
21
+ <option value="minutes">Minutes</option>
22
+ </select>
23
+ </div>
24
+ </script>
25
+
26
+ <!-- JavaScript Section -->
27
+ <script type="text/javascript">
28
+ RED.nodes.registerType("triangle-wave-block", {
29
+ category: "control",
30
+ color: "#301934",
31
+ defaults: {
32
+ name: { value: "" },
33
+ lowerLimit: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
34
+ upperLimit: { value: 100, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
35
+ period: { value: 10, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0 && isFinite(parseFloat(v)); } },
36
+ periodUnits: { value: "seconds" }
37
+ },
38
+ inputs: 1,
39
+ outputs: 1,
40
+ inputLabels: ["input"],
41
+ outputLabels: ["output"],
42
+ icon: "font-awesome/fa-wave-square",
43
+ paletteLabel: "triangle wave",
44
+ label: function() {
45
+ return this.name || "triangle wave";
46
+ }
47
+ });
48
+ </script>
49
+
50
+ <!-- Help Section -->
51
+ <script type="text/markdown" data-help-name="triangle-wave-block">
52
+ Generates a triangle wave output scaled to a configurable range.
53
+
54
+ ### Inputs
55
+ : context (string) : Configures settings (`"lowerLimit"`, `"upperLimit"`, `"period"`). Unmatched values trigger error.
56
+ : payload (number) : Config value for lowerLimit, upperLimit, or period.
57
+ : units (string, optional) : Units for period (`"milliseconds"`, `"seconds"`, `"minutes"`).
58
+ : Any input triggers wave output.
59
+
60
+ ### Outputs
61
+ : payload (number) : Triangle wave value scaled between lowerLimit and upperLimit.
62
+
63
+ ### Properties
64
+ : lowerLimit (number) : Minimum output value.
65
+ : upperLimit (number) : Maximum output value (≥ lowerLimit).
66
+ : period (number) : Wave period (positive, in periodUnits).
67
+ : periodUnits (string) : Units for period (`"milliseconds"`, `"seconds"`, `"minutes"`).
68
+
69
+ ### Details
70
+ Generates a triangle wave output, linearly rising from `lowerLimit` to `upperLimit` and falling back over `period`, triggered by any input.
71
+
72
+ Tracks phase (0 to 1) for continuity.
73
+
74
+ Ensures `upperLimit ≥ lowerLimit` by adjusting limits. Outputs `msg.payload number` with wave value; if `period ≤ 0`, outputs `lowerLimit`.
75
+
76
+ ### Status
77
+ - Green (dot): Configuration update
78
+ - Blue (dot): State changed
79
+ - Blue (ring): State unchanged
80
+ - Red (ring): Error
81
+ - Yellow (ring): Warning
82
+
83
+ ### References
84
+ - [Node-RED Documentation](https://nodered.org/docs/)
85
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
86
+ </script>
@@ -0,0 +1,154 @@
1
+ module.exports = function(RED) {
2
+ function TriangleWaveBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name || "",
9
+ lowerLimit: parseFloat(config.lowerLimit),
10
+ upperLimit: parseFloat(config.upperLimit),
11
+ period: (parseFloat(config.period)) * (config.periodUnits === "minutes" ? 60000 : config.periodUnits === "seconds" ? 1000 : 1),
12
+ periodUnits: config.periodUnits,
13
+ lastExecution: Date.now(),
14
+ phase: 0
15
+ };
16
+
17
+ // Validate initial config
18
+ if (isNaN(node.runtime.lowerLimit) || isNaN(node.runtime.upperLimit) || !isFinite(node.runtime.lowerLimit) || !isFinite(node.runtime.upperLimit)) {
19
+ node.runtime.lowerLimit = 0;
20
+ node.runtime.upperLimit = 100;
21
+ node.status({ fill: "red", shape: "ring", text: "invalid limits" });
22
+ } else if (node.runtime.lowerLimit > node.runtime.upperLimit) {
23
+ node.runtime.upperLimit = node.runtime.lowerLimit;
24
+ node.status({ fill: "red", shape: "ring", text: "invalid limits" });
25
+ }
26
+ if (isNaN(node.runtime.period) || node.runtime.period <= 0 || !isFinite(node.runtime.period)) {
27
+ node.runtime.period = 10000;
28
+ node.runtime.periodUnits = "milliseconds";
29
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
30
+ }
31
+
32
+ node.on("input", function(msg, send, done) {
33
+ send = send || function() { node.send.apply(node, arguments); };
34
+
35
+ // Guard against invalid message
36
+ if (!msg) {
37
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
38
+ if (done) done();
39
+ return;
40
+ }
41
+
42
+ // Handle context updates
43
+ if (msg.hasOwnProperty("context")) {
44
+ if (!msg.hasOwnProperty("payload")) {
45
+ node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
46
+ if (done) done();
47
+ return;
48
+ }
49
+ if (typeof msg.context !== "string") {
50
+ node.status({ fill: "red", shape: "ring", text: "invalid context" });
51
+ if (done) done();
52
+ return;
53
+ }
54
+ let value = parseFloat(msg.payload);
55
+ if (isNaN(value) || !isFinite(value)) {
56
+ node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
57
+ if (done) done();
58
+ return;
59
+ }
60
+ switch (msg.context) {
61
+ case "lowerLimit":
62
+ node.runtime.lowerLimit = value;
63
+ if (node.runtime.lowerLimit > node.runtime.upperLimit) {
64
+ node.runtime.upperLimit = node.runtime.lowerLimit;
65
+ node.status({
66
+ fill: "green",
67
+ shape: "dot",
68
+ text: `lower: ${node.runtime.lowerLimit.toFixed(2)}, upper adjusted to ${node.runtime.upperLimit.toFixed(2)}`
69
+ });
70
+ } else {
71
+ node.status({
72
+ fill: "green",
73
+ shape: "dot",
74
+ text: `lower: ${node.runtime.lowerLimit.toFixed(2)}`
75
+ });
76
+ }
77
+ break;
78
+ case "upperLimit":
79
+ node.runtime.upperLimit = value;
80
+ if (node.runtime.upperLimit < node.runtime.lowerLimit) {
81
+ node.runtime.lowerLimit = node.runtime.upperLimit;
82
+ node.status({
83
+ fill: "green",
84
+ shape: "dot",
85
+ text: `upper: ${node.runtime.upperLimit.toFixed(2)}, lower adjusted to ${node.runtime.lowerLimit.toFixed(2)}`
86
+ });
87
+ } else {
88
+ node.status({
89
+ fill: "green",
90
+ shape: "dot",
91
+ text: `upper: ${node.runtime.upperLimit.toFixed(2)}`
92
+ });
93
+ }
94
+ break;
95
+ case "period":
96
+ const multiplier = msg.units === "minutes" ? 60000 : msg.units === "seconds" ? 1000 : 1;
97
+ value *= multiplier;
98
+ if (value <= 0) {
99
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
100
+ if (done) done();
101
+ return;
102
+ }
103
+ node.runtime.period = value;
104
+ node.runtime.periodUnits = msg.units || "milliseconds";
105
+ node.status({
106
+ fill: "green",
107
+ shape: "dot",
108
+ text: `period: ${node.runtime.period.toFixed(2)} ms`
109
+ });
110
+ break;
111
+ default:
112
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
113
+ if (done) done("Unknown context");
114
+ return;
115
+ }
116
+ if (done) done();
117
+ return;
118
+ }
119
+
120
+ // Calculate time difference
121
+ const now = Date.now();
122
+ const deltaTime = (now - node.runtime.lastExecution) / 1000; // Seconds
123
+ node.runtime.lastExecution = now;
124
+
125
+ // Return lowerLimit if period is invalid
126
+ if (node.runtime.period <= 0) {
127
+ node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.lowerLimit.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}` });
128
+ send({ payload: node.runtime.lowerLimit });
129
+ if (done) done();
130
+ return;
131
+ }
132
+
133
+ // Update phase
134
+ node.runtime.phase = (node.runtime.phase + deltaTime / (node.runtime.period / 1000)) % 1;
135
+
136
+ // Triangle wave calculation
137
+ const triangleValue = node.runtime.phase < 0.5 ? 2 * node.runtime.phase : 2 * (1 - node.runtime.phase);
138
+ const amplitude = (node.runtime.upperLimit - node.runtime.lowerLimit) / 2;
139
+ const value = node.runtime.lowerLimit + amplitude * triangleValue;
140
+
141
+ // Output new message
142
+ node.status({ fill: "blue", shape: "dot", text: `out: ${value.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}` });
143
+ send({ payload: value });
144
+
145
+ if (done) done();
146
+ });
147
+
148
+ node.on("close", function(done) {
149
+ done();
150
+ });
151
+ }
152
+
153
+ RED.nodes.registerType("triangle-wave-block", TriangleWaveBlockNode);
154
+ };