@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,90 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="memory-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-writePeriod" title="Delay in milliseconds before writing to file (non-negative number from msg, flow, global, or static value). Ignored if Write On Update is enabled."><i class="fa fa-clock-o"></i> File Write Period (ms)</label>
|
|
9
|
+
<input type="text" id="node-input-writePeriod" placeholder="60000">
|
|
10
|
+
<input type="hidden" id="node-input-writePeriodType">
|
|
11
|
+
</div>
|
|
12
|
+
<div class="form-row">
|
|
13
|
+
<label for="node-input-transferProperty" title="Property to transfer from stored message to output message"><i class="fa fa-exchange"></i> Transfer Property</label>
|
|
14
|
+
<input type="text" id="node-input-transferProperty" placeholder="payload">
|
|
15
|
+
</div>
|
|
16
|
+
<div class="form-row">
|
|
17
|
+
<label for="node-input-writeOnUpdate" title="If checked, updates write directly to file without storing in memory. Execute reads from file."><i class="fa fa-save"></i> Write On Update</label>
|
|
18
|
+
<input type="checkbox" id="node-input-writeOnUpdate" style="width: auto;">
|
|
19
|
+
</div>
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<!-- JavaScript Section -->
|
|
23
|
+
<script type="text/javascript">
|
|
24
|
+
RED.nodes.registerType("memory-block", {
|
|
25
|
+
category: "control",
|
|
26
|
+
color: "#301934",
|
|
27
|
+
defaults: {
|
|
28
|
+
name: { value: "" },
|
|
29
|
+
writePeriod: { value: "60000", validate: function(v) { return RED.validators.number(true)(v) && parseFloat(v) >= 0; } },
|
|
30
|
+
writePeriodType: { value: "num" },
|
|
31
|
+
transferProperty: { value: "payload", required: true, validate: RED.validators.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/) },
|
|
32
|
+
writeOnUpdate: { value: false }
|
|
33
|
+
},
|
|
34
|
+
inputs: 1,
|
|
35
|
+
outputs: 2,
|
|
36
|
+
inputLabels: ["input"],
|
|
37
|
+
outputLabels: ["query", "value"],
|
|
38
|
+
icon: "font-awesome/fa-database",
|
|
39
|
+
paletteLabel: "memory",
|
|
40
|
+
label: function() {
|
|
41
|
+
return this.name || "memory";
|
|
42
|
+
},
|
|
43
|
+
oneditprepare: function() {
|
|
44
|
+
const periodInput = $("#node-input-writePeriod").typedInput({
|
|
45
|
+
default: "num",
|
|
46
|
+
types: ["num", "msg", "flow", "global"],
|
|
47
|
+
typeField: "#node-input-writePeriodType"
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<!-- Help Section -->
|
|
55
|
+
<script type="text/markdown" data-help-name="memory-block">
|
|
56
|
+
Stores a message property persistently across reboots and redeployments, with configurable property transfer and optional direct file writes.
|
|
57
|
+
|
|
58
|
+
### Inputs
|
|
59
|
+
: context (string) : Action (`"update"` to store message property, `"execute"` to output stored property, `"executeWithFallback"` to output stored property or store and output fallback, `"query"` to check stored value existence).
|
|
60
|
+
: [transferProperty] (any) : Property to store (`"update"`, `"executeWithFallback"`) or pass through (no `context`).
|
|
61
|
+
|
|
62
|
+
### Outputs
|
|
63
|
+
: query : For `context = "query"`; `{ payload; boolean }` (`true` if stored value exists, `false` if none).
|
|
64
|
+
: value : For no `context`; Input `msg` unchanged. For `context = "execute"`; Input `msg` with `transferProperty` from stored message, or `{ payload; null }` if none. For `context = "executeWithFallback"`; Input `msg` with `transferProperty` from stored message, or stores and outputs `msg[transferProperty]`. For `context = "update"`; No output.
|
|
65
|
+
|
|
66
|
+
### Properties
|
|
67
|
+
: writePeriod (string) : Delay in milliseconds before writing to file (non-negative, from `msg`, `flow`, `global`, or static). Ignored if Write On Update is enabled.
|
|
68
|
+
: writePeriodType (string) : Source of `writePeriod` (`"num"`, `"msg"`, `"flow"`, `"global"`).
|
|
69
|
+
: transferProperty (string) : Property to transfer from stored message to output message (valid JavaScript property name).
|
|
70
|
+
: writeOnUpdate (boolean) : If true, `update` writes directly to file without in-memory storage; `execute` and `executeWithFallback` read from file.
|
|
71
|
+
|
|
72
|
+
### Details
|
|
73
|
+
Stores a single message property persistently in a JSON file, surviving Node-RED reboots and redeployments. Actions;
|
|
74
|
+
- `msg.context = "update"` Stores `msg[transferProperty]`. If Write On Update is enabled, writes directly to file without in-memory storage; otherwise, stores in memory and context, schedules file write after `writePeriod` (ms, default 60000). No output.
|
|
75
|
+
- `msg.context = "execute"` Outputs input `msg` with `transferProperty` from stored message to Output 2 if available, or `{ payload; null }` if none. Reads from file if Write On Update is enabled, else uses in-memory value. No file write.
|
|
76
|
+
- `msg.context = "executeWithFallback"` Outputs input `msg` with `transferProperty` from stored message to Output 2 if available; if none, stores `msg[transferProperty]`, schedules file write (or writes directly if Write On Update is enabled), and outputs `msg[transferProperty]`.
|
|
77
|
+
- `msg.context = "query"` Outputs `{ payload; true }` to Output 1 if a stored value exists, `{ payload; false }` if none. Checks file existence if Write On Update is enabled. No file write or Output 2 message.
|
|
78
|
+
- No `msg.context` Passes input `msg` unchanged to Output 2, no effect on stored message or file.
|
|
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,241 @@
|
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fsSync = require("fs");
|
|
4
|
+
|
|
5
|
+
module.exports = function(RED) {
|
|
6
|
+
function MemoryBlockNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
|
|
10
|
+
// Initialize runtime state
|
|
11
|
+
node.runtime = {
|
|
12
|
+
name: config.name,
|
|
13
|
+
writePeriod: config.writePeriod,
|
|
14
|
+
writePeriodType: config.writePeriodType,
|
|
15
|
+
transferProperty: config.transferProperty,
|
|
16
|
+
writeOnUpdate: config.writeOnUpdate === true,
|
|
17
|
+
storedMsg: null
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// File path for persistent storage
|
|
21
|
+
const filePath = path.join(RED.settings.userDir, `memory-${node.id}.json`);
|
|
22
|
+
|
|
23
|
+
let writeTimeout = null;
|
|
24
|
+
let lastUpdateMsg = null;
|
|
25
|
+
|
|
26
|
+
// Load stored message from file
|
|
27
|
+
async function loadStoredMessage() {
|
|
28
|
+
try {
|
|
29
|
+
const data = await fs.readFile(filePath, "utf8");
|
|
30
|
+
node.runtime.storedMsg = JSON.parse(data);
|
|
31
|
+
const payloadStr = node.runtime.storedMsg[node.runtime.transferProperty] != null ? String(node.runtime.storedMsg[node.runtime.transferProperty]).substring(0, 20) : "null";
|
|
32
|
+
node.status({ fill: "green", shape: "dot", text: `loaded: ${payloadStr}` });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err.code !== "ENOENT") {
|
|
35
|
+
node.status({ fill: "red", shape: "ring", text: "file error" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Read message from file synchronously (for execute and executeWithFallback when writeOnUpdate is true)
|
|
41
|
+
function readStoredMessageSync() {
|
|
42
|
+
try {
|
|
43
|
+
if (fsSync.existsSync(filePath)) {
|
|
44
|
+
const data = fsSync.readFileSync(filePath, "utf8");
|
|
45
|
+
return JSON.parse(data);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
node.status({ fill: "red", shape: "ring", text: "file read error" });
|
|
50
|
+
node.error("Failed to read stored message: " + err.message);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Save message to file
|
|
56
|
+
async function saveMessage() {
|
|
57
|
+
if (lastUpdateMsg === null) return;
|
|
58
|
+
try {
|
|
59
|
+
await fs.writeFile(filePath, JSON.stringify(lastUpdateMsg));
|
|
60
|
+
lastUpdateMsg = null;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
node.status({ fill: "red", shape: "ring", text: "file error" });
|
|
63
|
+
node.error("Failed to save message: " + err.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initialize (load only if writeOnUpdate is false)
|
|
68
|
+
if (!node.runtime.writeOnUpdate) {
|
|
69
|
+
loadStoredMessage().catch(err => {
|
|
70
|
+
node.error("Failed to load stored message: " + err.message);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
node.on("input", function(msg, send, done) {
|
|
75
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
76
|
+
|
|
77
|
+
// Guard against invalid message
|
|
78
|
+
if (!msg) {
|
|
79
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
80
|
+
if (done) done();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve typed inputs
|
|
85
|
+
node.runtime.writePeriod = RED.util.evaluateNodeProperty(
|
|
86
|
+
config.writePeriod, config.writePeriodType, node, msg
|
|
87
|
+
);
|
|
88
|
+
node.runtime.writePeriod = parseFloat(node.runtime.writePeriod);
|
|
89
|
+
|
|
90
|
+
// Initialize output array: [Output 1, Output 2]
|
|
91
|
+
const output = [null, null];
|
|
92
|
+
|
|
93
|
+
// Handle context
|
|
94
|
+
if (!msg.hasOwnProperty("context") || !msg.context || typeof msg.context !== "string") {
|
|
95
|
+
// Pass-through message to Output 2
|
|
96
|
+
const payloadStr = msg[node.runtime.transferProperty] != null ? String(msg[node.runtime.transferProperty]).substring(0, 20) : "null";
|
|
97
|
+
node.status({ fill: "blue", shape: "dot", text: `in: ${payloadStr}, out2: ${payloadStr}` });
|
|
98
|
+
output[1] = msg;
|
|
99
|
+
send(output);
|
|
100
|
+
if (done) done();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (msg.context === "update") {
|
|
105
|
+
if (!msg.hasOwnProperty(node.runtime.transferProperty)) {
|
|
106
|
+
node.status({ fill: "red", shape: "ring", text: `missing ${node.runtime.transferProperty}` });
|
|
107
|
+
if (done) done();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const payloadStr = msg[node.runtime.transferProperty] != null ? String(msg[node.runtime.transferProperty]).substring(0, 20) : "null";
|
|
111
|
+
if (node.runtime.writeOnUpdate) {
|
|
112
|
+
// Write directly to file, do not store in memory
|
|
113
|
+
try {
|
|
114
|
+
fs.writeFile(filePath, JSON.stringify(msg)).catch(err => {
|
|
115
|
+
node.status({ fill: "red", shape: "ring", text: "file error" });
|
|
116
|
+
node.error("Failed to save message: " + err.message);
|
|
117
|
+
});
|
|
118
|
+
node.status({ fill: "green", shape: "dot", text: `updated: ${payloadStr}` });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
node.status({ fill: "red", shape: "ring", text: "file error" });
|
|
121
|
+
node.error("Failed to save message: " + err.message);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Original behavior: store in memory and context, delay write
|
|
125
|
+
node.runtime.storedMsg = RED.util.cloneMessage(msg);
|
|
126
|
+
node.context().set("storedMsg", node.runtime.storedMsg);
|
|
127
|
+
lastUpdateMsg = node.runtime.storedMsg;
|
|
128
|
+
node.status({ fill: "green", shape: "dot", text: `updated: ${payloadStr}` });
|
|
129
|
+
if (writeTimeout) clearTimeout(writeTimeout);
|
|
130
|
+
writeTimeout = setTimeout(() => {
|
|
131
|
+
saveMessage();
|
|
132
|
+
}, writePeriod);
|
|
133
|
+
}
|
|
134
|
+
if (done) done();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (msg.context === "execute") {
|
|
139
|
+
let storedMsg = node.runtime.writeOnUpdate ? readStoredMessageSync() : node.runtime.storedMsg;
|
|
140
|
+
if (storedMsg !== null) {
|
|
141
|
+
const outMsg = RED.util.cloneMessage(msg);
|
|
142
|
+
outMsg[node.runtime.transferProperty] = storedMsg[node.runtime.transferProperty];
|
|
143
|
+
const payloadStr = outMsg[node.runtime.transferProperty] != null ? String(outMsg[node.runtime.transferProperty]).substring(0, 20) : "null";
|
|
144
|
+
node.status({ fill: "blue", shape: "dot", text: `in: execute, out2: ${payloadStr}` });
|
|
145
|
+
output[1] = outMsg;
|
|
146
|
+
} else {
|
|
147
|
+
node.status({ fill: "blue", shape: "ring", text: `in: execute, out2: null` });
|
|
148
|
+
output[1] = { payload: null };
|
|
149
|
+
}
|
|
150
|
+
send(output);
|
|
151
|
+
if (done) done();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (msg.context === "executeWithFallback") {
|
|
156
|
+
let storedMsg = node.runtime.writeOnUpdate ? readStoredMessageSync() : node.runtime.storedMsg;
|
|
157
|
+
if (storedMsg !== null) {
|
|
158
|
+
const outMsg = RED.util.cloneMessage(msg);
|
|
159
|
+
outMsg[node.runtime.transferProperty] = storedMsg[node.runtime.transferProperty];
|
|
160
|
+
const payloadStr = outMsg[node.runtime.transferProperty] != null ? String(outMsg[node.runtime.transferProperty]).substring(0, 20) : "null";
|
|
161
|
+
node.status({ fill: "blue", shape: "dot", text: `in: executeWithFallback, out2: ${payloadStr}` });
|
|
162
|
+
output[1] = outMsg;
|
|
163
|
+
} else {
|
|
164
|
+
let value;
|
|
165
|
+
if (msg.hasOwnProperty(node.runtime.transferProperty)) {
|
|
166
|
+
value = msg[node.runtime.transferProperty];
|
|
167
|
+
}
|
|
168
|
+
else if (msg.hasOwnProperty("fallback")) {
|
|
169
|
+
value = msg.fallback;
|
|
170
|
+
} else {
|
|
171
|
+
node.status({ fill: "red", shape: "ring", text: `missing ${node.runtime.transferProperty}` });
|
|
172
|
+
if (done) done();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (node.runtime.writeOnUpdate) {
|
|
177
|
+
// Write directly to file
|
|
178
|
+
try {
|
|
179
|
+
fs.writeFile(filePath, JSON.stringify({ [node.runtime.transferProperty]: value })).catch(err => {
|
|
180
|
+
node.status({ fill: "red", shape: "ring", text: "file error" });
|
|
181
|
+
node.error("Failed to save message: " + err.message);
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
node.status({ fill: "red", shape: "ring", text: "file error" });
|
|
185
|
+
node.error("Failed to save message: " + err.message);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// Store in memory and context
|
|
189
|
+
node.runtime.storedMsg = { [node.runtime.transferProperty]: value };
|
|
190
|
+
node.context().set("storedMsg", node.runtime.storedMsg);
|
|
191
|
+
lastUpdateMsg = node.runtime.storedMsg;
|
|
192
|
+
if (writeTimeout) clearTimeout(writeTimeout);
|
|
193
|
+
writeTimeout = setTimeout(() => {
|
|
194
|
+
saveMessage();
|
|
195
|
+
}, writePeriod);
|
|
196
|
+
}
|
|
197
|
+
const outMsg = RED.util.cloneMessage(msg);
|
|
198
|
+
outMsg[node.runtime.transferProperty] = value;
|
|
199
|
+
const payloadStr = msg[node.runtime.transferProperty] != null ? String(msg[node.runtime.transferProperty]).substring(0, 20) : "null";
|
|
200
|
+
node.status({ fill: "blue", shape: "dot", text: `in: executeWithFallback, out2: ${payloadStr}` });
|
|
201
|
+
output[1] = outMsg;
|
|
202
|
+
}
|
|
203
|
+
send(output);
|
|
204
|
+
if (done) done();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (msg.context === "query") {
|
|
209
|
+
const hasValue = node.runtime.writeOnUpdate ? fsSync.existsSync(filePath) : node.runtime.storedMsg !== null;
|
|
210
|
+
node.status({ fill: "blue", shape: "dot", text: `in: query, out1: ${hasValue}` });
|
|
211
|
+
output[0] = { payload: hasValue };
|
|
212
|
+
send(output);
|
|
213
|
+
if (done) done();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
218
|
+
if (done) done("Unknown context");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
node.on("close", function(done) {
|
|
222
|
+
if (writeTimeout) clearTimeout(writeTimeout);
|
|
223
|
+
if (!node.runtime.writeOnUpdate && lastUpdateMsg) {
|
|
224
|
+
saveMessage()
|
|
225
|
+
.then(() => {
|
|
226
|
+
node.status({});
|
|
227
|
+
done();
|
|
228
|
+
})
|
|
229
|
+
.catch(err => {
|
|
230
|
+
node.error("Failed to save message on close: " + err.message);
|
|
231
|
+
node.status({});
|
|
232
|
+
done();
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
done();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
RED.nodes.registerType("memory-block", MemoryBlockNode);
|
|
241
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="min-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-min" title="Minimum value for capping the input number"><i class="fa fa-arrow-down"></i> Min</label>
|
|
9
|
+
<input type="text" id="node-input-min" placeholder="50" min="0" step="any">
|
|
10
|
+
<input type="hidden" id="node-input-minType">
|
|
11
|
+
</div>
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
15
|
+
<script type="text/javascript">
|
|
16
|
+
RED.nodes.registerType("min-block", {
|
|
17
|
+
category: "control",
|
|
18
|
+
color: "#301934",
|
|
19
|
+
defaults: {
|
|
20
|
+
name: { value: "" },
|
|
21
|
+
min: { value: 50, required: true },
|
|
22
|
+
minType: { value: "num" },
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
inputLabels: ["input"],
|
|
27
|
+
outputLabels: ["output"],
|
|
28
|
+
icon: "font-awesome/fa-arrow-circle-down",
|
|
29
|
+
paletteLabel: "min",
|
|
30
|
+
label: function() {
|
|
31
|
+
return this.name || "min";
|
|
32
|
+
},
|
|
33
|
+
oneditprepare: function() {
|
|
34
|
+
const node = this;
|
|
35
|
+
|
|
36
|
+
// Initialize typed inputs
|
|
37
|
+
$("#node-input-min").typedInput({
|
|
38
|
+
default: "num",
|
|
39
|
+
types: ["num", "msg", "flow", "global"],
|
|
40
|
+
typeField: "#node-input-minType"
|
|
41
|
+
}).typedInput("type", node.minType || "num").typedInput("value", node.min);
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<!-- Help Section -->
|
|
48
|
+
<script type="text/markdown" data-help-name="min-block">
|
|
49
|
+
Ensures a numeric input is at least a configurable minimum value.
|
|
50
|
+
|
|
51
|
+
### Inputs
|
|
52
|
+
: context (string) : Configures minimum value (`"min"`, `"setpoint"`). Unmatched values trigger warning.
|
|
53
|
+
: payload (number) : Input number to cap or new minimum value with `msg.context`.
|
|
54
|
+
|
|
55
|
+
### Outputs
|
|
56
|
+
: payload (number) : Input number capped at the minimum value.
|
|
57
|
+
: *other* (any) : All input properties preserved, including `msg.context`.
|
|
58
|
+
|
|
59
|
+
### Properties
|
|
60
|
+
: min (number) : Minimum value for capping.
|
|
61
|
+
|
|
62
|
+
### Details
|
|
63
|
+
Ensures `msg.payload` (a number) is at least the minimum value, forwarding the input message with updated `msg.payload`.
|
|
64
|
+
|
|
65
|
+
Outputs on every valid input, with status indicating whether the output changed.
|
|
66
|
+
|
|
67
|
+
### Status
|
|
68
|
+
- Green (dot): Configuration update
|
|
69
|
+
- Blue (dot): State changed
|
|
70
|
+
- Blue (ring): State unchanged
|
|
71
|
+
- Red (ring): Error
|
|
72
|
+
- Yellow (ring): Warning
|
|
73
|
+
|
|
74
|
+
### References
|
|
75
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
76
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
77
|
+
</script>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function MinBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Store last output value for status
|
|
12
|
+
let lastOutput = null;
|
|
13
|
+
|
|
14
|
+
node.on("input", function(msg, send, done) {
|
|
15
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
16
|
+
|
|
17
|
+
// Evaluate typed-inputs
|
|
18
|
+
try {
|
|
19
|
+
node.runtime.min = RED.util.evaluateNodeProperty(
|
|
20
|
+
config.min, config.minType, node, msg
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Validate values
|
|
24
|
+
if (isNaN(node.runtime.min)) {
|
|
25
|
+
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
26
|
+
if (done) done();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
} catch(err) {
|
|
30
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
31
|
+
if (done) done(err);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
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 min" });
|
|
46
|
+
if (done) done();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (msg.context === "min" || msg.context === "setpoint") {
|
|
50
|
+
const minValue = parseFloat(msg.payload);
|
|
51
|
+
if (!isNaN(minValue) && minValue >= 0) {
|
|
52
|
+
node.runtime.min = minValue;
|
|
53
|
+
node.status({
|
|
54
|
+
fill: "green",
|
|
55
|
+
shape: "dot",
|
|
56
|
+
text: `min: ${minValue}`
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
node.status({ fill: "red", shape: "ring", text: "invalid min" });
|
|
60
|
+
}
|
|
61
|
+
if (done) done();
|
|
62
|
+
return;
|
|
63
|
+
} else {
|
|
64
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
65
|
+
if (done) done();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate input payload
|
|
71
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
72
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
73
|
+
if (done) done();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const inputValue = parseFloat(msg.payload);
|
|
78
|
+
if (isNaN(inputValue)) {
|
|
79
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
80
|
+
if (done) done();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cap input at min
|
|
85
|
+
const outputValue = Math.max(inputValue, node.runtime.min);
|
|
86
|
+
|
|
87
|
+
// Update status and send output
|
|
88
|
+
msg.payload = outputValue;
|
|
89
|
+
node.status({
|
|
90
|
+
fill: "blue",
|
|
91
|
+
shape: lastOutput === outputValue ? "ring" : "dot",
|
|
92
|
+
text: `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`
|
|
93
|
+
});
|
|
94
|
+
lastOutput = outputValue;
|
|
95
|
+
send(msg);
|
|
96
|
+
|
|
97
|
+
if (done) done();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
node.on("close", function(done) {
|
|
101
|
+
done();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
RED.nodes.registerType("min-block", MinBlockNode);
|
|
106
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="minmax-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-min" title="Minimum value for clamping the input number"><i class="fa fa-arrow-down"></i> Min</label>
|
|
9
|
+
<input type="text" id="node-input-min" placeholder="0" min="0" step="any">
|
|
10
|
+
<input type="hidden" id="node-input-minType">
|
|
11
|
+
</div>
|
|
12
|
+
<div class="form-row">
|
|
13
|
+
<label for="node-input-max" title="Maximum value for clamping the input number"><i class="fa fa-arrow-up"></i> Max</label>
|
|
14
|
+
<input type="text" id="node-input-max" placeholder="100" min="0" step="any">
|
|
15
|
+
<input type="hidden" id="node-input-maxType">
|
|
16
|
+
</div>
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
20
|
+
<script type="text/javascript">
|
|
21
|
+
RED.nodes.registerType("minmax-block", {
|
|
22
|
+
category: "control",
|
|
23
|
+
color: "#301934",
|
|
24
|
+
defaults: {
|
|
25
|
+
name: { value: "" },
|
|
26
|
+
min: { value: 0, required: true },
|
|
27
|
+
max: { value: 100, required: true }
|
|
28
|
+
},
|
|
29
|
+
inputs: 1,
|
|
30
|
+
outputs: 1,
|
|
31
|
+
inputLabels: ["input"],
|
|
32
|
+
outputLabels: ["output"],
|
|
33
|
+
icon: "font-awesome/fa-arrows-v",
|
|
34
|
+
paletteLabel: "minmax",
|
|
35
|
+
label: function() {
|
|
36
|
+
return this.name || "minmax";
|
|
37
|
+
},
|
|
38
|
+
oneditprepare: function() {
|
|
39
|
+
const node = this;
|
|
40
|
+
|
|
41
|
+
// Initialize typed inputs
|
|
42
|
+
$("#node-input-min").typedInput({
|
|
43
|
+
default: "num",
|
|
44
|
+
types: ["num", "msg", "flow", "global"],
|
|
45
|
+
typeField: "#node-input-minType"
|
|
46
|
+
}).typedInput("type", node.minType || "num").typedInput("value", node.min);
|
|
47
|
+
|
|
48
|
+
$("#node-input-max").typedInput({
|
|
49
|
+
default: "num",
|
|
50
|
+
types: ["num", "msg", "flow", "global"],
|
|
51
|
+
typeField: "#node-input-maxType"
|
|
52
|
+
}).typedInput("type", node.maxType || "num").typedInput("value", node.max);
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!-- Help Section -->
|
|
59
|
+
<script type="text/markdown" data-help-name="minmax-block">
|
|
60
|
+
Clamps a numeric input to a configurable range.
|
|
61
|
+
|
|
62
|
+
### Inputs
|
|
63
|
+
: context (string) : Configures range (`"min"`, `"max"`). Unmatched values trigger warning.
|
|
64
|
+
: payload (number) : Input number to clamp or new min/max value with `msg.context`.
|
|
65
|
+
|
|
66
|
+
### Outputs
|
|
67
|
+
: payload (number) : Input number clamped to `[min, max]`.
|
|
68
|
+
: *other* (any) : All input properties preserved, including `msg.context`.
|
|
69
|
+
|
|
70
|
+
### Properties
|
|
71
|
+
: min (number) : Minimum value for clamping (≥ 0).
|
|
72
|
+
: max (number) : Maximum value for clamping (≥ 0, ≥ min)..
|
|
73
|
+
|
|
74
|
+
### Details
|
|
75
|
+
Clamps `msg.payload` (a number) to the range `[min, max]`, forwarding the input message with updated `msg.payload`.
|
|
76
|
+
|
|
77
|
+
Ensures `min ≤ max` by adjusting the other value if needed (e.g., setting `min > max` sets `max = min`).
|
|
78
|
+
|
|
79
|
+
### Status
|
|
80
|
+
- Green (dot): Configuration update
|
|
81
|
+
- Blue (dot): State changed
|
|
82
|
+
- Blue (ring): State unchanged
|
|
83
|
+
- Red (ring): Error
|
|
84
|
+
- Yellow (ring): Warning
|
|
85
|
+
|
|
86
|
+
### References
|
|
87
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
88
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
89
|
+
</script>
|