@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.
- package/README.md +43 -0
- package/nodes/accumulate-block.html +71 -0
- package/nodes/accumulate-block.js +104 -0
- package/nodes/add-block.html +67 -0
- package/nodes/add-block.js +97 -0
- package/nodes/analog-switch-block.html +65 -0
- package/nodes/analog-switch-block.js +129 -0
- package/nodes/and-block.html +64 -0
- package/nodes/and-block.js +73 -0
- package/nodes/average-block.html +97 -0
- package/nodes/average-block.js +137 -0
- package/nodes/boolean-switch-block.html +59 -0
- package/nodes/boolean-switch-block.js +88 -0
- package/nodes/boolean-to-number-block.html +59 -0
- package/nodes/boolean-to-number-block.js +45 -0
- package/nodes/cache-block.html +69 -0
- package/nodes/cache-block.js +106 -0
- package/nodes/call-status-block.html +111 -0
- package/nodes/call-status-block.js +274 -0
- package/nodes/changeover-block.html +234 -0
- package/nodes/changeover-block.js +392 -0
- package/nodes/comment-block.html +83 -0
- package/nodes/comment-block.js +53 -0
- package/nodes/compare-block.html +64 -0
- package/nodes/compare-block.js +84 -0
- package/nodes/contextual-label-block.html +67 -0
- package/nodes/contextual-label-block.js +52 -0
- package/nodes/convert-block.html +179 -0
- package/nodes/convert-block.js +289 -0
- package/nodes/count-block.html +57 -0
- package/nodes/count-block.js +92 -0
- package/nodes/debounce-block.html +64 -0
- package/nodes/debounce-block.js +140 -0
- package/nodes/delay-block.html +104 -0
- package/nodes/delay-block.js +180 -0
- package/nodes/divide-block.html +65 -0
- package/nodes/divide-block.js +123 -0
- package/nodes/edge-block.html +71 -0
- package/nodes/edge-block.js +120 -0
- package/nodes/frequency-block.html +55 -0
- package/nodes/frequency-block.js +140 -0
- package/nodes/hysteresis-block.html +131 -0
- package/nodes/hysteresis-block.js +142 -0
- package/nodes/interpolate-block.html +74 -0
- package/nodes/interpolate-block.js +141 -0
- package/nodes/load-sequence-block.html +134 -0
- package/nodes/load-sequence-block.js +272 -0
- package/nodes/max-block.html +76 -0
- package/nodes/max-block.js +103 -0
- package/nodes/memory-block.html +90 -0
- package/nodes/memory-block.js +241 -0
- package/nodes/min-block.html +77 -0
- package/nodes/min-block.js +106 -0
- package/nodes/minmax-block.html +89 -0
- package/nodes/minmax-block.js +119 -0
- package/nodes/modulo-block.html +73 -0
- package/nodes/modulo-block.js +126 -0
- package/nodes/multiply-block.html +63 -0
- package/nodes/multiply-block.js +115 -0
- package/nodes/negate-block.html +55 -0
- package/nodes/negate-block.js +91 -0
- package/nodes/nullify-block.html +111 -0
- package/nodes/nullify-block.js +78 -0
- package/nodes/on-change-block.html +79 -0
- package/nodes/on-change-block.js +191 -0
- package/nodes/oneshot-block.html +96 -0
- package/nodes/oneshot-block.js +169 -0
- package/nodes/or-block.html +64 -0
- package/nodes/or-block.js +73 -0
- package/nodes/pid-block.html +205 -0
- package/nodes/pid-block.js +407 -0
- package/nodes/priority-block.html +66 -0
- package/nodes/priority-block.js +239 -0
- package/nodes/rate-limit-block.html +99 -0
- package/nodes/rate-limit-block.js +221 -0
- package/nodes/round-block.html +73 -0
- package/nodes/round-block.js +89 -0
- package/nodes/saw-tooth-wave-block.html +87 -0
- package/nodes/saw-tooth-wave-block.js +161 -0
- package/nodes/scale-range-block.html +90 -0
- package/nodes/scale-range-block.js +137 -0
- package/nodes/sine-wave-block.html +88 -0
- package/nodes/sine-wave-block.js +142 -0
- package/nodes/subtract-block.html +64 -0
- package/nodes/subtract-block.js +103 -0
- package/nodes/thermistor-block.html +81 -0
- package/nodes/thermistor-block.js +146 -0
- package/nodes/tick-tock-block.html +66 -0
- package/nodes/tick-tock-block.js +110 -0
- package/nodes/time-sequence-block.html +67 -0
- package/nodes/time-sequence-block.js +144 -0
- package/nodes/triangle-wave-block.html +86 -0
- package/nodes/triangle-wave-block.js +154 -0
- package/nodes/tstat-block.html +311 -0
- package/nodes/tstat-block.js +499 -0
- package/nodes/units-block.html +150 -0
- package/nodes/units-block.js +106 -0
- 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>
|