@mschaeffler/node-red-logic 4.3.0 → 4.3.1
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/math_filter.html +3 -59
- package/math_filter.js +18 -90
- package/package.json +1 -1
package/math_filter.html
CHANGED
|
@@ -7,15 +7,7 @@
|
|
|
7
7
|
topic:{value:""},
|
|
8
8
|
property:{value:"payload", required:true},
|
|
9
9
|
propertyType:{value:"msg"},
|
|
10
|
-
|
|
11
|
-
controlType:{value:"num"},
|
|
12
|
-
processNoise:{value:1, validate:RED.validators.number(), required:true },
|
|
13
|
-
measurementNoise:{value:1, validate:RED.validators.number(), required:true },
|
|
14
|
-
stateVector:{value:1, validate:RED.validators.number(), required:true },
|
|
15
|
-
controlVector:{value:0, validate:RED.validators.number(), required:true },
|
|
16
|
-
measurementVector:{value:1, validate:RED.validators.number(), required:true },
|
|
17
|
-
adaptionCount:{value:0, validate:RED.validators.number(), required:true },
|
|
18
|
-
contextStore:{value:"none",required:true},
|
|
10
|
+
alpha:{value:0.5, validate:RED.validators.number(), required:true },
|
|
19
11
|
filter:{value:0, validate:RED.validators.number()},
|
|
20
12
|
filterUnit:{value:"secs"},
|
|
21
13
|
filterVal:{value:0, validate:RED.validators.number()},
|
|
@@ -42,23 +34,6 @@
|
|
|
42
34
|
types:["msg","jsonata"],
|
|
43
35
|
typeField: "#node-input-propertyType"
|
|
44
36
|
});
|
|
45
|
-
$("#node-input-control").typedInput({
|
|
46
|
-
type:"num",
|
|
47
|
-
types:["msg","num"],
|
|
48
|
-
typeField: "#node-input-controlType"
|
|
49
|
-
});
|
|
50
|
-
// populate store names
|
|
51
|
-
$("#node-input-contextStore").empty();
|
|
52
|
-
const defaultStore = RED.settings.hasOwnProperty("context") ? RED.settings.context.default : '';
|
|
53
|
-
RED.settings.context.stores.forEach(function(item){
|
|
54
|
-
if( item ) {
|
|
55
|
-
const opt = $(`<option value="${item}">${item}${item===defaultStore?" (default)":""}</option>`);
|
|
56
|
-
$("#node-input-contextStore").append(opt);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
const currentStore = node.contextStore || '';
|
|
60
|
-
// select the current option
|
|
61
|
-
$("#node-input-contextStore").val(currentStore);
|
|
62
37
|
}
|
|
63
38
|
});
|
|
64
39
|
</script>
|
|
@@ -78,40 +53,9 @@
|
|
|
78
53
|
<input type="hidden" id="node-input-propertyType">
|
|
79
54
|
</div>
|
|
80
55
|
<div class="form-row">
|
|
81
|
-
<label for="node-input-
|
|
82
|
-
<input type="text" id="node-input-
|
|
83
|
-
<input type="hidden" id="node-input-controlType">
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div class="form-row">
|
|
87
|
-
<label for="node-input-processNoise"> process noise</label>
|
|
88
|
-
<input type="text" id="node-input-processNoise">
|
|
89
|
-
</div>
|
|
90
|
-
<div class="form-row">
|
|
91
|
-
<label for="node-input-measurementNoise"> measure noise</label>
|
|
92
|
-
<input type="text" id="node-input-measurementNoise">
|
|
93
|
-
</div>
|
|
94
|
-
<div class="form-row">
|
|
95
|
-
<label for="node-input-stateVector"> process factor</label>
|
|
96
|
-
<input type="text" id="node-input-stateVector">
|
|
56
|
+
<label for="node-input-alpha"> alpha</label>
|
|
57
|
+
<input type="text" id="node-input-alpha">
|
|
97
58
|
</div>
|
|
98
|
-
<div class="form-row">
|
|
99
|
-
<label for="node-input-controlVector"> control factor</label>
|
|
100
|
-
<input type="text" id="node-input-controlVector">
|
|
101
|
-
</div>
|
|
102
|
-
<div class="form-row">
|
|
103
|
-
<label for="node-input-measurementVector"> measur. factor</label>
|
|
104
|
-
<input type="text" id="node-input-measurementVector">
|
|
105
|
-
</div>
|
|
106
|
-
<div class="form-row">
|
|
107
|
-
<label for="node-input-adaptionCount"> adaption counts</label>
|
|
108
|
-
<input type="number" id="node-input-adaptionCount">
|
|
109
|
-
</div>
|
|
110
|
-
<div class="form-row">
|
|
111
|
-
<label for="node-input-contextStore"><i class="fa fa-database"></i> context store</label>
|
|
112
|
-
<select type="text" id="node-input-contextStore"></select>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
59
|
<div class="form-row">
|
|
116
60
|
<label for="node-input-filter"><i class="fa fa-filter"></i> filter time</label>
|
|
117
61
|
<input type="number" id="node-input-filter" style="width:70px;" placeholder="Time">
|
package/math_filter.js
CHANGED
|
@@ -6,27 +6,18 @@ module.exports = function(RED) {
|
|
|
6
6
|
//this.config = config;
|
|
7
7
|
var node = this;
|
|
8
8
|
var context = this.context();
|
|
9
|
-
this.topic
|
|
10
|
-
this.property
|
|
11
|
-
this.propertyType
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
this.adaptionCount = Number( config.adaptionCount ?? 0 );
|
|
20
|
-
this.contextStore = config.contextStore ?? "";
|
|
21
|
-
this.filterTime = Number( config.filter ?? 0 ) * tools.timeUnits( config.filterUnit );
|
|
22
|
-
this.filterValue = Number( config.filterVal ?? 0 );
|
|
23
|
-
this.filterLongTime = this.filterTime * Number( config.filterMul ?? 10 );
|
|
24
|
-
this.zeroIsZero = Boolean( config.zeroIsZero );
|
|
25
|
-
this.round = config.decimals ? Math.pow( 10, Number( config.decimals ) ) : null;
|
|
26
|
-
this.showState = Boolean( config.showState );
|
|
9
|
+
this.topic = config.topic ?? "";
|
|
10
|
+
this.property = config.property ?? "payload";
|
|
11
|
+
this.propertyType = config.propertyType ?? "msg";
|
|
12
|
+
this.alpha = Number( config.alpha ?? 0.5 );
|
|
13
|
+
this.filterTime = Number( config.filter ?? 0 ) * tools.timeUnits( config.filterUnit );
|
|
14
|
+
this.filterValue = Number( config.filterVal ?? 0 );
|
|
15
|
+
this.filterLongTime = this.filterTime * Number( config.filterMul ?? 10 );
|
|
16
|
+
this.zeroIsZero = Boolean( config.zeroIsZero );
|
|
17
|
+
this.round = config.decimals ? Math.pow( 10, Number( config.decimals ) ) : null;
|
|
18
|
+
this.showState = Boolean( config.showState );
|
|
27
19
|
this.data = {};
|
|
28
20
|
this.last = {};
|
|
29
|
-
this.adaptation = {};
|
|
30
21
|
if( this.propertyType === "jsonata" )
|
|
31
22
|
{
|
|
32
23
|
try {
|
|
@@ -38,27 +29,6 @@ module.exports = function(RED) {
|
|
|
38
29
|
}
|
|
39
30
|
}
|
|
40
31
|
node.status( "" );
|
|
41
|
-
context.get( "data", node.contextStore, function(err,value)
|
|
42
|
-
{
|
|
43
|
-
if( err )
|
|
44
|
-
{
|
|
45
|
-
node.error( err );
|
|
46
|
-
}
|
|
47
|
-
else
|
|
48
|
-
{
|
|
49
|
-
//console.log( "context read", value );
|
|
50
|
-
if( value !== undefined )
|
|
51
|
-
{
|
|
52
|
-
const now = Date.now();
|
|
53
|
-
for( const i in value )
|
|
54
|
-
{
|
|
55
|
-
value[i].time = now;
|
|
56
|
-
value[i].processNoise = node.processNoise;
|
|
57
|
-
}
|
|
58
|
-
node.data = value;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} );
|
|
62
32
|
|
|
63
33
|
function setStatus(color,text)
|
|
64
34
|
{
|
|
@@ -75,10 +45,8 @@ module.exports = function(RED) {
|
|
|
75
45
|
}
|
|
76
46
|
else if( msg.reset || msg.topic==="init" )
|
|
77
47
|
{
|
|
78
|
-
node.data
|
|
79
|
-
node.last
|
|
80
|
-
node.adaptation = {};
|
|
81
|
-
context.set( "data", node.data, node.contextStore );
|
|
48
|
+
node.data = {};
|
|
49
|
+
node.last = {};
|
|
82
50
|
node.status( "" );
|
|
83
51
|
done();
|
|
84
52
|
}
|
|
@@ -114,55 +82,21 @@ module.exports = function(RED) {
|
|
|
114
82
|
const payload = Number( value );
|
|
115
83
|
if( ! isNaN( payload ) )
|
|
116
84
|
{
|
|
117
|
-
const control = RED.util.evaluateNodeProperty( node.control, node.controlType, node, msg );
|
|
118
|
-
if( ! isNaN( control ) )
|
|
119
|
-
{
|
|
120
85
|
node.data[msg.topic] ??= {};
|
|
121
86
|
const data = node.data[msg.topic];
|
|
122
87
|
const now = Date.now();
|
|
123
88
|
|
|
124
|
-
function
|
|
89
|
+
function lowPassFilter(measurement)
|
|
125
90
|
{
|
|
126
|
-
if( data.
|
|
91
|
+
if( data.last == undefined )
|
|
127
92
|
{
|
|
128
|
-
data.
|
|
129
|
-
data.estimate = measurement / node.measurementVector;
|
|
130
|
-
data.covariance = node.measurementNoise / node.measurementVector**2;
|
|
93
|
+
data.last = measurement;
|
|
131
94
|
}
|
|
132
95
|
else
|
|
133
96
|
{
|
|
134
|
-
|
|
135
|
-
const deltaTime = ( now - data.time ) / 1000;
|
|
136
|
-
const stateVector = 1 + ( node.stateVector - 1 ) * deltaTime;
|
|
137
|
-
const controlVector = node.controlVector * deltaTime;
|
|
138
|
-
// prediction
|
|
139
|
-
const predX = stateVector * data.estimate + controlVector * control;
|
|
140
|
-
const predCov = stateVector**2 * data.covariance + node.processNoise;
|
|
141
|
-
// innovation
|
|
142
|
-
const innovation = measurement - node.measurementVector * predX;
|
|
143
|
-
// adaptation
|
|
144
|
-
if( node.adaptionCount )
|
|
145
|
-
{
|
|
146
|
-
const adaptation = node.adaptation[msg.topic] ?? [];
|
|
147
|
-
adaptation.push( innovation );
|
|
148
|
-
if( adaptation.length > node.adaptionCount )
|
|
149
|
-
{
|
|
150
|
-
adaptation.shift();
|
|
151
|
-
const mean = adaptation.reduce( (a,b) => a + b ) / adaptation.length;
|
|
152
|
-
const variance = adaptation.reduce( (a,b) => a + ( b - mean ) ** 2, 0 ) / ( adaptation.length - 1 );
|
|
153
|
-
data.r = Math.max( variance, 1e-6 );
|
|
154
|
-
data.r2 = ( 2 * variance + node.processNoise ) / 3;
|
|
155
|
-
}
|
|
156
|
-
node.adaptation[msg.topic] = adaptation;
|
|
157
|
-
}
|
|
158
|
-
// kalman gain
|
|
159
|
-
const gain = predCov * node.measurementVector / ( predCov * node.measurementVector**2 + node.measurementNoise );
|
|
160
|
-
// correction
|
|
161
|
-
data.estimate = predX + gain * innovation;
|
|
162
|
-
data.covariance = ( 1 - gain * node.measurementVector ) * predCov;
|
|
97
|
+
data.last += node.alpha * ( measurement - data.last );
|
|
163
98
|
}
|
|
164
|
-
data.
|
|
165
|
-
return data.estimate;
|
|
99
|
+
return data.last;
|
|
166
100
|
}
|
|
167
101
|
|
|
168
102
|
function sendValue(value)
|
|
@@ -180,7 +114,7 @@ module.exports = function(RED) {
|
|
|
180
114
|
}
|
|
181
115
|
else
|
|
182
116
|
{
|
|
183
|
-
let value =
|
|
117
|
+
let value = lowPassFilter( payload );
|
|
184
118
|
const help = node.last[msg.topic];
|
|
185
119
|
if( help === undefined ||
|
|
186
120
|
( help.time + node.filterTime < now && tools.distance( help.value, value ) >= node.filterValue ) ||
|
|
@@ -198,12 +132,6 @@ module.exports = function(RED) {
|
|
|
198
132
|
}
|
|
199
133
|
}
|
|
200
134
|
context.set( "data", node.data, node.contextStore );
|
|
201
|
-
}
|
|
202
|
-
else
|
|
203
|
-
{
|
|
204
|
-
setStatus( "red", "control is NaN" );
|
|
205
|
-
node.warn( `control is NaN (${msg.topic}=${control})` );
|
|
206
|
-
}
|
|
207
135
|
}
|
|
208
136
|
else
|
|
209
137
|
{
|
package/package.json
CHANGED