@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,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>
|