@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,111 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="nullify-block">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label><i class="fa fa-eraser"></i> Rules</label>
|
|
8
|
+
<ol id="node-input-rule-container"></ol>
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script type="text/javascript">
|
|
13
|
+
RED.nodes.registerType("nullify-block", {
|
|
14
|
+
category: "control",
|
|
15
|
+
color: "#301934",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: "" },
|
|
18
|
+
rules: { value: [{ property: "payload", propertyType: "msg" }], required: true }
|
|
19
|
+
},
|
|
20
|
+
inputs: 1,
|
|
21
|
+
outputs: 1,
|
|
22
|
+
inputLabels: ["input"],
|
|
23
|
+
outputLabels: ["null"],
|
|
24
|
+
icon: "font-awesome/fa-eraser",
|
|
25
|
+
paletteLabel: "nullify",
|
|
26
|
+
label: function() {
|
|
27
|
+
return this.name || "Nullify";
|
|
28
|
+
},
|
|
29
|
+
oneditprepare: function() {
|
|
30
|
+
const node = this;
|
|
31
|
+
const $container = $("#node-input-rule-container");
|
|
32
|
+
|
|
33
|
+
// Initialize rule list
|
|
34
|
+
function addRule(rule) {
|
|
35
|
+
const index = $container.find("li").length;
|
|
36
|
+
const $li = $("<li>", { class: "node-input-rule-row" }).appendTo($container);
|
|
37
|
+
const $property = $("<input>", { type: "text", class: "node-input-rule-property" }).appendTo($li);
|
|
38
|
+
const $delete = $("<a>", { href: "#", class: "red-ui-button red-ui-button-small", style: "margin-left: 5px" })
|
|
39
|
+
.html('<i class="fa fa-remove"></i>')
|
|
40
|
+
.appendTo($li);
|
|
41
|
+
|
|
42
|
+
$property.typedInput({
|
|
43
|
+
default: rule.propertyType || "msg",
|
|
44
|
+
types: ["msg"],
|
|
45
|
+
typeField: $("<input>", { type: "hidden", class: "node-input-rule-property-type" }).appendTo($li)
|
|
46
|
+
}).typedInput("value", rule.property || "");
|
|
47
|
+
|
|
48
|
+
$delete.click(function(e) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
$li.remove();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Populate existing rules
|
|
55
|
+
(node.rules || [{ property: "payload", propertyType: "msg" }]).forEach(addRule);
|
|
56
|
+
|
|
57
|
+
// Add new rule button
|
|
58
|
+
$("<a>", { href: "#", class: "red-ui-button", style: "margin-top: 5px" })
|
|
59
|
+
.text("Add rule")
|
|
60
|
+
.click(function(e) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
addRule({ property: "", propertyType: "msg" });
|
|
63
|
+
})
|
|
64
|
+
.insertAfter($container);
|
|
65
|
+
},
|
|
66
|
+
oneditsave: function() {
|
|
67
|
+
const rules = [];
|
|
68
|
+
$("#node-input-rule-container").find(".node-input-rule-row").each(function() {
|
|
69
|
+
const $property = $(this).find(".node-input-rule-property");
|
|
70
|
+
const property = $property.typedInput("value");
|
|
71
|
+
const propertyType = $property.typedInput("type");
|
|
72
|
+
if (property) {
|
|
73
|
+
rules.push({ property, propertyType });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
this.rules = rules.length > 0 ? rules : [{ property: "payload", propertyType: "msg" }];
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<script type="text/markdown" data-help-name="nullify-block">
|
|
82
|
+
Sets multiple specified message properties to null.
|
|
83
|
+
|
|
84
|
+
### Inputs
|
|
85
|
+
: context (string) : Configures rules (`"rules"`). Unmatched values ignored.
|
|
86
|
+
: payload (any) : Input message with properties to nullify.
|
|
87
|
+
|
|
88
|
+
### Outputs
|
|
89
|
+
: payload (any) : Input message with specified properties set to `null`.
|
|
90
|
+
: *other* (any) : Other input properties preserved.
|
|
91
|
+
|
|
92
|
+
### Details
|
|
93
|
+
Sets multiple configured `msg` properties to `null`, creating each property if it doesn't exist.
|
|
94
|
+
|
|
95
|
+
Properties are specified via a dynamic rule list in the editor, each defining a `msg` property.
|
|
96
|
+
|
|
97
|
+
Outputs the modified input message, preserving all other properties.
|
|
98
|
+
|
|
99
|
+
Invalid inputs (missing message, invalid rule configuration) prevent output and show error status.
|
|
100
|
+
|
|
101
|
+
### Status
|
|
102
|
+
- Green (dot): Configuration update
|
|
103
|
+
- Blue (dot): State changed
|
|
104
|
+
- Blue (ring): State unchanged
|
|
105
|
+
- Red (ring): Error
|
|
106
|
+
- Yellow (ring): Warning
|
|
107
|
+
|
|
108
|
+
### References
|
|
109
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
110
|
+
- [GitHub Repository](https://github.com/your-repo/node-red-contrib-buildingblocks-control)
|
|
111
|
+
</script>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function NullifyBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name,
|
|
9
|
+
rules: config.rules
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Validate configuration
|
|
13
|
+
let valid = true;
|
|
14
|
+
node.runtime.rules = node.runtime.rules.map(rule => {
|
|
15
|
+
if (rule.propertyType !== "msg" || !rule.property || typeof rule.property !== "string" || !rule.property.trim()) {
|
|
16
|
+
valid = false;
|
|
17
|
+
return { property: "payload", propertyType: "msg" };
|
|
18
|
+
}
|
|
19
|
+
return rule;
|
|
20
|
+
});
|
|
21
|
+
if (!valid) {
|
|
22
|
+
node.status({ fill: "red", shape: "ring", text: "invalid rules, using defaults" });
|
|
23
|
+
} else {
|
|
24
|
+
node.status({ fill: "green", shape: "dot", text: `rules: ${node.runtime.rules.map(r => r.property).join(", ")}` });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
node.on("input", function(msg, send, done) {
|
|
28
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
29
|
+
|
|
30
|
+
// Guard against invalid message
|
|
31
|
+
if (!msg) {
|
|
32
|
+
node.status({ fill: "red", shape: "ring", text: "missing message" });
|
|
33
|
+
if (done) done();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle configuration messages
|
|
38
|
+
if (msg.context) {
|
|
39
|
+
if (typeof msg.context !== "string" || !msg.context.trim()) {
|
|
40
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
41
|
+
if (done) done();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (msg.context === "rules") {
|
|
45
|
+
if (!msg.hasOwnProperty("payload") || !Array.isArray(msg.payload) || !msg.payload.every(r => r.property && typeof r.property === "string" && r.propertyType === "msg")) {
|
|
46
|
+
node.status({ fill: "red", shape: "ring", text: "invalid rules" });
|
|
47
|
+
if (done) done();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
node.runtime.rules = msg.payload;
|
|
51
|
+
node.status({ fill: "green", shape: "dot", text: `rules: ${node.runtime.rules.map(r => r.property).join(", ")}` });
|
|
52
|
+
if (done) done();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Apply nullification rules
|
|
58
|
+
const outputMsg = RED.util.cloneMessage(msg);
|
|
59
|
+
const nullified = [];
|
|
60
|
+
node.runtime.rules.forEach(rule => {
|
|
61
|
+
RED.util.setMessageProperty(outputMsg, rule.property, null);
|
|
62
|
+
nullified.push(rule.property);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Update status and send output
|
|
66
|
+
node.status({ fill: "blue", shape: "dot", text: `nullified: ${nullified.join(", ")}` });
|
|
67
|
+
send(outputMsg);
|
|
68
|
+
|
|
69
|
+
if (done) done();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
node.on("close", function(done) {
|
|
73
|
+
done();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
RED.nodes.registerType("nullify-block", NullifyBlockNode);
|
|
78
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="on-change-block">
|
|
3
|
+
<div class="form-row">
|
|
4
|
+
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
5
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
6
|
+
</div>
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-period" title="Filter period in milliseconds (non-negative number from msg, flow, global, or static value)"><i class="fa fa-clock-o"></i> Filter Period (ms)</label>
|
|
9
|
+
<input type="text" id="node-input-period" placeholder="0">
|
|
10
|
+
<input type="hidden" id="node-input-periodType">
|
|
11
|
+
</div>
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- JavaScript Section -->
|
|
15
|
+
<script type="text/javascript">
|
|
16
|
+
RED.nodes.registerType("on-change-block", {
|
|
17
|
+
category: "control",
|
|
18
|
+
color: "#301934",
|
|
19
|
+
defaults: {
|
|
20
|
+
name: { value: "" },
|
|
21
|
+
period: { value: 0 },
|
|
22
|
+
periodType: { value: "num" }
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
inputLabels: ["input"],
|
|
27
|
+
outputLabels: ["output"],
|
|
28
|
+
icon: "font-awesome/fa-filter",
|
|
29
|
+
paletteLabel: "on change",
|
|
30
|
+
label: function() {
|
|
31
|
+
return this.name || "on change";
|
|
32
|
+
},
|
|
33
|
+
oneditprepare: function() {
|
|
34
|
+
const periodInput = $("#node-input-period").typedInput({
|
|
35
|
+
default: "num",
|
|
36
|
+
types: ["num", "msg", "flow", "global"],
|
|
37
|
+
typeField: "#node-input-periodType"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<!-- Help Section -->
|
|
44
|
+
<script type="text/markdown" data-help-name="on-change-block">
|
|
45
|
+
Filters redundant messages based on value changes and a configurable period.
|
|
46
|
+
|
|
47
|
+
### Inputs
|
|
48
|
+
: payload (any) : Input value to compare.
|
|
49
|
+
: context (string, optional) : Configures period (`"period"`) or queries state (`"status"`). Unknown `msg.context` is ignored
|
|
50
|
+
: payload (number | any, for `context`) : Non-negative number for `"period"` (ms), any for `"status"`.
|
|
51
|
+
|
|
52
|
+
### Outputs
|
|
53
|
+
: msg (any) : Input message if value changed and not filtered.
|
|
54
|
+
: payload (object, for `msg.context = "status"`) : `{ period, periodType }`.
|
|
55
|
+
|
|
56
|
+
### Properties
|
|
57
|
+
: name (string) : Display name in editor. Default "" (shows "on change").
|
|
58
|
+
: period (number) : Filter period in milliseconds (≥ 0, static or from `msg`, `flow`, `global`). Default 0.
|
|
59
|
+
: periodType (string) : Source of period (`"num"`, `"msg"`, `"flow"`, `"global"`). Default `"num"`.
|
|
60
|
+
|
|
61
|
+
### Details
|
|
62
|
+
Filters messages based on value changes (deep comparison) and a filter period. When `period = 0`, outputs the input message only if `msg.payload`
|
|
63
|
+
differs from the last output value. When `period > 0`, outputs the first message, suppresses all messages during the period (including blips),
|
|
64
|
+
and after the period, outputs only if the value differs from the last output. Supports complex payloads (objects, arrays) via deep comparison.
|
|
65
|
+
Configuration
|
|
66
|
+
- `msg.context = "period"` Sets period (ms), no output.
|
|
67
|
+
- `msg.context = "status"` Outputs `{ period, periodType }`.
|
|
68
|
+
|
|
69
|
+
### Status
|
|
70
|
+
- Green (dot): Configuration update
|
|
71
|
+
- Blue (dot): State changed
|
|
72
|
+
- Blue (ring): State unchanged
|
|
73
|
+
- Red (ring): Error
|
|
74
|
+
- Yellow (ring): Warning
|
|
75
|
+
|
|
76
|
+
### References
|
|
77
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
78
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function OnChangeBlockNode(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: Number(config.period),
|
|
10
|
+
periodType: config.periodType,
|
|
11
|
+
lastValue: null,
|
|
12
|
+
periodValue: null,
|
|
13
|
+
blockTimer: null,
|
|
14
|
+
pendingMsg: null
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Validate initial config
|
|
18
|
+
if (isNaN(node.runtime.period) || node.runtime.period < 0) {
|
|
19
|
+
node.runtime.period = 0;
|
|
20
|
+
node.status({ fill: "red", shape: "ring", text: "invalid period, using 0" });
|
|
21
|
+
} else {
|
|
22
|
+
node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name || "on change"}, period: ${node.runtime.period.toFixed(0)} ms` });
|
|
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") && typeof msg.context === "string") {
|
|
37
|
+
const contextLower = msg.context.toLowerCase();
|
|
38
|
+
if (contextLower === "period") {
|
|
39
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
40
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload for period" });
|
|
41
|
+
if (done) done();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const newPeriod = parseFloat(msg.payload);
|
|
45
|
+
if (isNaN(newPeriod) || newPeriod < 0) {
|
|
46
|
+
node.status({ fill: "red", shape: "ring", text: "invalid period" });
|
|
47
|
+
if (done) done();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
node.runtime.period = newPeriod;
|
|
51
|
+
node.runtime.periodType = "num";
|
|
52
|
+
node.status({
|
|
53
|
+
fill: "green",
|
|
54
|
+
shape: "dot",
|
|
55
|
+
text: `period: ${node.runtime.period.toFixed(0)} ms`
|
|
56
|
+
});
|
|
57
|
+
if (done) done();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (contextLower === "status") {
|
|
61
|
+
send({
|
|
62
|
+
payload: {
|
|
63
|
+
period: node.runtime.period,
|
|
64
|
+
periodType: node.runtime.periodType
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (done) done();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Ignore unknown context
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate input payload
|
|
74
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
75
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
76
|
+
send(msg);
|
|
77
|
+
if (done) done();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get period
|
|
82
|
+
let period;
|
|
83
|
+
try {
|
|
84
|
+
period = RED.util.evaluateNodeProperty(
|
|
85
|
+
node.runtime.period,
|
|
86
|
+
node.runtime.periodType,
|
|
87
|
+
node,
|
|
88
|
+
msg
|
|
89
|
+
);
|
|
90
|
+
if (isNaN(period) || period < 0) {
|
|
91
|
+
throw new Error("invalid period");
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
node.status({ fill: "red", shape: "ring", text: "invalid period" });
|
|
95
|
+
send(msg);
|
|
96
|
+
if (done) done();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const currentValue = msg.payload;
|
|
101
|
+
|
|
102
|
+
// Deep comparison function
|
|
103
|
+
function isEqual(a, b) {
|
|
104
|
+
if (a === b) return true;
|
|
105
|
+
if (typeof a !== typeof b) return false;
|
|
106
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
107
|
+
if (a.length !== b.length) return false;
|
|
108
|
+
return a.every((item, i) => isEqual(item, b[i]));
|
|
109
|
+
}
|
|
110
|
+
if (typeof a === "object" && a !== null && b !== null) {
|
|
111
|
+
const keysA = Object.keys(a);
|
|
112
|
+
const keysB = Object.keys(b);
|
|
113
|
+
if (keysA.length !== keysB.length) return false;
|
|
114
|
+
return keysA.every(key => isEqual(a[key], b[key]));
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle input during filter period
|
|
120
|
+
if (node.runtime.blockTimer) {
|
|
121
|
+
node.runtime.pendingMsg = RED.util.cloneMessage(msg);
|
|
122
|
+
node.status({
|
|
123
|
+
fill: "blue",
|
|
124
|
+
shape: "ring",
|
|
125
|
+
text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}`
|
|
126
|
+
});
|
|
127
|
+
if (done) done();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if value changed
|
|
132
|
+
if (!isEqual(currentValue, node.runtime.lastValue)) {
|
|
133
|
+
node.runtime.lastValue = RED.util.cloneMessage(currentValue);
|
|
134
|
+
node.runtime.periodValue = RED.util.cloneMessage(currentValue);
|
|
135
|
+
node.status({
|
|
136
|
+
fill: "blue",
|
|
137
|
+
shape: "dot",
|
|
138
|
+
text: `out: ${JSON.stringify(currentValue).slice(0, 20)}`
|
|
139
|
+
});
|
|
140
|
+
send(msg);
|
|
141
|
+
|
|
142
|
+
// Start filter period if applicable
|
|
143
|
+
if (period > 0) {
|
|
144
|
+
node.runtime.blockTimer = setTimeout(() => {
|
|
145
|
+
node.runtime.blockTimer = null;
|
|
146
|
+
if (node.runtime.pendingMsg) {
|
|
147
|
+
const pendingValue = node.runtime.pendingMsg.payload;
|
|
148
|
+
if (!isEqual(pendingValue, node.runtime.lastValue)) {
|
|
149
|
+
node.runtime.lastValue = RED.util.cloneMessage(pendingValue);
|
|
150
|
+
node.runtime.periodValue = RED.util.cloneMessage(pendingValue);
|
|
151
|
+
node.status({
|
|
152
|
+
fill: "blue",
|
|
153
|
+
shape: "dot",
|
|
154
|
+
text: `out: ${JSON.stringify(pendingValue).slice(0, 20)}`
|
|
155
|
+
});
|
|
156
|
+
send(node.runtime.pendingMsg);
|
|
157
|
+
} else {
|
|
158
|
+
node.status({
|
|
159
|
+
fill: "blue",
|
|
160
|
+
shape: "ring",
|
|
161
|
+
text: `unchanged: ${JSON.stringify(pendingValue).slice(0, 20)}`
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
node.runtime.pendingMsg = null;
|
|
165
|
+
} else {
|
|
166
|
+
node.status({});
|
|
167
|
+
}
|
|
168
|
+
}, period);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
node.status({
|
|
172
|
+
fill: "blue",
|
|
173
|
+
shape: "ring",
|
|
174
|
+
text: `unchanged: ${JSON.stringify(currentValue).slice(0, 20)}`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (done) done();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
node.on("close", function(done) {
|
|
182
|
+
if (node.runtime.blockTimer) {
|
|
183
|
+
clearTimeout(node.runtime.blockTimer);
|
|
184
|
+
node.runtime.blockTimer = null;
|
|
185
|
+
}
|
|
186
|
+
done();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
RED.nodes.registerType("on-change-block", OnChangeBlockNode);
|
|
191
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!-- UI Template Section -->
|
|
2
|
+
<script type="text/html" data-template-name="oneshot-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-duration" title="Duration of true pulse (positive number)"><i class="fa fa-clock-o"></i> Duration</label>
|
|
9
|
+
<input type="number" id="node-input-duration" placeholder="1000" min="0" step="any">
|
|
10
|
+
<select id="node-input-durationUnits">
|
|
11
|
+
<option value="milliseconds">Milliseconds</option>
|
|
12
|
+
<option value="seconds">Seconds</option>
|
|
13
|
+
<option value="minutes">Minutes</option>
|
|
14
|
+
</select>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="form-row">
|
|
17
|
+
<label for="node-input-resetRequireTrue" title="Require msg.payload = true for reset"><i class="fa fa-check"></i> Reset Require True</label>
|
|
18
|
+
<input type="checkbox" id="node-input-resetRequireTrue" style="width: auto; vertical-align: middle;" checked>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="form-row">
|
|
21
|
+
<label for="node-input-resetOnComplete" title="Automatically reset (unlock) after pulse duration"><i class="fa fa-undo"></i> Reset On Complete</label>
|
|
22
|
+
<input type="checkbox" id="node-input-resetOnComplete" style="width: auto; vertical-align: middle;">
|
|
23
|
+
</div>
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<!-- JavaScript Section -->
|
|
27
|
+
<script type="text/javascript">
|
|
28
|
+
RED.nodes.registerType("oneshot-block", {
|
|
29
|
+
category: "control",
|
|
30
|
+
color: "#301934",
|
|
31
|
+
defaults: {
|
|
32
|
+
name: { value: "" },
|
|
33
|
+
duration: {
|
|
34
|
+
value: 1000,
|
|
35
|
+
required: true,
|
|
36
|
+
validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0; }
|
|
37
|
+
},
|
|
38
|
+
durationUnits: { value: "milliseconds" },
|
|
39
|
+
resetRequireTrue: { value: true },
|
|
40
|
+
resetOnComplete: { value: false }
|
|
41
|
+
},
|
|
42
|
+
inputs: 1,
|
|
43
|
+
outputs: 1,
|
|
44
|
+
inputLabels: ["trigger/reset"],
|
|
45
|
+
outputLabels: ["pulse"],
|
|
46
|
+
icon: "font-awesome/fa-bolt",
|
|
47
|
+
paletteLabel: "oneshot",
|
|
48
|
+
label: function() {
|
|
49
|
+
return this.name || `oneshot (${this.duration}${this.durationUnits.charAt(0)})`;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<!-- Help Section -->
|
|
55
|
+
<script type="text/markdown" data-help-name="oneshot-block">
|
|
56
|
+
Outputs a true pulse for a configurable duration when triggered with `msg.payload = true`, then false, and locks until reset.
|
|
57
|
+
|
|
58
|
+
### Inputs
|
|
59
|
+
: context (string) : Configures node (`"reset"`, `"duration"`). Unmatched values trigger error.
|
|
60
|
+
: payload (boolean | number) : `true` triggers pulse if not locked; number for `"duration"`; boolean for `"reset"`.
|
|
61
|
+
: units (string) : Units for `"duration"` (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
62
|
+
|
|
63
|
+
### Outputs
|
|
64
|
+
: payload (boolean) : `true` for `duration` on trigger, then `false`; `false` on reset or if locked.
|
|
65
|
+
|
|
66
|
+
### Properties
|
|
67
|
+
: name (string) : Display name in editor.
|
|
68
|
+
: duration (number) : Pulse duration (positive number).
|
|
69
|
+
: durationUnits (string) : Units for duration (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
70
|
+
: resetRequireTrue (boolean) : Require `msg.payload = true` for reset.
|
|
71
|
+
: resetOnComplete (boolean) : Automatically reset (unlock) after pulse duration.
|
|
72
|
+
|
|
73
|
+
### Details
|
|
74
|
+
Generates a `true` pulse for `duration` when triggered with `msg.payload = true`,
|
|
75
|
+
then outputs `false`.
|
|
76
|
+
|
|
77
|
+
Locks until reset via `msg.context = "reset"` (requires `msg.payload = true` if `resetRequireTrue = true`) or
|
|
78
|
+
automatically if `resetOnComplete = true`. Non-`true` payloads are ignored with no output.
|
|
79
|
+
|
|
80
|
+
Duration configurable via editor or `msg.context = "duration"` with `msg.units` (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
81
|
+
|
|
82
|
+
Tracks `triggerCount` (number of triggers) and displays in status.
|
|
83
|
+
|
|
84
|
+
Outputs new `{ payload boolean }` messages for every trigger (`true` then `false`), reset, or locked input.
|
|
85
|
+
|
|
86
|
+
### Status
|
|
87
|
+
- Green (dot): Configuration update
|
|
88
|
+
- Blue (dot): State changed
|
|
89
|
+
- Blue (ring): State unchanged
|
|
90
|
+
- Red (ring): Error
|
|
91
|
+
- Yellow (ring): Warning
|
|
92
|
+
|
|
93
|
+
### References
|
|
94
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
95
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
96
|
+
</script>
|