@mschaeffler/node-red-logic 4.2.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.
@@ -1,5 +1,5 @@
1
1
  <script type="text/javascript">
2
- RED.nodes.registerType('kalman',{
2
+ RED.nodes.registerType('filter',{
3
3
  category: 'logic',
4
4
  color: "#C0DEED",
5
5
  defaults: {
@@ -7,15 +7,7 @@
7
7
  topic:{value:""},
8
8
  property:{value:"payload", required:true},
9
9
  propertyType:{value:"msg"},
10
- control:{value:0, required:true},
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()},
@@ -28,7 +20,7 @@
28
20
  outputs:1,
29
21
  icon:"font-awesome/fa-area-chart",
30
22
  label: function() {
31
- return this.name||this.topic||"kalman";
23
+ return this.name||this.topic||"filter";
32
24
  },
33
25
  labelStyle: function() {
34
26
  return this.name?"node_label_italic":"";
@@ -42,28 +34,11 @@
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>
65
40
 
66
- <script type="text/html" data-template-name="kalman">
41
+ <script type="text/html" data-template-name="filter">
67
42
  <div class="form-row">
68
43
  <label for="node-input-name"><i class="fa fa-tag"></i> name</label>
69
44
  <input type="text" id="node-input-name" placeholder="Name">
@@ -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-control"> control value</label>
82
- <input type="text" id="node-input-control">
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">
@@ -144,7 +88,7 @@
144
88
  </div>
145
89
  </script>
146
90
 
147
- <script type="text/html" data-help-name="kalman">
91
+ <script type="text/html" data-help-name="filter">
148
92
 
149
93
  <p>Filter the values of one or more data streams wuth an adaptive Kalman filter.</p>
150
94
  <p>If the msg property <code>invalid</code> is present in the message, all nodes ignore the message.</p>
package/math_filter.js ADDED
@@ -0,0 +1,151 @@
1
+ module.exports = function(RED) {
2
+ var tools = require('./tools.js');
3
+
4
+ function FilterNode(config) {
5
+ RED.nodes.createNode(this,config);
6
+ //this.config = config;
7
+ var node = this;
8
+ var context = this.context();
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 );
19
+ this.data = {};
20
+ this.last = {};
21
+ if( this.propertyType === "jsonata" )
22
+ {
23
+ try {
24
+ this.propertyPrepared = RED.util.prepareJSONataExpression( this.property, this );
25
+ }
26
+ catch (e) {
27
+ node.error(RED._("debug.invalid-exp", {error: this.property}));
28
+ return;
29
+ }
30
+ }
31
+ node.status( "" );
32
+
33
+ function setStatus(color,text)
34
+ {
35
+ if( node.showState )
36
+ {
37
+ node.status({ fill:color, shape:"dot", text:text });
38
+ }
39
+ }
40
+
41
+ node.on('input', function(msg,send,done) {
42
+ if( msg.invalid )
43
+ {
44
+ done();
45
+ }
46
+ else if( msg.reset || msg.topic==="init" )
47
+ {
48
+ node.data = {};
49
+ node.last = {};
50
+ node.status( "" );
51
+ done();
52
+ }
53
+ else
54
+ {
55
+ function getPayload(callback)
56
+ {
57
+ if( node.propertyPrepared )
58
+ {
59
+ RED.util.evaluateJSONataExpression( node.propertyPrepared, msg, function(err, value)
60
+ {
61
+ if( err )
62
+ {
63
+ done( err.message );
64
+ }
65
+ else
66
+ {
67
+ callback( value );
68
+ }
69
+ } );
70
+ }
71
+ else
72
+ {
73
+ callback( RED.util.getMessageProperty( msg, node.property ) );
74
+ }
75
+ }
76
+ getPayload( function(value)
77
+ {
78
+ if( node.topic )
79
+ {
80
+ msg.topic = node.topic;
81
+ }
82
+ const payload = Number( value );
83
+ if( ! isNaN( payload ) )
84
+ {
85
+ node.data[msg.topic] ??= {};
86
+ const data = node.data[msg.topic];
87
+ const now = Date.now();
88
+
89
+ function lowPassFilter(measurement)
90
+ {
91
+ if( data.last == undefined )
92
+ {
93
+ data.last = measurement;
94
+ }
95
+ else
96
+ {
97
+ data.last += node.alpha * ( measurement - data.last );
98
+ }
99
+ return data.last;
100
+ }
101
+
102
+ function sendValue(value)
103
+ {
104
+ msg.payload = value;
105
+ node.last[msg.topic] = {value:value,time:now};
106
+ setStatus( "green", tools.formatNumber(value) );
107
+ send( msg );
108
+ }
109
+
110
+ if( node.zeroIsZero && payload === 0 )
111
+ {
112
+ data.estimate = 0;
113
+ sendValue( 0 );
114
+ }
115
+ else
116
+ {
117
+ let value = lowPassFilter( payload );
118
+ const help = node.last[msg.topic];
119
+ if( help === undefined ||
120
+ ( help.time + node.filterTime < now && tools.distance( help.value, value ) >= node.filterValue ) ||
121
+ ( node.filterLongTime > 0 && help.time + node.filterLongTime < now ) )
122
+ {
123
+ if( node.round )
124
+ {
125
+ value = Math.round( value * node.round ) / node.round;
126
+ }
127
+ sendValue( value );
128
+ }
129
+ else
130
+ {
131
+ setStatus( "gray", tools.formatNumber(value) );
132
+ }
133
+ }
134
+ context.set( "data", node.data, node.contextStore );
135
+ }
136
+ else
137
+ {
138
+ if( value !== undefined )
139
+ {
140
+ setStatus( "red", "payload is NaN" );
141
+ node.warn( `payload is NaN (${msg.topic}=${value})` );
142
+ }
143
+ }
144
+ done();
145
+ } );
146
+ }
147
+ });
148
+ }
149
+
150
+ RED.nodes.registerType("filter",FilterNode);
151
+ }
package/math_mean.html CHANGED
@@ -99,7 +99,7 @@
99
99
  </div>
100
100
  <div class="form-row">
101
101
  <label for="node-input-decimals"><i class="fa fa-sort-numeric-asc"></i> Nachkommastellen</label>
102
- <input type="text" id="node-input-decimals">
102
+ <input type="number" id="node-input-decimals">
103
103
  </div>
104
104
  <div class="form-row">
105
105
  <label for="node-input-zeroIsZero"><i class="fa fa-exclamation-circle"></i> Null ist Null</label>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mschaeffler/node-red-logic",
3
- "version": "4.2.0",
3
+ "version": "4.3.1",
4
4
  "description": "nodes for simple operations.",
5
5
  "license": "LGPL-2.1",
6
6
  "homepage": "https://github.com/m-schaeffler/node-red-my-nodes/tree/main/node-red-logic",
@@ -33,7 +33,7 @@
33
33
  "threePoint": "math_3point.js",
34
34
  "statistics": "math_statistics.js",
35
35
  "mean": "math_mean.js",
36
- "kalman": "math_kalman.js"
36
+ "filter": "math_filter.js"
37
37
  }
38
38
  },
39
39
  "author": {
@@ -41,6 +41,6 @@
41
41
  "url": "https://github.com/m-schaeffler"
42
42
  },
43
43
  "scripts": {
44
- "test": "mocha \"test/math_kalman_spec.js\""
44
+ "test": "mocha \"test/math_filter_spec.js\""
45
45
  }
46
46
  }
package/math_kalman.js DELETED
@@ -1,223 +0,0 @@
1
- module.exports = function(RED) {
2
- var tools = require('./tools.js');
3
-
4
- function KalmanNode(config) {
5
- RED.nodes.createNode(this,config);
6
- //this.config = config;
7
- var node = this;
8
- var context = this.context();
9
- this.topic = config.topic ?? "";
10
- this.property = config.property ?? "payload";
11
- this.propertyType = config.propertyType ?? "msg";
12
- this.control = config.control ?? 0;
13
- this.controlType = config.controlType ?? "num";
14
- this.processNoise = Number( config.processNoise ?? 1 );
15
- this.measurementNoise = Number( config.measurementNoise ?? 1 );
16
- this.stateVector = Number( config.stateVector ?? 1 );
17
- this.controlVector = Number( config.controlVector ?? 0 );
18
- this.measurementVector = Number( config.measurementVector ?? 1 );
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 );
27
- this.data = {};
28
- this.last = {};
29
- this.adaptation = {};
30
- if( this.propertyType === "jsonata" )
31
- {
32
- try {
33
- this.propertyPrepared = RED.util.prepareJSONataExpression( this.property, this );
34
- }
35
- catch (e) {
36
- node.error(RED._("debug.invalid-exp", {error: this.property}));
37
- return;
38
- }
39
- }
40
- 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
-
63
- function setStatus(color,text)
64
- {
65
- if( node.showState )
66
- {
67
- node.status({ fill:color, shape:"dot", text:text });
68
- }
69
- }
70
-
71
- node.on('input', function(msg,send,done) {
72
- if( msg.invalid )
73
- {
74
- done();
75
- }
76
- else if( msg.reset || msg.topic==="init" )
77
- {
78
- node.data = {};
79
- node.last = {};
80
- node.adaptation = {};
81
- context.set( "data", node.data, node.contextStore );
82
- node.status( "" );
83
- done();
84
- }
85
- else
86
- {
87
- function getPayload(callback)
88
- {
89
- if( node.propertyPrepared )
90
- {
91
- RED.util.evaluateJSONataExpression( node.propertyPrepared, msg, function(err, value)
92
- {
93
- if( err )
94
- {
95
- done( err.message );
96
- }
97
- else
98
- {
99
- callback( value );
100
- }
101
- } );
102
- }
103
- else
104
- {
105
- callback( RED.util.getMessageProperty( msg, node.property ) );
106
- }
107
- }
108
- getPayload( function(value)
109
- {
110
- if( node.topic )
111
- {
112
- msg.topic = node.topic;
113
- }
114
- const payload = Number( value );
115
- if( ! isNaN( payload ) )
116
- {
117
- const control = RED.util.evaluateNodeProperty( node.control, node.controlType, node, msg );
118
- if( ! isNaN( control ) )
119
- {
120
- node.data[msg.topic] ??= {};
121
- const data = node.data[msg.topic];
122
- const now = Date.now();
123
-
124
- function kalmanFilter(measurement,control)
125
- {
126
- if( data.estimate === undefined )
127
- {
128
- data.processNoise = node.processNoise;
129
- data.estimate = measurement / node.measurementVector;
130
- data.covariance = node.measurementNoise / node.measurementVector**2;
131
- }
132
- else
133
- {
134
- // measurement time
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;
163
- }
164
- data.time = now;
165
- return data.estimate;
166
- }
167
-
168
- function sendValue(value)
169
- {
170
- msg.payload = value;
171
- node.last[msg.topic] = {value:value,time:now};
172
- setStatus( "green", tools.formatNumber(value) );
173
- send( msg );
174
- }
175
-
176
- if( node.zeroIsZero && payload === 0 )
177
- {
178
- data.estimate = 0;
179
- sendValue( 0 );
180
- }
181
- else
182
- {
183
- let value = kalmanFilter( payload, control );
184
- const help = node.last[msg.topic];
185
- if( help === undefined ||
186
- ( help.time + node.filterTime < now && tools.distance( help.value, value ) >= node.filterValue ) ||
187
- ( node.filterLongTime > 0 && help.time + node.filterLongTime < now ) )
188
- {
189
- if( node.round )
190
- {
191
- value = Math.round( value * node.round ) / node.round;
192
- }
193
- sendValue( value );
194
- }
195
- else
196
- {
197
- setStatus( "gray", tools.formatNumber(value) );
198
- }
199
- }
200
- 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
- }
208
- else
209
- {
210
- if( value !== undefined )
211
- {
212
- setStatus( "red", "payload is NaN" );
213
- node.warn( `payload is NaN (${msg.topic}=${value})` );
214
- }
215
- }
216
- done();
217
- } );
218
- }
219
- });
220
- }
221
-
222
- RED.nodes.registerType("kalman",KalmanNode);
223
- }