@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,89 @@
1
+ module.exports = function(RED) {
2
+ function RoundBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ precision: config.precision
10
+ };
11
+
12
+ // Validate initial config
13
+ const validPrecisions = ["0.1", "0.5", "1.0"];
14
+ if (!validPrecisions.includes(node.runtime.precision)) {
15
+ node.runtime.precision = "1.0";
16
+ node.status({ fill: "red", shape: "ring", text: "invalid precision, using 1.0" });
17
+ } else {
18
+ node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name || "round"}, precision: ${node.runtime.precision}` });
19
+ }
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 precision configuration
32
+ if (msg.hasOwnProperty("context") && msg.context === "precision") {
33
+ if (!msg.hasOwnProperty("payload")) {
34
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
35
+ if (done) done();
36
+ return;
37
+ }
38
+ const newPrecision = String(msg.payload);
39
+ if (!validPrecisions.includes(newPrecision)) {
40
+ node.status({ fill: "red", shape: "ring", text: "invalid precision" });
41
+ if (done) done();
42
+ return;
43
+ }
44
+ node.runtime.precision = newPrecision;
45
+ node.status({ fill: "green", shape: "dot", text: `precision: ${newPrecision}` });
46
+ if (done) done();
47
+ return;
48
+ }
49
+
50
+ // Passthrough: Process payload if numeric, else pass unchanged
51
+ if (!msg.hasOwnProperty("payload")) {
52
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
53
+ send(msg);
54
+ if (done) done();
55
+ return;
56
+ }
57
+
58
+ const input = parseFloat(msg.payload);
59
+ if (isNaN(input) || !isFinite(input)) {
60
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
61
+ send(msg);
62
+ if (done) done();
63
+ return;
64
+ }
65
+
66
+ // Round based on precision
67
+ let result;
68
+ const precision = parseFloat(node.runtime.precision);
69
+ if (precision === 0.1) {
70
+ result = Math.round(input * 10) / 10;
71
+ } else if (precision === 0.5) {
72
+ result = Math.round(input / 0.5) * 0.5;
73
+ } else {
74
+ result = Math.round(input);
75
+ }
76
+
77
+ msg.payload = result;
78
+ node.status({ fill: "blue", shape: "dot", text: `in: ${input.toFixed(2)}, out: ${result.toFixed(2)}` });
79
+ send(msg);
80
+ if (done) done();
81
+ });
82
+
83
+ node.on("close", function(done) {
84
+ done();
85
+ });
86
+ }
87
+
88
+ RED.nodes.registerType("round-block", RoundBlockNode);
89
+ };
@@ -0,0 +1,87 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="saw-tooth-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("saw-tooth-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: "saw tooth wave",
44
+ label: function() {
45
+ return this.name || "saw tooth wave";
46
+ }
47
+ });
48
+ </script>
49
+
50
+ <!-- Help Section -->
51
+ <script type="text/markdown" data-help-name="saw-tooth-wave-block">
52
+ Generates a sawtooth 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.
58
+ : Any input triggers wave output.
59
+
60
+ ### Outputs
61
+ : payload (number) : Sawtooth wave value scaled between lowerLimit and upperLimit.
62
+
63
+ ### Properties
64
+ : name (string) : Display name in editor.
65
+ : lowerLimit (number) : Minimum output value.
66
+ : upperLimit (number) : Maximum output value (≥ lowerLimit).
67
+ : period (number) : Wave period (positive, in periodUnits).
68
+ : periodUnits (string) : Units for period (`"milliseconds"`, `"seconds"`, `"minutes"`).
69
+
70
+ ### Details
71
+ Generates a sawtooth wave output, linearly rising from `lowerLimit` to `upperLimit` over `period`, then dropping sharply, triggered by any input.
72
+
73
+ Tracks phase (0 to 1) for continuity. Configurable via editor or `msg.context` with numeric `msg.payload` and optional `msg.units`.
74
+
75
+ Ensures `upperLimit ≥ lowerLimit` by adjusting limits. Outputs `msg.payload` with wave value; if `period ≤ 0`, outputs `lowerLimit`.
76
+
77
+ ### Status
78
+ - Green (dot): Configuration update
79
+ - Blue (dot): State changed
80
+ - Blue (ring): State unchanged
81
+ - Red (ring): Error
82
+ - Yellow (ring): Warning
83
+
84
+ ### References
85
+ - [Node-RED Documentation](https://nodered.org/docs/)
86
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
87
+ </script>
@@ -0,0 +1,161 @@
1
+ module.exports = function(RED) {
2
+ function SawToothWaveBlockNode(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) || 10) * (config.periodUnits === "minutes" ? 60000 : config.periodUnits === "seconds" ? 1000 : 1),
12
+ periodUnits: config.periodUnits || "seconds",
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({
128
+ fill: "blue",
129
+ shape: "dot",
130
+ text: `out: ${node.runtime.lowerLimit.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}`
131
+ });
132
+ send({ payload: node.runtime.lowerLimit });
133
+ if (done) done();
134
+ return;
135
+ }
136
+
137
+ // Update phase
138
+ node.runtime.phase = (node.runtime.phase + deltaTime / (node.runtime.period / 1000)) % 1;
139
+
140
+ // Sawtooth wave calculation
141
+ const amplitude = node.runtime.upperLimit - node.runtime.lowerLimit;
142
+ const value = node.runtime.lowerLimit + amplitude * node.runtime.phase;
143
+
144
+ // Output new message
145
+ node.status({
146
+ fill: "blue",
147
+ shape: "dot",
148
+ text: `out: ${value.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}`
149
+ });
150
+ send({ payload: value });
151
+
152
+ if (done) done();
153
+ });
154
+
155
+ node.on("close", function(done) {
156
+ done();
157
+ });
158
+ }
159
+
160
+ RED.nodes.registerType("saw-tooth-wave-block", SawToothWaveBlockNode);
161
+ };
@@ -0,0 +1,90 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="scale-range-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-inMin" title="Minimum input value (number)"><i class="fa fa-arrow-down"></i> Input Min</label>
9
+ <input type="number" id="node-input-inMin" placeholder="0.0" step="any">
10
+ </div>
11
+ <div class="form-row">
12
+ <label for="node-input-inMax" title="Maximum input value (number, > inMin)"><i class="fa fa-arrow-up"></i> Input Max</label>
13
+ <input type="number" id="node-input-inMax" placeholder="100.0" step="any">
14
+ </div>
15
+ <div class="form-row">
16
+ <label for="node-input-outMin" title="Minimum output value (number)"><i class="fa fa-arrow-down"></i> Output Min</label>
17
+ <input type="number" id="node-input-outMin" placeholder="0.0" step="any">
18
+ </div>
19
+ <div class="form-row">
20
+ <label for="node-input-outMax" title="Maximum output value (number, > outMin)"><i class="fa fa-arrow-up"></i> Output Max</label>
21
+ <input type="number" id="node-input-outMax" placeholder="80.0" step="any">
22
+ </div>
23
+ <div class="form-row">
24
+ <label for="node-input-clamp" title="Clamp output to output range (boolean)"><i class="fa fa-lock"></i> Clamp</label>
25
+ <input type="checkbox" id="node-input-clamp" style="width: auto; vertical-align: middle;">
26
+ </div>
27
+ </script>
28
+
29
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
30
+ <script type="text/javascript">
31
+ RED.nodes.registerType("scale-range-block", {
32
+ category: "control",
33
+ color: "#301934",
34
+ defaults: {
35
+ name: { value: "" },
36
+ inMin: { value: 0.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
37
+ inMax: { value: 100.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
38
+ outMin: { value: 0.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
39
+ outMax: { value: 80.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
40
+ clamp: { value: true }
41
+ },
42
+ inputs: 1,
43
+ outputs: 1,
44
+ inputLabels: ["input"],
45
+ outputLabels: ["output"],
46
+ icon: "font-awesome/fa-arrows-h",
47
+ paletteLabel: "scale range",
48
+ label: function() {
49
+ return this.name || "scale range";
50
+ }
51
+ });
52
+ </script>
53
+
54
+ <!-- Help Section -->
55
+ <script type="text/markdown" data-help-name="scale-range-block">
56
+ Scales a numeric input from one range to another and passes the original message.
57
+
58
+ ### Inputs
59
+ : context (string) : Configures settings (`"inMin"`, `"inMax"`, `"outMin"`, `"outMax"`, `"clamp"`). Unmatched values trigger error.
60
+ : payload (number | boolean) : Number for input or range configuration, boolean for clamp.
61
+
62
+ ### Outputs
63
+ : payload (number) : Scaled output value.
64
+
65
+ ### Properties
66
+ : inMin (number) : Minimum input value.
67
+ : inMax (number) : Maximum input value (> inMin).
68
+ : outMin (number) : Minimum output value.
69
+ : outMax (number) : Maximum output value (> outMin).
70
+ : clamp (boolean) : Clamp output to output range.
71
+
72
+ ### Details
73
+ Scales `msg.payload` (number) from an input range (`inMin` to `inMax`) to an output range (`outMin` to `outMax`) using linear interpolation, with optional
74
+ clamping to `[outMin, outMax]` if `clamp` is `true`.
75
+
76
+ Outputs the input message with `msg.payload` set to the scaled value, preserving other properties.
77
+
78
+ Outputs on valid input or configuration change (recalculates with last input).
79
+
80
+ ### Status
81
+ - Green (dot): Configuration update
82
+ - Blue (dot): State changed
83
+ - Blue (ring): State unchanged
84
+ - Red (ring): Error
85
+ - Yellow (ring): Warning
86
+
87
+ ### References
88
+ - [Node-RED Documentation](https://nodered.org/docs/)
89
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
90
+ </script>
@@ -0,0 +1,137 @@
1
+ module.exports = function(RED) {
2
+ function ScaleRangeBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name || "",
9
+ inMin: parseFloat(config.inMin),
10
+ inMax: parseFloat(config.inMax),
11
+ outMin: parseFloat(config.outMin),
12
+ outMax: parseFloat(config.outMax),
13
+ clamp: config.clamp,
14
+ lastInput: parseFloat(config.inMin)
15
+ };
16
+
17
+ // Validate initial config
18
+ if (isNaN(node.runtime.inMin) || isNaN(node.runtime.inMax) || !isFinite(node.runtime.inMin) || !isFinite(node.runtime.inMax) || node.runtime.inMin >= node.runtime.inMax) {
19
+ node.runtime.inMin = 0.0;
20
+ node.runtime.inMax = 100.0;
21
+ node.runtime.lastInput = 0.0;
22
+ node.status({ fill: "red", shape: "ring", text: "invalid input range" });
23
+ }
24
+
25
+ node.on("input", function(msg, send, done) {
26
+ send = send || function() { node.send.apply(node, arguments); };
27
+
28
+ // Guard against invalid message
29
+ if (!msg) {
30
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
31
+ if (done) done();
32
+ return;
33
+ }
34
+
35
+ // Handle context updates
36
+ if (msg.hasOwnProperty("context")) {
37
+ if (!msg.hasOwnProperty("payload")) {
38
+ node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
39
+ if (done) done();
40
+ return;
41
+ }
42
+
43
+ let shouldOutput = false;
44
+ switch (msg.context) {
45
+ case "inMin":
46
+ case "inMax":
47
+ case "outMin":
48
+ case "outMax":
49
+ const value = parseFloat(msg.payload);
50
+ if (isNaN(value) || !isFinite(value)) {
51
+ node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
52
+ if (done) done();
53
+ return;
54
+ }
55
+ node.runtime[msg.context] = value;
56
+ if (node.runtime.inMax <= node.runtime.inMin) {
57
+ node.status({ fill: "red", shape: "ring", text: "invalid input range" });
58
+ if (done) done();
59
+ return;
60
+ }
61
+ if (node.runtime.outMax <= node.runtime.outMin) {
62
+ node.status({ fill: "red", shape: "ring", text: "invalid output range" });
63
+ if (done) done();
64
+ return;
65
+ }
66
+ node.status({ fill: "green", shape: "dot", text: `${msg.context}: ${value.toFixed(2)}` });
67
+ shouldOutput = true;
68
+ break;
69
+ case "clamp":
70
+ if (typeof msg.payload !== "boolean") {
71
+ node.status({ fill: "red", shape: "ring", text: "invalid clamp" });
72
+ if (done) done();
73
+ return;
74
+ }
75
+ node.runtime.clamp = msg.payload;
76
+ node.status({ fill: "green", shape: "dot", text: `clamp: ${node.runtime.clamp}` });
77
+ shouldOutput = true;
78
+ break;
79
+ default:
80
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
81
+ if (done) done("Unknown context");
82
+ return;
83
+ }
84
+
85
+ // Recalculate with last input after config update
86
+ if (shouldOutput) {
87
+ const out = calculate(node.runtime.lastInput, node.runtime.inMin, node.runtime.inMax, node.runtime.outMin, node.runtime.outMax, node.runtime.clamp);
88
+ msg.payload = out;
89
+ node.status({ fill: "blue", shape: "dot", text: `out: ${out.toFixed(2)}, in: ${node.runtime.lastInput.toFixed(2)}` });
90
+ send(msg);
91
+ }
92
+ if (done) done();
93
+ return;
94
+ }
95
+
96
+ // Validate input
97
+ if (!msg.hasOwnProperty("payload")) {
98
+ node.status({ fill: "red", shape: "ring", text: "missing input" });
99
+ if (done) done();
100
+ return;
101
+ }
102
+ const inputValue = parseFloat(msg.payload);
103
+ if (isNaN(inputValue) || !isFinite(inputValue)) {
104
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
105
+ if (done) done();
106
+ return;
107
+ }
108
+ if (node.runtime.inMax <= node.runtime.inMin) {
109
+ node.status({ fill: "red", shape: "ring", text: "inMinx must be < inMax" });
110
+ if (done) done();
111
+ return;
112
+ }
113
+
114
+ // Scale input
115
+ node.runtime.lastInput = inputValue;
116
+ const out = calculate(inputValue, node.runtime.inMin, node.runtime.inMax, node.runtime.outMin, node.runtime.outMax, node.runtime.clamp);
117
+ msg.payload = out;
118
+ node.status({ fill: "blue", shape: "dot", text: `out: ${out.toFixed(2)}, in: ${inputValue.toFixed(2)}` });
119
+ send(msg);
120
+
121
+ if (done) done();
122
+ });
123
+
124
+ // Scaling function
125
+ function calculate(input, inMin, inMax, outMin, outMax, clamp) {
126
+ const scaleRatio = (outMax - outMin) / (inMax - inMin);
127
+ let output = scaleRatio * (input - inMin) + outMin;
128
+ return clamp ? Math.max(outMin, Math.min(outMax, output)) : output;
129
+ }
130
+
131
+ node.on("close", function(done) {
132
+ done();
133
+ });
134
+ }
135
+
136
+ RED.nodes.registerType("scale-range-block", ScaleRangeBlockNode);
137
+ };
@@ -0,0 +1,88 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="sine-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("sine-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: "sine wave",
44
+ label: function() {
45
+ return this.name || "sine wave";
46
+ }
47
+ });
48
+ </script>
49
+
50
+ <!-- Help Section -->
51
+ <script type="text/markdown" data-help-name="sine-wave-block">
52
+ Generates a sine 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) : Sine 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 sine wave output, smoothly oscillating between `lowerLimit` and `upperLimit` over `period`, triggered by any input.
71
+
72
+ Tracks phase (0 to 1) for continuity. Configurable via editor or `msg.context` with numeric `msg.payload` and optional `msg.units`.
73
+
74
+ Ensures `upperLimit ≥ lowerLimit` by adjusting limits.
75
+
76
+ Outputs `msg.payload` with wave value; if `period ≤ 0`, outputs `lowerLimit`.
77
+
78
+ ### Status
79
+ - Green (dot): Configuration update
80
+ - Blue (dot): State changed
81
+ - Blue (ring): State unchanged
82
+ - Red (ring): Error
83
+ - Yellow (ring): Warning
84
+
85
+ ### References
86
+ - [Node-RED Documentation](https://nodered.org/docs/)
87
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
88
+ </script>