@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.36
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/nodes/accumulate-block.html +18 -8
- package/nodes/accumulate-block.js +39 -44
- package/nodes/add-block.html +1 -1
- package/nodes/add-block.js +18 -11
- package/nodes/alarm-collector.html +260 -0
- package/nodes/alarm-collector.js +292 -0
- package/nodes/alarm-config.html +129 -0
- package/nodes/alarm-config.js +126 -0
- package/nodes/alarm-service.html +96 -0
- package/nodes/alarm-service.js +142 -0
- package/nodes/analog-switch-block.js +25 -36
- package/nodes/and-block.js +44 -15
- package/nodes/average-block.js +46 -41
- package/nodes/boolean-switch-block.js +10 -28
- package/nodes/boolean-to-number-block.html +18 -5
- package/nodes/boolean-to-number-block.js +24 -16
- package/nodes/cache-block.js +24 -37
- package/nodes/call-status-block.html +91 -32
- package/nodes/call-status-block.js +398 -115
- package/nodes/changeover-block.html +5 -0
- package/nodes/changeover-block.js +167 -162
- package/nodes/comment-block.html +1 -1
- package/nodes/comment-block.js +14 -9
- package/nodes/compare-block.html +14 -4
- package/nodes/compare-block.js +23 -18
- package/nodes/contextual-label-block.html +5 -0
- package/nodes/contextual-label-block.js +6 -16
- package/nodes/convert-block.html +25 -39
- package/nodes/convert-block.js +31 -16
- package/nodes/count-block.html +11 -5
- package/nodes/count-block.js +34 -32
- package/nodes/delay-block.js +58 -53
- package/nodes/divide-block.js +43 -45
- package/nodes/edge-block.html +17 -10
- package/nodes/edge-block.js +43 -41
- package/nodes/enum-switch-block.js +6 -6
- package/nodes/frequency-block.html +6 -1
- package/nodes/frequency-block.js +64 -74
- package/nodes/global-getter.html +51 -15
- package/nodes/global-getter.js +43 -13
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +40 -12
- package/nodes/history-buffer.html +96 -0
- package/nodes/history-buffer.js +461 -0
- package/nodes/history-collector.html +29 -1
- package/nodes/history-collector.js +37 -16
- package/nodes/history-config.html +13 -1
- package/nodes/history-service.html +84 -0
- package/nodes/history-service.js +52 -0
- package/nodes/hysteresis-block.html +5 -0
- package/nodes/hysteresis-block.js +13 -16
- package/nodes/interpolate-block.html +20 -2
- package/nodes/interpolate-block.js +39 -50
- package/nodes/join.html +78 -0
- package/nodes/join.js +78 -0
- package/nodes/latch-block.js +12 -14
- package/nodes/load-sequence-block.js +102 -110
- package/nodes/max-block.js +26 -26
- package/nodes/memory-block.js +57 -58
- package/nodes/min-block.js +26 -25
- package/nodes/minmax-block.js +35 -34
- package/nodes/modulo-block.js +45 -43
- package/nodes/multiply-block.js +43 -41
- package/nodes/negate-block.html +17 -7
- package/nodes/negate-block.js +25 -19
- package/nodes/network-point-read.html +128 -0
- package/nodes/network-point-read.js +230 -0
- package/nodes/{network-register.html → network-point-register.html} +94 -7
- package/nodes/{network-register.js → network-point-register.js} +18 -4
- package/nodes/network-point-write.html +149 -0
- package/nodes/network-point-write.js +222 -0
- package/nodes/network-service-bridge.html +131 -0
- package/nodes/network-service-bridge.js +376 -0
- package/nodes/network-service-read.html +81 -0
- package/nodes/{network-read.js → network-service-read.js} +4 -3
- package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
- package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
- package/nodes/network-service-write.html +89 -0
- package/nodes/{network-write.js → network-service-write.js} +3 -3
- package/nodes/nullify-block.js +13 -15
- package/nodes/on-change-block.html +17 -9
- package/nodes/on-change-block.js +49 -46
- package/nodes/oneshot-block.html +13 -10
- package/nodes/oneshot-block.js +57 -75
- package/nodes/or-block.js +44 -15
- package/nodes/pid-block.html +54 -4
- package/nodes/pid-block.js +459 -248
- package/nodes/priority-block.js +24 -35
- package/nodes/rate-limit-block.js +70 -72
- package/nodes/rate-of-change-block.html +33 -14
- package/nodes/rate-of-change-block.js +74 -62
- package/nodes/round-block.html +14 -9
- package/nodes/round-block.js +32 -25
- package/nodes/saw-tooth-wave-block.js +49 -76
- package/nodes/scale-range-block.html +12 -6
- package/nodes/scale-range-block.js +46 -39
- package/nodes/sine-wave-block.js +49 -57
- package/nodes/string-builder-block.js +6 -6
- package/nodes/subtract-block.js +38 -34
- package/nodes/thermistor-block.js +44 -44
- package/nodes/tick-tock-block.js +32 -32
- package/nodes/time-sequence-block.js +30 -42
- package/nodes/triangle-wave-block.js +49 -69
- package/nodes/tstat-block.js +34 -44
- package/nodes/units-block.html +90 -69
- package/nodes/units-block.js +22 -30
- package/nodes/utils.js +206 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- package/nodes/network-write.html +0 -65
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require("./utils")(RED);
|
|
3
|
+
|
|
2
4
|
function SawToothWaveBlockNode(config) {
|
|
3
5
|
RED.nodes.createNode(this, config);
|
|
4
6
|
const node = this;
|
|
5
7
|
|
|
6
8
|
// Initialize runtime state
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
9
|
+
// Initialize state
|
|
10
|
+
node.name = config.name;
|
|
11
|
+
node.lowerLimit = parseFloat(config.lowerLimit);
|
|
12
|
+
node.upperLimit = parseFloat(config.upperLimit);
|
|
13
|
+
node.period = (parseFloat(config.period) || 10) * (config.periodUnits === "minutes" ? 60000 : config.periodUnits === "seconds" ? 1000 : 1);
|
|
14
|
+
node.periodUnits = config.periodUnits || "seconds";
|
|
15
|
+
node.lastExecution = Date.now();
|
|
16
|
+
node.phase = 0;
|
|
16
17
|
|
|
17
18
|
// Validate initial config
|
|
18
|
-
if (isNaN(node.
|
|
19
|
-
node.
|
|
20
|
-
node.
|
|
21
|
-
|
|
22
|
-
} else if (node.
|
|
23
|
-
node.
|
|
24
|
-
|
|
19
|
+
if (isNaN(node.lowerLimit) || isNaN(node.upperLimit) || !isFinite(node.lowerLimit) || !isFinite(node.upperLimit)) {
|
|
20
|
+
node.lowerLimit = 0;
|
|
21
|
+
node.upperLimit = 100;
|
|
22
|
+
utils.setStatusError(node, "invalid limits");
|
|
23
|
+
} else if (node.lowerLimit > node.upperLimit) {
|
|
24
|
+
node.upperLimit = node.lowerLimit;
|
|
25
|
+
utils.setStatusError(node, "invalid limits");
|
|
25
26
|
}
|
|
26
|
-
if (isNaN(node.
|
|
27
|
-
node.
|
|
28
|
-
node.
|
|
29
|
-
|
|
27
|
+
if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
|
|
28
|
+
node.period = 10000;
|
|
29
|
+
node.periodUnits = "milliseconds";
|
|
30
|
+
utils.setStatusError(node, "invalid period");
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
node.on("input", function(msg, send, done) {
|
|
@@ -34,7 +35,7 @@ module.exports = function(RED) {
|
|
|
34
35
|
|
|
35
36
|
// Guard against invalid message
|
|
36
37
|
if (!msg) {
|
|
37
|
-
|
|
38
|
+
utils.setStatusError(node, "invalid message");
|
|
38
39
|
if (done) done();
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
@@ -42,74 +43,54 @@ module.exports = function(RED) {
|
|
|
42
43
|
// Handle context updates
|
|
43
44
|
if (msg.hasOwnProperty("context")) {
|
|
44
45
|
if (!msg.hasOwnProperty("payload")) {
|
|
45
|
-
|
|
46
|
+
utils.setStatusError(node, `missing payload for ${msg.context}`);
|
|
46
47
|
if (done) done();
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
if (typeof msg.context !== "string") {
|
|
50
|
-
|
|
51
|
+
utils.setStatusError(node, "invalid context");
|
|
51
52
|
if (done) done();
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
let value = parseFloat(msg.payload);
|
|
55
56
|
if (isNaN(value) || !isFinite(value)) {
|
|
56
|
-
|
|
57
|
+
utils.setStatusError(node, `invalid ${msg.context}`);
|
|
57
58
|
if (done) done();
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
switch (msg.context) {
|
|
61
62
|
case "lowerLimit":
|
|
62
|
-
node.
|
|
63
|
-
if (node.
|
|
64
|
-
node.
|
|
65
|
-
node.
|
|
66
|
-
fill: "green",
|
|
67
|
-
shape: "dot",
|
|
68
|
-
text: `lower: ${node.runtime.lowerLimit.toFixed(2)}, upper adjusted to ${node.runtime.upperLimit.toFixed(2)}`
|
|
69
|
-
});
|
|
63
|
+
node.lowerLimit = value;
|
|
64
|
+
if (node.lowerLimit > node.upperLimit) {
|
|
65
|
+
node.upperLimit = node.lowerLimit;
|
|
66
|
+
utils.setStatusOK(node, `lower: ${node.lowerLimit.toFixed(2)}, upper adjusted to ${node.upperLimit.toFixed(2)}`);
|
|
70
67
|
} else {
|
|
71
|
-
node.
|
|
72
|
-
fill: "green",
|
|
73
|
-
shape: "dot",
|
|
74
|
-
text: `lower: ${node.runtime.lowerLimit.toFixed(2)}`
|
|
75
|
-
});
|
|
68
|
+
utils.setStatusOK(node, `lower: ${node.lowerLimit.toFixed(2)}`);
|
|
76
69
|
}
|
|
77
70
|
break;
|
|
78
71
|
case "upperLimit":
|
|
79
|
-
node.
|
|
80
|
-
if (node.
|
|
81
|
-
node.
|
|
82
|
-
node.
|
|
83
|
-
fill: "green",
|
|
84
|
-
shape: "dot",
|
|
85
|
-
text: `upper: ${node.runtime.upperLimit.toFixed(2)}, lower adjusted to ${node.runtime.lowerLimit.toFixed(2)}`
|
|
86
|
-
});
|
|
72
|
+
node.upperLimit = value;
|
|
73
|
+
if (node.upperLimit < node.lowerLimit) {
|
|
74
|
+
node.lowerLimit = node.upperLimit;
|
|
75
|
+
utils.setStatusOK(node, `upper: ${node.upperLimit.toFixed(2)}, lower adjusted to ${node.lowerLimit.toFixed(2)}`);
|
|
87
76
|
} else {
|
|
88
|
-
node.
|
|
89
|
-
fill: "green",
|
|
90
|
-
shape: "dot",
|
|
91
|
-
text: `upper: ${node.runtime.upperLimit.toFixed(2)}`
|
|
92
|
-
});
|
|
77
|
+
utils.setStatusOK(node, `upper: ${node.upperLimit.toFixed(2)}`);
|
|
93
78
|
}
|
|
94
79
|
break;
|
|
95
80
|
case "period":
|
|
96
81
|
const multiplier = msg.units === "minutes" ? 60000 : msg.units === "seconds" ? 1000 : 1;
|
|
97
82
|
value *= multiplier;
|
|
98
83
|
if (value <= 0) {
|
|
99
|
-
|
|
84
|
+
utils.setStatusError(node, "invalid period");
|
|
100
85
|
if (done) done();
|
|
101
86
|
return;
|
|
102
87
|
}
|
|
103
|
-
node.
|
|
104
|
-
node.
|
|
105
|
-
node.
|
|
106
|
-
fill: "green",
|
|
107
|
-
shape: "dot",
|
|
108
|
-
text: `period: ${node.runtime.period.toFixed(2)} ms`
|
|
109
|
-
});
|
|
88
|
+
node.period = value;
|
|
89
|
+
node.periodUnits = msg.units || "milliseconds";
|
|
90
|
+
utils.setStatusOK(node, `period: ${node.period.toFixed(2)} ms`);
|
|
110
91
|
break;
|
|
111
92
|
default:
|
|
112
|
-
|
|
93
|
+
utils.setStatusWarn(node, "unknown context");
|
|
113
94
|
if (done) done("Unknown context");
|
|
114
95
|
return;
|
|
115
96
|
}
|
|
@@ -119,34 +100,26 @@ module.exports = function(RED) {
|
|
|
119
100
|
|
|
120
101
|
// Calculate time difference
|
|
121
102
|
const now = Date.now();
|
|
122
|
-
const deltaTime = (now - node.
|
|
123
|
-
node.
|
|
103
|
+
const deltaTime = (now - node.lastExecution) / 1000; // Seconds
|
|
104
|
+
node.lastExecution = now;
|
|
124
105
|
|
|
125
106
|
// Return lowerLimit if period is invalid
|
|
126
|
-
if (node.
|
|
127
|
-
node.
|
|
128
|
-
|
|
129
|
-
shape: "dot",
|
|
130
|
-
text: `out: ${node.runtime.lowerLimit.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}`
|
|
131
|
-
});
|
|
132
|
-
send({ payload: node.runtime.lowerLimit });
|
|
107
|
+
if (node.period <= 0) {
|
|
108
|
+
utils.setStatusOK(node, `out: ${node.lowerLimit.toFixed(2)}, phase: ${node.phase.toFixed(2)}`);
|
|
109
|
+
send({ payload: node.lowerLimit });
|
|
133
110
|
if (done) done();
|
|
134
111
|
return;
|
|
135
112
|
}
|
|
136
113
|
|
|
137
114
|
// Update phase
|
|
138
|
-
node.
|
|
115
|
+
node.phase = (node.phase + deltaTime / (node.period / 1000)) % 1;
|
|
139
116
|
|
|
140
117
|
// Sawtooth wave calculation
|
|
141
|
-
const amplitude = node.
|
|
142
|
-
const value = node.
|
|
118
|
+
const amplitude = node.upperLimit - node.lowerLimit;
|
|
119
|
+
const value = node.lowerLimit + amplitude * node.phase;
|
|
143
120
|
|
|
144
121
|
// Output new message
|
|
145
|
-
node.
|
|
146
|
-
fill: "blue",
|
|
147
|
-
shape: "dot",
|
|
148
|
-
text: `out: ${value.toFixed(2)}, phase: ${node.runtime.phase.toFixed(2)}`
|
|
149
|
-
});
|
|
122
|
+
utils.setStatusOK(node, `out: ${value.toFixed(2)}, phase: ${node.phase.toFixed(2)}`);
|
|
150
123
|
send({ payload: value });
|
|
151
124
|
|
|
152
125
|
if (done) done();
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
5
5
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
6
6
|
</div>
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
9
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
10
|
+
</div>
|
|
7
11
|
<div class="form-row">
|
|
8
12
|
<label for="node-input-inMin" title="Minimum input value (number)"><i class="fa fa-arrow-down"></i> Input Min</label>
|
|
9
13
|
<input type="number" id="node-input-inMin" placeholder="0.0" step="any">
|
|
@@ -33,6 +37,7 @@
|
|
|
33
37
|
color: "#301934",
|
|
34
38
|
defaults: {
|
|
35
39
|
name: { value: "" },
|
|
40
|
+
inputProperty: { value: "payload" },
|
|
36
41
|
inMin: { value: 0.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
|
|
37
42
|
inMax: { value: 100.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
|
|
38
43
|
outMin: { value: 0.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && isFinite(parseFloat(v)); } },
|
|
@@ -53,16 +58,18 @@
|
|
|
53
58
|
|
|
54
59
|
<!-- Help Section -->
|
|
55
60
|
<script type="text/markdown" data-help-name="scale-range-block">
|
|
56
|
-
Scales
|
|
61
|
+
Scales numeric input from one range to another.
|
|
57
62
|
|
|
58
63
|
### Inputs
|
|
64
|
+
: input-property (number) : Numeric value to scale, read from the configured Input Property.
|
|
59
65
|
: 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
66
|
|
|
62
67
|
### Outputs
|
|
63
68
|
: payload (number) : Scaled output value.
|
|
64
69
|
|
|
65
70
|
### Properties
|
|
71
|
+
: name (string) : Display name in editor.
|
|
72
|
+
: inputProperty (string) : Message property to read input from (default: `payload`). Supports nested properties (e.g., `data.value`).
|
|
66
73
|
: inMin (number) : Minimum input value.
|
|
67
74
|
: inMax (number) : Maximum input value (> inMin).
|
|
68
75
|
: outMin (number) : Minimum output value.
|
|
@@ -70,12 +77,11 @@ Scales a numeric input from one range to another and passes the original message
|
|
|
70
77
|
: clamp (boolean) : Clamp output to output range.
|
|
71
78
|
|
|
72
79
|
### Details
|
|
73
|
-
Scales `msg.payload`
|
|
74
|
-
clamping to `[outMin, outMax]` if `clamp` is `true`.
|
|
80
|
+
Scales numeric input (read from the configured **Input Property**, default: `msg.payload`) from an input range (`inMin` to `inMax`) to an output range (`outMin` to `outMax`) using linear interpolation, with optional clamping to `[outMin, outMax]` if `clamp` is enabled.
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
Formula: `outValue = outMin + (inValue - inMin) * (outMax - outMin) / (inMax - inMin)`
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
Output is always written to `msg.payload`.
|
|
79
85
|
|
|
80
86
|
### Status
|
|
81
87
|
- Green (dot): Configuration update
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
2
3
|
function ScaleRangeBlockNode(config) {
|
|
3
4
|
RED.nodes.createNode(this, config);
|
|
4
5
|
const node = this;
|
|
5
6
|
|
|
6
7
|
// Initialize runtime state
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
// Initialize state
|
|
9
|
+
node.name = config.name || "";
|
|
10
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
11
|
+
node.inMin = parseFloat(config.inMin);
|
|
12
|
+
node.inMax = parseFloat(config.inMax);
|
|
13
|
+
node.outMin = parseFloat(config.outMin);
|
|
14
|
+
node.outMax = parseFloat(config.outMax);
|
|
15
|
+
node.clamp = config.clamp;
|
|
16
|
+
node.lastInput = parseFloat(config.inMin);
|
|
16
17
|
|
|
17
18
|
// Validate initial config
|
|
18
|
-
if (isNaN(node.
|
|
19
|
-
node.
|
|
20
|
-
node.
|
|
21
|
-
node.
|
|
22
|
-
|
|
19
|
+
if (isNaN(node.inMin) || isNaN(node.inMax) || !isFinite(node.inMin) || !isFinite(node.inMax) || node.inMin >= node.inMax) {
|
|
20
|
+
node.inMin = 0.0;
|
|
21
|
+
node.inMax = 100.0;
|
|
22
|
+
node.lastInput = 0.0;
|
|
23
|
+
utils.setStatusError(node, "invalid input range");
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
node.on("input", function(msg, send, done) {
|
|
@@ -27,7 +28,7 @@ module.exports = function(RED) {
|
|
|
27
28
|
|
|
28
29
|
// Guard against invalid message
|
|
29
30
|
if (!msg) {
|
|
30
|
-
|
|
31
|
+
utils.setStatusError(node, "invalid message");
|
|
31
32
|
if (done) done();
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
@@ -35,7 +36,7 @@ module.exports = function(RED) {
|
|
|
35
36
|
// Handle context updates
|
|
36
37
|
if (msg.hasOwnProperty("context")) {
|
|
37
38
|
if (!msg.hasOwnProperty("payload")) {
|
|
38
|
-
|
|
39
|
+
utils.setStatusError(node, `missing payload for ${msg.context}`);
|
|
39
40
|
if (done) done();
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
@@ -48,74 +49,80 @@ module.exports = function(RED) {
|
|
|
48
49
|
case "outMax":
|
|
49
50
|
const value = parseFloat(msg.payload);
|
|
50
51
|
if (isNaN(value) || !isFinite(value)) {
|
|
51
|
-
|
|
52
|
+
utils.setStatusError(node, `invalid ${msg.context}`);
|
|
52
53
|
if (done) done();
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
|
-
node
|
|
56
|
-
if (node.
|
|
57
|
-
|
|
56
|
+
node[msg.context] = value;
|
|
57
|
+
if (node.inMax <= node.inMin) {
|
|
58
|
+
utils.setStatusError(node, "invalid input range");
|
|
58
59
|
if (done) done();
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
|
-
if (node.
|
|
62
|
-
|
|
62
|
+
if (node.outMax <= node.outMin) {
|
|
63
|
+
utils.setStatusError(node, "invalid output range");
|
|
63
64
|
if (done) done();
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
utils.setStatusOK(node, `${msg.context}: ${value.toFixed(2)}`);
|
|
67
68
|
shouldOutput = true;
|
|
68
69
|
break;
|
|
69
70
|
case "clamp":
|
|
70
71
|
if (typeof msg.payload !== "boolean") {
|
|
71
|
-
|
|
72
|
+
utils.setStatusError(node, "invalid clamp");
|
|
72
73
|
if (done) done();
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
|
-
node.
|
|
76
|
-
|
|
76
|
+
node.clamp = msg.payload;
|
|
77
|
+
utils.setStatusOK(node, `clamp: ${node.clamp}`);
|
|
77
78
|
shouldOutput = true;
|
|
78
79
|
break;
|
|
79
80
|
default:
|
|
80
|
-
|
|
81
|
+
utils.setStatusWarn(node, "unknown context");
|
|
81
82
|
if (done) done("Unknown context");
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// Recalculate with last input after config update
|
|
86
87
|
if (shouldOutput) {
|
|
87
|
-
const out = calculate(node.
|
|
88
|
+
const out = calculate(node.lastInput, node.inMin, node.inMax, node.outMin, node.outMax, node.clamp);
|
|
88
89
|
msg.payload = out;
|
|
89
|
-
|
|
90
|
+
utils.setStatusOK(node, `in: ${node.lastInput.toFixed(2)}, out: ${out.toFixed(2)}`);
|
|
90
91
|
send(msg);
|
|
91
92
|
}
|
|
92
93
|
if (done) done();
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Get input from configured property
|
|
98
|
+
let input;
|
|
99
|
+
try {
|
|
100
|
+
input = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
input = undefined;
|
|
103
|
+
}
|
|
104
|
+
if (input === undefined) {
|
|
105
|
+
utils.setStatusError(node, "missing or invalid input property");
|
|
99
106
|
if (done) done();
|
|
100
107
|
return;
|
|
101
108
|
}
|
|
102
|
-
const inputValue = parseFloat(
|
|
109
|
+
const inputValue = parseFloat(input);
|
|
103
110
|
if (isNaN(inputValue) || !isFinite(inputValue)) {
|
|
104
|
-
|
|
111
|
+
utils.setStatusError(node, "invalid input");
|
|
105
112
|
if (done) done();
|
|
106
113
|
return;
|
|
107
114
|
}
|
|
108
|
-
if (node.
|
|
109
|
-
|
|
115
|
+
if (node.inMax <= node.inMin) {
|
|
116
|
+
utils.setStatusError(node, "inMinx must be < inMax");
|
|
110
117
|
if (done) done();
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
// Scale input
|
|
115
|
-
node.
|
|
116
|
-
const out = calculate(inputValue, node.
|
|
122
|
+
node.lastInput = inputValue;
|
|
123
|
+
const out = calculate(inputValue, node.inMin, node.inMax, node.outMin, node.outMax, node.clamp);
|
|
117
124
|
msg.payload = out;
|
|
118
|
-
|
|
125
|
+
utils.setStatusOK(node, `in: ${inputValue.toFixed(2)}, out: ${out.toFixed(2)}`);
|
|
119
126
|
send(msg);
|
|
120
127
|
|
|
121
128
|
if (done) done();
|
package/nodes/sine-wave-block.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
2
3
|
function SineWaveBlockNode(config) {
|
|
3
4
|
RED.nodes.createNode(this, config);
|
|
4
5
|
const node = this;
|
|
5
6
|
|
|
6
7
|
// Initialize runtime state
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
8
|
+
// Initialize state
|
|
9
|
+
node.name = config.name;
|
|
10
|
+
node.lowerLimit = parseFloat(config.lowerLimit);
|
|
11
|
+
node.upperLimit = parseFloat(config.upperLimit);
|
|
12
|
+
node.period = (parseFloat(config.period)) * (config.periodUnits === "minutes" ? 60000 : config.periodUnits === "seconds" ? 1000 : 1);
|
|
13
|
+
node.periodUnits = config.periodUnits;
|
|
14
|
+
node.lastExecution = Date.now();
|
|
15
|
+
node.phase = 0;
|
|
16
16
|
|
|
17
17
|
// Validate initial config
|
|
18
|
-
if (isNaN(node.
|
|
19
|
-
node.
|
|
20
|
-
node.
|
|
21
|
-
|
|
22
|
-
} else if (node.
|
|
23
|
-
node.
|
|
24
|
-
|
|
18
|
+
if (isNaN(node.lowerLimit) || isNaN(node.upperLimit) || !isFinite(node.lowerLimit) || !isFinite(node.upperLimit)) {
|
|
19
|
+
node.lowerLimit = 0;
|
|
20
|
+
node.upperLimit = 100;
|
|
21
|
+
utils.setStatusError(node, "invalid limits");
|
|
22
|
+
} else if (node.lowerLimit > node.upperLimit) {
|
|
23
|
+
node.upperLimit = node.lowerLimit;
|
|
24
|
+
utils.setStatusError(node, "invalid limits");
|
|
25
25
|
}
|
|
26
|
-
if (isNaN(node.
|
|
27
|
-
node.
|
|
28
|
-
node.
|
|
29
|
-
|
|
26
|
+
if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
|
|
27
|
+
node.period = 10000;
|
|
28
|
+
node.periodUnits = "milliseconds";
|
|
29
|
+
utils.setStatusError(node, "invalid period");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
node.on("input", function(msg, send, done) {
|
|
@@ -34,7 +34,7 @@ module.exports = function(RED) {
|
|
|
34
34
|
|
|
35
35
|
// Guard against invalid message
|
|
36
36
|
if (!msg) {
|
|
37
|
-
|
|
37
|
+
utils.setStatusError(node, "invalid message");
|
|
38
38
|
if (done) done();
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
@@ -42,62 +42,54 @@ module.exports = function(RED) {
|
|
|
42
42
|
// Handle context updates
|
|
43
43
|
if (msg.hasOwnProperty("context")) {
|
|
44
44
|
if (!msg.hasOwnProperty("payload")) {
|
|
45
|
-
|
|
45
|
+
utils.setStatusError(node, `missing payload for ${msg.context}`);
|
|
46
46
|
if (done) done();
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
if (typeof msg.context !== "string") {
|
|
50
|
-
|
|
50
|
+
utils.setStatusError(node, "invalid context");
|
|
51
51
|
if (done) done();
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
let value = parseFloat(msg.payload);
|
|
55
55
|
if (isNaN(value) || !isFinite(value)) {
|
|
56
|
-
|
|
56
|
+
utils.setStatusError(node, `invalid ${msg.context}`);
|
|
57
57
|
if (done) done();
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
switch (msg.context) {
|
|
61
61
|
case "lowerLimit":
|
|
62
|
-
node.
|
|
63
|
-
if (node.
|
|
64
|
-
node.
|
|
65
|
-
node.
|
|
66
|
-
fill: "green",
|
|
67
|
-
shape: "dot",
|
|
68
|
-
text: `lower: ${node.runtime.lowerLimit.toFixed(2)}, upper adjusted to ${node.runtime.upperLimit.toFixed(2)}`
|
|
69
|
-
});
|
|
62
|
+
node.lowerLimit = value;
|
|
63
|
+
if (node.lowerLimit > node.upperLimit) {
|
|
64
|
+
node.upperLimit = node.lowerLimit;
|
|
65
|
+
utils.setStatusOK(node, `lower: ${node.lowerLimit.toFixed(2)}, upper adjusted to ${node.upperLimit.toFixed(2)}`);
|
|
70
66
|
} else {
|
|
71
|
-
|
|
67
|
+
utils.setStatusOK(node, `lower: ${node.lowerLimit.toFixed(2)}`);
|
|
72
68
|
}
|
|
73
69
|
break;
|
|
74
70
|
case "upperLimit":
|
|
75
|
-
node.
|
|
76
|
-
if (node.
|
|
77
|
-
node.
|
|
78
|
-
node.
|
|
79
|
-
fill: "green",
|
|
80
|
-
shape: "dot",
|
|
81
|
-
text: `upper: ${node.runtime.upperLimit.toFixed(2)}, lower adjusted to ${node.runtime.lowerLimit.toFixed(2)}`
|
|
82
|
-
});
|
|
71
|
+
node.upperLimit = value;
|
|
72
|
+
if (node.upperLimit < node.lowerLimit) {
|
|
73
|
+
node.lowerLimit = node.upperLimit;
|
|
74
|
+
utils.setStatusOK(node, `upper: ${node.upperLimit.toFixed(2)}, lower adjusted to ${node.lowerLimit.toFixed(2)}`);
|
|
83
75
|
} else {
|
|
84
|
-
|
|
76
|
+
utils.setStatusOK(node, `upper: ${node.upperLimit.toFixed(2)}`);
|
|
85
77
|
}
|
|
86
78
|
break;
|
|
87
79
|
case "period":
|
|
88
80
|
const multiplier = msg.units === "minutes" ? 60000 : msg.units === "seconds" ? 1000 : 1;
|
|
89
81
|
value *= multiplier;
|
|
90
82
|
if (value <= 0) {
|
|
91
|
-
|
|
83
|
+
utils.setStatusError(node, "invalid period");
|
|
92
84
|
if (done) done();
|
|
93
85
|
return;
|
|
94
86
|
}
|
|
95
|
-
node.
|
|
96
|
-
node.
|
|
97
|
-
|
|
87
|
+
node.period = value;
|
|
88
|
+
node.periodUnits = msg.units || "milliseconds";
|
|
89
|
+
utils.setStatusOK(node, `period: ${node.period.toFixed(2)} ms`);
|
|
98
90
|
break;
|
|
99
91
|
default:
|
|
100
|
-
|
|
92
|
+
utils.setStatusWarn(node, "unknown context");
|
|
101
93
|
if (done) done("Unknown context");
|
|
102
94
|
return;
|
|
103
95
|
}
|
|
@@ -107,27 +99,27 @@ module.exports = function(RED) {
|
|
|
107
99
|
|
|
108
100
|
// Calculate time difference
|
|
109
101
|
const now = Date.now();
|
|
110
|
-
const deltaTime = (now - node.
|
|
111
|
-
node.
|
|
102
|
+
const deltaTime = (now - node.lastExecution) / 1000; // Seconds
|
|
103
|
+
node.lastExecution = now;
|
|
112
104
|
|
|
113
105
|
// Return lowerLimit if period is invalid
|
|
114
|
-
if (node.
|
|
115
|
-
|
|
116
|
-
send({ payload: node.
|
|
106
|
+
if (node.period <= 0) {
|
|
107
|
+
utils.setStatusOK(node, `out: ${node.lowerLimit.toFixed(2)}, phase: ${node.phase.toFixed(2)}`);
|
|
108
|
+
send({ payload: node.lowerLimit });
|
|
117
109
|
if (done) done();
|
|
118
110
|
return;
|
|
119
111
|
}
|
|
120
112
|
|
|
121
113
|
// Update phase
|
|
122
|
-
node.
|
|
114
|
+
node.phase = (node.phase + deltaTime / (node.period / 1000)) % 1;
|
|
123
115
|
|
|
124
116
|
// Sine wave calculation
|
|
125
|
-
const sineValue = Math.sin(2 * Math.PI * node.
|
|
126
|
-
const amplitude = (node.
|
|
127
|
-
const value = node.
|
|
117
|
+
const sineValue = Math.sin(2 * Math.PI * node.phase);
|
|
118
|
+
const amplitude = (node.upperLimit - node.lowerLimit) / 2;
|
|
119
|
+
const value = node.lowerLimit + amplitude * (sineValue + 1);
|
|
128
120
|
|
|
129
121
|
// Output new message
|
|
130
|
-
|
|
122
|
+
utils.setStatusOK(node, `out: ${value.toFixed(2)}, phase: ${node.phase.toFixed(2)}`);
|
|
131
123
|
send({ payload: value });
|
|
132
124
|
|
|
133
125
|
if (done) done();
|