@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,142 @@
1
+ module.exports = function(RED) {
2
+ function SineWaveBlockNode(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({ fill: "green", shape: "dot", text: `lower: ${node.runtime.lowerLimit.toFixed(2)}` });
72
+ }
73
+ break;
74
+ case "upperLimit":
75
+ node.runtime.upperLimit = value;
76
+ if (node.runtime.upperLimit < node.runtime.lowerLimit) {
77
+ node.runtime.lowerLimit = node.runtime.upperLimit;
78
+ node.status({
79
+ fill: "green",
80
+ shape: "dot",
81
+ text: `upper: ${node.runtime.upperLimit.toFixed(2)}, lower adjusted to ${node.runtime.lowerLimit.toFixed(2)}`
82
+ });
83
+ } else {
84
+ node.status({ fill: "green", shape: "dot", text: `upper: ${node.runtime.upperLimit.toFixed(2)}` });
85
+ }
86
+ break;
87
+ case "period":
88
+ const multiplier = msg.units === "minutes" ? 60000 : msg.units === "seconds" ? 1000 : 1;
89
+ value *= multiplier;
90
+ if (value <= 0) {
91
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
92
+ if (done) done();
93
+ return;
94
+ }
95
+ node.runtime.period = value;
96
+ node.runtime.periodUnits = msg.units || "milliseconds";
97
+ node.status({ fill: "green", shape: "dot", text: `period: ${node.runtime.period.toFixed(2)} ms` });
98
+ break;
99
+ default:
100
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
101
+ if (done) done("Unknown context");
102
+ return;
103
+ }
104
+ if (done) done();
105
+ return;
106
+ }
107
+
108
+ // Calculate time difference
109
+ const now = Date.now();
110
+ const deltaTime = (now - node.runtime.lastExecution) / 1000; // Seconds
111
+ node.runtime.lastExecution = now;
112
+
113
+ // Return lowerLimit if period is invalid
114
+ if (node.runtime.period <= 0) {
115
+ node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.lowerLimit.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}` });
116
+ send({ payload: node.runtime.lowerLimit });
117
+ if (done) done();
118
+ return;
119
+ }
120
+
121
+ // Update phase
122
+ node.runtime.phase = (node.runtime.phase + deltaTime / (node.runtime.period / 1000)) % 1;
123
+
124
+ // Sine wave calculation
125
+ const sineValue = Math.sin(2 * Math.PI * node.runtime.phase);
126
+ const amplitude = (node.runtime.upperLimit - node.runtime.lowerLimit) / 2;
127
+ const value = node.runtime.lowerLimit + amplitude * (sineValue + 1);
128
+
129
+ // Output new message
130
+ node.status({ fill: "blue", shape: "dot", text: `out: ${value.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}` });
131
+ send({ payload: value });
132
+
133
+ if (done) done();
134
+ });
135
+
136
+ node.on("close", function(done) {
137
+ done();
138
+ });
139
+ }
140
+
141
+ RED.nodes.registerType("sine-wave-block", SineWaveBlockNode);
142
+ };
@@ -0,0 +1,64 @@
1
+ <script type="text/html" data-template-name="subtract-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("subtract-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: ["difference"],
28
+ icon: "font-awesome/fa-minus",
29
+ paletteLabel: "subtract",
30
+ label: function() {
31
+ return this.name ? `${this.name} (${this.slots})` : `subtract (${this.slots})`;
32
+ }
33
+ });
34
+ </script>
35
+
36
+ <script type="text/markdown" data-help-name="subtract-block">
37
+ Subtracts numeric inputs from multiple slots in sequence, preserving the input message.
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 or slots configuration, boolean for reset.
42
+
43
+ ### Outputs
44
+ : payload (number) : Result of subtracting slots in sequence (in1 - in2 - ...).
45
+ : *other* (any) : Other input message properties (e.g., `msg.context`, `msg.topic`) preserved.
46
+
47
+ ### Details
48
+ Subtracts numeric `msg.payload` values from slots identified by `msg.context` (e.g., `"in1"`, `"in2"`) in sequence.
49
+
50
+ Inputs default to 0, updated via `msg.context = "inX"`.
51
+
52
+ Resets inputs to 0 via `msg.context = "reset"` with `msg.payload = true`.
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>
@@ -0,0 +1,103 @@
1
+ module.exports = function(RED) {
2
+ function SubtractBlockNode(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)).fill(0),
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(0);
19
+ node.status({ fill: "red", shape: "ring", text: "invalid slots, using 2" });
20
+ } else {
21
+ node.status({
22
+ fill: "green",
23
+ shape: "dot",
24
+ text: `name: ${node.runtime.name}, slots: ${node.runtime.slots}`
25
+ });
26
+ }
27
+
28
+ node.on("input", function(msg, send, done) {
29
+ send = send || function() { node.send.apply(node, arguments); };
30
+
31
+ // Guard against invalid msg
32
+ if (!msg) {
33
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
34
+ if (done) done();
35
+ return;
36
+ }
37
+
38
+ // Check for missing context or payload
39
+ if (!msg.hasOwnProperty("context")) {
40
+ node.status({ fill: "red", shape: "ring", text: "missing context" });
41
+ if (done) done();
42
+ return;
43
+ }
44
+
45
+ if (!msg.hasOwnProperty("payload")) {
46
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
47
+ if (done) done();
48
+ return;
49
+ }
50
+
51
+ // Handle configuration messages
52
+ if (msg.context === "reset") {
53
+ if (typeof msg.payload !== "boolean") {
54
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
55
+ if (done) done();
56
+ return;
57
+ }
58
+ if (msg.payload === true) {
59
+ node.runtime.inputs = Array(node.runtime.slots).fill(0);
60
+ node.runtime.lastResult = null;
61
+ node.status({ fill: "green", shape: "dot", text: "state reset" });
62
+ if (done) done();
63
+ return;
64
+ }
65
+ } else if (msg.context.startsWith("in")) {
66
+ let slotIndex = parseInt(msg.context.slice(2)) - 1;
67
+ if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
68
+ node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
69
+ if (done) done();
70
+ return;
71
+ }
72
+ let newValue = parseFloat(msg.payload);
73
+ if (isNaN(newValue)) {
74
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
75
+ if (done) done();
76
+ return;
77
+ }
78
+ node.runtime.inputs[slotIndex] = newValue;
79
+
80
+ // Calculate subtraction
81
+ const result = node.runtime.inputs.reduce((acc, val, idx) => idx === 0 ? val : acc - val, 0);
82
+ const isUnchanged = result === node.runtime.lastResult;
83
+ node.status({ fill: "blue", shape: isUnchanged ? "ring" : "dot", text: `${msg.context}: ${newValue.toFixed(2)}, diff: ${result.toFixed(2)}` });
84
+
85
+ node.runtime.lastResult = result;
86
+ send({ payload: result });
87
+
88
+ if (done) done();
89
+ return;
90
+ } else {
91
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
92
+ if (done) done();
93
+ return;
94
+ }
95
+ });
96
+
97
+ node.on("close", function(done) {
98
+ done();
99
+ });
100
+ }
101
+
102
+ RED.nodes.registerType("subtract-block", SubtractBlockNode);
103
+ };
@@ -0,0 +1,81 @@
1
+ <script type="text/html" data-template-name="thermistor-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-R_fixed" title="Fixed resistor value in ohms for thermistor circuit"><i class="fa fa-wrench"></i> R_fixed (ohms)</label>
8
+ <input type="number" id="node-input-R_fixed" placeholder="23500" min="0.01" step="0.001">
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-input-Vsupply" title="Supply voltage in volts for the thermistor circuit"><i class="fa fa-bolt"></i> Vsupply (volts)</label>
12
+ <input type="number" id="node-input-Vsupply" placeholder="5.08" min="0.01" step="0.001">
13
+ </div>
14
+ <div class="form-row">
15
+ <label for="node-input-Vref" title="Reference voltage in volts for ADC conversion"><i class="fa fa-bolt"></i> Vref (volts)</label>
16
+ <input type="number" id="node-input-Vref" placeholder="4.096" min="0.01" step="0.001">
17
+ </div>
18
+ <div class="form-row">
19
+ <label for="node-input-ADC_max" title="Maximum ADC value for raw data conversion"><i class="fa fa-microchip"></i> ADC_max</label>
20
+ <input type="number" id="node-input-ADC_max" placeholder="32768" min="1" step="1">
21
+ </div>
22
+ </script>
23
+
24
+ <script type="text/javascript">
25
+ RED.nodes.registerType("thermistor-block", {
26
+ category: "control",
27
+ color: "#301934",
28
+ defaults: {
29
+ name: { value: "" },
30
+ R_fixed: { value: 23500, required: true, validate: function(v) { return Number(v) > 0; } },
31
+ Vsupply: { value: 5.08, required: true, validate: function(v) { return Number(v) > 0; } },
32
+ Vref: { value: 4.096, required: true, validate: function(v) { return Number(v) > 0; } },
33
+ ADC_max: { value: 32768, required: true, validate: function(v) { return Number(v) > 0; } }
34
+ },
35
+ inputs: 1,
36
+ outputs: 2,
37
+ inputLabels: ["raw data"],
38
+ outputLabels: ["voltage", "resistance"],
39
+ icon: "font-awesome/fa-thermometer",
40
+ paletteLabel: "thermistor",
41
+ label: function() {
42
+ return this.name || "thermistor";
43
+ }
44
+ });
45
+ </script>
46
+
47
+ <script type="text/markdown" data-help-name="thermistor-block">
48
+ Converts a 16-bit raw sensor value into voltage and thermistor resistance.
49
+
50
+ ### Inputs
51
+ : payload (array | buffer | object) : Two-byte array `[highByte, lowByte]`, 2-byte buffer, or object `type "Buffer", data [highByte, lowByte]` representing a 16-bit raw value.
52
+
53
+ ### Outputs
54
+ : payload (number) : Output 1 -> Calculated voltage in volts.
55
+ : payload (number) : Output 2 -> Calculated thermistor resistance in ohms.
56
+
57
+ ### Details
58
+ Converts raw ADC data from a thermistor circuit into voltage and resistance. Calculates
59
+ - Voltage: `(raw * Vref) / ADC_max`, where `raw = (highByte << 8) | lowByte`.
60
+ - Resistance: `R_fixed * (voltage / (Vsupply - voltage))`.
61
+
62
+ Configuration (set in editor, no runtime changes):
63
+ : R_fixed (number) : Fixed resistor value in ohms
64
+ : Vsupply (number) : Supply voltage in volts
65
+ : Vref (number) : Reference voltage in volts for ADC conversion
66
+ : ADC_max (number) : Maximum ADC value
67
+ : name (string) : Optional display name.
68
+
69
+ Outputs new messages for both outputs when values change. Stores `lastVoltage` and `lastResistance` in context for persistence.
70
+
71
+ ### Status
72
+ - Green (dot): Configuration update
73
+ - Blue (dot): State changed
74
+ - Blue (ring): State unchanged
75
+ - Red (ring): Error
76
+ - Yellow (ring): Warning
77
+
78
+ ### References
79
+ - [Node-RED Documentation](https://nodered.org/docs/)
80
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
81
+ </script>
@@ -0,0 +1,146 @@
1
+ module.exports = function(RED) {
2
+ function ThermistorBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ const context = this.context();
6
+
7
+ // Initialize runtime state
8
+ node.runtime = {
9
+ name: config.name,
10
+ R_fixed: parseFloat(config.R_fixed),
11
+ Vsupply: parseFloat(config.Vsupply),
12
+ Vref: parseFloat(config.Vref),
13
+ ADC_max: parseFloat(config.ADC_max),
14
+ lastVoltage: context.get("lastVoltage"),
15
+ lastResistance: context.get("lastResistance")
16
+ };
17
+
18
+ // Validate configuration
19
+ if (isNaN(node.runtime.R_fixed) || node.runtime.R_fixed <= 0) {
20
+ node.status({ fill: "red", shape: "ring", text: "invalid r_fixed" });
21
+ node.warn(`Invalid configuration: r_fixed=${node.runtime.R_fixed}`);
22
+ return;
23
+ }
24
+ if (isNaN(node.runtime.Vsupply) || node.runtime.Vsupply <= 0) {
25
+ node.status({ fill: "red", shape: "ring", text: "invalid vsupply" });
26
+ node.warn(`Invalid configuration: vsupply=${node.runtime.Vsupply}`);
27
+ return;
28
+ }
29
+ if (isNaN(node.runtime.Vref) || node.runtime.Vref <= 0) {
30
+ node.status({ fill: "red", shape: "ring", text: "invalid vref" });
31
+ node.warn(`Invalid configuration: vref=${node.runtime.Vref}`);
32
+ return;
33
+ }
34
+ if (isNaN(node.runtime.ADC_max) || node.runtime.ADC_max <= 0) {
35
+ node.status({ fill: "red", shape: "ring", text: "invalid adc_max" });
36
+ node.warn(`Invalid configuration: adc_max=${node.runtime.ADC_max}`);
37
+ return;
38
+ }
39
+
40
+ // Set initial status
41
+ node.status({ fill: "green", shape: "dot", text: `r_fixed: ${node.runtime.R_fixed}, vsupply: ${node.runtime.Vsupply}` });
42
+
43
+ node.on("input", function(msg, send, done) {
44
+ send = send || function() { node.send.apply(node, arguments); };
45
+
46
+ // Validate input
47
+ if (!msg || typeof msg !== "object") {
48
+ node.status({ fill: "red", shape: "ring", text: "missing message" });
49
+ node.warn(`Missing message`);
50
+ if (done) done();
51
+ return;
52
+ }
53
+
54
+ let inputArray;
55
+ if (Buffer.isBuffer(msg.payload)) {
56
+ if (msg.payload.length !== 2) {
57
+ node.status({ fill: "red", shape: "ring", text: "invalid input: expected 2-byte buffer" });
58
+ node.warn(`Invalid input: expected 2-byte buffer, got ${JSON.stringify(msg.payload)}`);
59
+ if (done) done();
60
+ return;
61
+ }
62
+ inputArray = [msg.payload[0], msg.payload[1]];
63
+ } else if (typeof msg.payload === "object" && msg.payload.type === "Buffer" && Array.isArray(msg.payload.data) && msg.payload.data.length === 2) {
64
+ inputArray = msg.payload.data;
65
+ if (typeof inputArray[0] !== "number" || typeof inputArray[1] !== "number") {
66
+ node.status({ fill: "red", shape: "ring", text: "invalid input: expected numeric [highByte, lowByte]" });
67
+ node.warn(`Invalid input: expected numeric [highByte, lowByte], got ${JSON.stringify(msg.payload)}`);
68
+ if (done) done();
69
+ return;
70
+ }
71
+ } else if (Array.isArray(msg.payload) && msg.payload.length === 2 && typeof msg.payload[0] === "number" && typeof msg.payload[1] === "number") {
72
+ inputArray = msg.payload;
73
+ } else {
74
+ node.status({ fill: "red", shape: "ring", text: "invalid input: expected [highByte, lowByte] or 2-byte buffer" });
75
+ node.warn(`Invalid input: expected [highByte, lowByte] or 2-byte buffer, got ${JSON.stringify(msg.payload)}`);
76
+ if (done) done();
77
+ return;
78
+ }
79
+
80
+ try {
81
+ // Calculate raw 16-bit value
82
+ const raw = (inputArray[0] << 8) | inputArray[1];
83
+ if (raw < 0 || raw > node.runtime.ADC_max) {
84
+ node.status({ fill: "red", shape: "ring", text: "raw value out of range" });
85
+ node.warn(`Raw value ${raw} out of range [0, ${node.runtime.ADC_max}]`);
86
+ if (done) done();
87
+ return;
88
+ }
89
+
90
+ // Calculate voltage
91
+ const voltage = (raw * node.runtime.Vref) / node.runtime.ADC_max;
92
+ if (voltage >= node.runtime.Vsupply || voltage <= 0) {
93
+ node.status({ fill: "red", shape: "ring", text: "voltage out of range" });
94
+ node.warn(`Voltage ${voltage} out of range (0, ${node.runtime.Vsupply})`);
95
+ if (done) done();
96
+ return;
97
+ }
98
+
99
+ // Calculate thermistor resistance
100
+ const R_thermistor = node.runtime.R_fixed * (voltage / (node.runtime.Vsupply - voltage));
101
+ if (isNaN(R_thermistor) || R_thermistor < 0) {
102
+ node.status({ fill: "red", shape: "ring", text: "invalid resistance" });
103
+ node.warn(`Invalid resistance ${R_thermistor}`);
104
+ if (done) done();
105
+ return;
106
+ }
107
+
108
+ // Check if outputs have changed
109
+ const isUnchanged = voltage === node.runtime.lastVoltage && R_thermistor === node.runtime.lastResistance;
110
+ node.status({
111
+ fill: "blue",
112
+ shape: isUnchanged ? "ring" : "dot",
113
+ text: `in: ${raw}, out: ${voltage.toFixed(2)}, ${R_thermistor.toFixed(2)}`
114
+ });
115
+
116
+ if (!isUnchanged) {
117
+ // Update context and runtime
118
+ node.runtime.lastVoltage = voltage;
119
+ node.runtime.lastResistance = R_thermistor;
120
+ context.set("lastVoltage", voltage);
121
+ context.set("lastResistance", R_thermistor);
122
+
123
+ // Send outputs
124
+ send([
125
+ { payload: voltage },
126
+ { payload: R_thermistor }
127
+ ]);
128
+ }
129
+
130
+ } catch (error) {
131
+ node.status({ fill: "red", shape: "ring", text: "calculation error" });
132
+ node.warn(`Calculation error: ${error.message}`);
133
+ if (done) done(error);
134
+ return;
135
+ }
136
+
137
+ if (done) done();
138
+ });
139
+
140
+ node.on("close", function(done) {
141
+ done();
142
+ });
143
+ }
144
+
145
+ RED.nodes.registerType("thermistor-block", ThermistorBlockNode);
146
+ };
@@ -0,0 +1,66 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="tick-tock-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-period" title="Period in seconds (positive number, e.g., 10)"><i class="fa fa-clock-o"></i> Period</label>
9
+ <input type="number" id="node-input-period" placeholder="10" min="0.001" step="any">
10
+ </div>
11
+ </script>
12
+
13
+ <!-- JavaScript Section -->
14
+ <script type="text/javascript">
15
+ RED.nodes.registerType("tick-tock-block", {
16
+ category: "control",
17
+ color: "#301934",
18
+ defaults: {
19
+ name: { value: "" },
20
+ period: { value: 10, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0 && isFinite(parseFloat(v)); } }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ inputLabels: ["input"],
25
+ outputLabels: ["output"],
26
+ icon: "font-awesome/fa-wave-square",
27
+ paletteLabel: "tick tock",
28
+ label: function() {
29
+ return this.name || "tick tock";
30
+ }
31
+ });
32
+ </script>
33
+
34
+ <!-- Help Section -->
35
+ <script type="text/markdown" data-help-name="tick-tock-block">
36
+ Generates a square wave output toggling between true and false.
37
+
38
+ ### Inputs
39
+ : context (string) : Configures settings (`"period"`, `"command"`). Unmatched values trigger error.
40
+ : payload (number | string) : Numeric value for period, string (`"start"`, `"stop"`) for command.
41
+
42
+ ### Outputs
43
+ : payload (boolean) : Square wave value (`true` or `false`).
44
+
45
+ ### Properties
46
+ : period (number) : Full wave period (seconds, positive).
47
+
48
+ ### Details
49
+ Generates a square wave output toggling between `true` and `false` every half `period` (seconds), started via `msg.context = "command"` with
50
+ `msg.payload = "start"`. Stops with `"stop"`.
51
+
52
+ Outputs `msg.payload for each state change when running.
53
+
54
+ Ignores redundant `start`/`stop` commands.
55
+
56
+ ### Status
57
+ - Green (dot): Configuration update
58
+ - Blue (dot): State changed
59
+ - Blue (ring): State unchanged
60
+ - Red (ring): Error
61
+ - Yellow (ring): Warning
62
+
63
+ ### References
64
+ - [Node-RED Documentation](https://nodered.org/docs/)
65
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
66
+ </script>