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