speedandcurrent 1.4.2 → 1.5.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/index.js CHANGED
@@ -1,55 +1,87 @@
1
- const { MessageHandler, Polar, PolarDamped, SI, Reporter } = require('signalkutilities');
2
- const { CorrectionTable } = require('./correctionTable.js');
3
- const { KalmanFilter } = require('kalman-filter');
4
-
5
-
1
+ const {
2
+ SmoothedHeading,
3
+ SmoothedAttitude,
4
+ SmoothedSpeedThroughWater,
5
+ SmoothedGroundSpeed,
6
+ SI,
7
+ MessageHandler,
8
+ Polar,
9
+ Reporter,
10
+ BaseSmoother,
11
+ MovingAverageSmoother,
12
+ KalmanSmoother,
13
+ PolarSmoother,
14
+ createSmoothedPolar,
15
+ createSmoothedHandler
16
+ } = require('signalkutilities');
6
17
 
18
+ const { CorrectionTable } = require('./correctionTable.js');
7
19
 
8
20
  module.exports = function (app) {
9
21
 
10
- let options = {};
11
22
  let isRunning = false;
12
- let corrTable = null;
13
- let lastSave = null;
14
- let heading = null;
15
- let attitude = null;
16
- let current = null;
17
- let currentStat = null;
18
- let boatSpeed = null;
19
- let boatSpeedStat = null;
23
+ let smoothedHeading = null;
24
+ let stability = null;
25
+ let smoothedAttitude = null;
26
+ let rawCurrent = null;
27
+ let smoothedCurrent = null;
28
+ let smoothedBoatSpeed = null;
20
29
  let correctedBoatSpeed = null;
21
30
  let boatSpeedRefGround = null;
22
- let groundSpeed = null;
23
- let groundSpeedStat = null;
31
+ let smoothedGroundSpeed = null;
24
32
  let speedCorrection = null;
25
33
  let residual = null;
26
34
  let reportFull = null;
27
35
  let reportVector = null;
28
36
  let table = null;
29
37
 
38
+ // add missing declarations
39
+ let rawHeading = null;
40
+ let rawAttitude = null;
41
+ let noCurrent = null;
42
+ let rawBoatSpeed = null;
43
+ let rawGroundSpeed = null;
44
+ let started = null;
45
+
30
46
  const plugin = {};
31
47
  plugin.id = "SpeedAndCurrent";
32
48
  plugin.name = "Speed and current";
33
- plugin.description = "An experimental plugin that uses sensor fusion to get boat speed, current and leeway.";
34
-
35
-
49
+ plugin.description = "A plugin that uses sensor fusion to get boat speed, current and leeway.";
36
50
 
37
51
  plugin.schema = {
38
52
  type: "object",
39
53
  properties: {
40
- mode: {
41
- type: "string",
42
- title: "Plugin mode",
43
- description: "The mode of the plugin.",
44
- enum: ["Start with new correction table, no current",
45
- "Start with new correction table, with current",
46
- "Fresh correction table, no current",
47
- "Fresh correction table, with current",
48
- "Mature correction table, no current",
49
- "Mature correction table, with current",
50
- "Locked correction table",
51
- "Manual configuration"],
52
- default: "Start with new correction table, no current",
54
+ startWithNewTable: {
55
+ type: "boolean",
56
+ title: "Start with new table",
57
+ description: "Start with a new correction table (overrides existing table).",
58
+ default: false
59
+ },
60
+ estimateBoatSpeed: {
61
+ type: "boolean",
62
+ title: "Estimate Boat Speed",
63
+ description: "Enable estimation of boat speed.",
64
+ default: false
65
+ },
66
+ updateCorrectionTable: {
67
+ type: "boolean",
68
+ title: "Update Correction Table",
69
+ description: "Enable updating of the correction table.",
70
+ default: true
71
+ },
72
+ stability: {
73
+ type: "number",
74
+ title: "Correction Table Stability",
75
+ description: "Stability of the corrections (higher means that corrections are changed at a slower rate).",
76
+ default: 7,
77
+ minimum: 1,
78
+ maximum: 20
79
+ },
80
+ assumeCurrent: {
81
+ type: "boolean",
82
+ title: "Assume Current",
83
+ description: "Assume there is a current present when updating the correction table.",
84
+ default: false
53
85
  },
54
86
  heelStep: {
55
87
  type: "number",
@@ -75,144 +107,98 @@ module.exports = function (app) {
75
107
  description: "Correction table maximum speed.",
76
108
  default: 9
77
109
  },
78
-
79
110
  headingSource: {
80
111
  type: "string",
81
112
  title: "Heading Source",
82
113
  description: "Source to use for navigation.heading (optional).",
83
114
  default: " "
84
115
  },
85
-
86
116
  boatSpeedSource: {
87
117
  type: "string",
88
118
  title: "Boat Speed Source",
89
119
  description: "Source to use for navigation.speedThroughWater (optional).",
90
120
  default: " "
91
121
  },
92
-
93
122
  COGSource: {
94
123
  type: "string",
95
124
  title: "Course over ground",
96
125
  description: "Source to use for navigation.courseOverGroundTrue (optional)",
97
126
  default: " "
98
127
  },
99
-
100
128
  SOGSource: {
101
129
  type: "string",
102
130
  title: "Ground Speed Source",
103
131
  description: "Source to use for navigation.speedOverGround (optional)",
104
132
  default: " "
105
133
  },
106
-
107
134
  attitudeSource: {
108
135
  type: "string",
109
136
  title: "Attitude Source",
110
137
  description: "Source to use for navigation.attitude (optional)",
111
138
  default: " "
112
139
  },
113
-
114
140
  preventDuplication: {
115
141
  type: "boolean",
116
142
  title: "Prevent duplication of boat speed",
117
143
  description: "Overwrite boat speed from sensor with corrected boat speed.",
118
- default: true,
119
- },
120
-
121
- updateCorrectionTable: {
122
- type: "boolean",
123
- title: "Update correction table",
124
- description: "Update estimations in the correction table.",
125
- default: true,
126
- },
127
- estimateBoatSpeed: {
128
- type: "boolean",
129
- title: "Correct boat speed",
130
- description: "Correct boat speed using correction table. This includes adding leeway.",
131
- default: true,
132
- },
133
- correctionStability: {
134
- type: "number",
135
- title: "Stability of correction estimation",
136
- description: `The stability of the correction estimation indicates how much the estimation is trusted
137
- opposed to the calculation of . Biggeboat speed. Bigger values give more trust to the estimation and more stable estimations`,
138
- default: 7,
139
- minimum: 5,
140
- maximum: 12
141
- },
142
- doStartFresh: {
143
- type: "boolean",
144
- title: "Empty correction table",
145
- description: "Restart with a fresh correction table. WARNING: all current corrections will be lost permanently!",
146
- default: false
147
- },
148
- assumeCurrent: {
149
- type: "boolean",
150
- title: "assume current",
151
- description: "The estimator assumes that there is current.",
152
- default: false,
153
- },
154
- estimateCurrent: {
155
- type: "boolean",
156
- title: "Estimate current",
157
- description: "Estimate water current.",
158
- default: true,
159
- },
160
- currentStability: {
161
- type: "number",
162
- title: "Stability of current estimation ",
163
- description: `The stability of the current estimation indicates how much the estimation is trusted
164
- opposed to the calculation of current. Bigger values give more trust to the estimation and more sable estimations`,
165
- default: 5,
166
- minimum: 1,
167
- maximum: 8
144
+ default: true
168
145
  }
169
146
  }
170
147
  };
171
148
 
172
149
  plugin.uiSchema = {
173
- 'ui:order': ["mode", "maxSpeed", "speedStep", "maxHeel", "heelStep", "headingSource", "boatSpeedSource", "COGSource", "SOGSource", "attitudeSource", "preventDuplication", "updateCorrectionTable", "estimateBoatSpeed", "correctionStability", "doStartFresh", "assumeCurrent", "estimateCurrent", "currentStability"],
174
- doStartFresh: {
175
- "ui:widget": "hidden",
150
+ 'ui:order': [
151
+ "estimateBoatSpeed",
152
+ "updateCorrectionTable",
153
+ "assumeCurrent",
154
+ "startWithNewTable",
155
+ "stability",
156
+ "maxSpeed",
157
+ "speedStep",
158
+ "maxHeel",
159
+ "heelStep",
160
+ "headingSource",
161
+ "boatSpeedSource",
162
+ "COGSource",
163
+ "SOGSource",
164
+ "attitudeSource",
165
+ "preventDuplication"
166
+ ],
167
+ startWithNewTable: {
168
+ "ui:widget": "checkbox"
169
+ },
170
+ estimateBoatSpeed: {
171
+ "ui:widget": "checkbox"
172
+ },
173
+ updateCorrectionTable: {
174
+ "ui:widget": "checkbox"
175
+ },
176
+ stability: {
177
+ "ui:widget": "updown"
178
+ },
179
+ assumeCurrent: {
180
+ "ui:widget": "checkbox"
176
181
  },
177
182
  heelStep: {
178
- "ui:widget": "updown",
183
+ "ui:widget": "updown"
179
184
  },
180
185
  maxHeel: {
181
- "ui:widget": "updown",
186
+ "ui:widget": "updown"
182
187
  },
183
188
  speedStep: {
184
- "ui:widget": "updown",
189
+ "ui:widget": "updown"
185
190
  },
186
191
  maxSpeed: {
187
- "ui:widget": "updown",
192
+ "ui:widget": "updown"
188
193
  },
189
194
  mode: {
190
- "ui:widget": "select",
191
- },
192
- updateCorrectionTable: {
193
- "ui:widget": "hidden",
194
- },
195
- estimateBoatSpeed: {
196
- "ui:widget": "hidden",
197
- },
198
- correctionStability: {
199
- "ui:widget": "hidden",
195
+ "ui:widget": "select"
200
196
  },
201
197
  preventDuplication: {
202
- "ui:widget": "checkbox",
203
- },
204
- assumeCurrent: {
205
- "ui:widget": "hidden",
206
- },
207
- estimateCurrent: {
208
- "ui:widget": "hidden",
209
- },
210
- currentStability: {
211
- "ui:widget": "hidden",
198
+ "ui:widget": "checkbox"
212
199
  }
213
200
  };
214
201
 
215
-
216
202
  plugin.registerWithRouter = function (router) {
217
203
  app.debug('registerWithRouter');
218
204
 
@@ -236,91 +222,66 @@ module.exports = function (app) {
236
222
 
237
223
  }
238
224
 
239
- // ...existing code...
240
-
241
- function saveTable(options, correctionTable) {
242
- options.correctionTable = correctionTable.toJSON();
243
- app.savePluginOptions(options, () => { app.debug('Correction table saved') });
244
- return new Date();
245
- }
246
-
247
-
248
- plugin.start = (opts) => {
249
- app.setPluginStatus("starting");
250
- app.debug("plugin starting");
251
- let outputs = [];
252
-
253
- // make options survive to the stop function
254
- options = opts;
255
- loadPresets();
256
-
257
- // correction table
258
- table = loadTable(options);
225
+ plugin.start = (settings) => {
226
+ app.setPluginStatus("Starting");
227
+ app.debug("Starting");
228
+ table = loadTable(settings);
259
229
 
230
+ //#region Handler and Polar Initialization
231
+ let smootherOptions = { timeConstant: 1, processVariance: 1, measurementVariance: 20, timeSpan: 5 };
260
232
 
261
233
  // heading
262
- heading = new MessageHandler("heading", "navigation.headingTrue", options.headingSource);
263
- heading.subscribe(app, plugin.id, true);
264
- heading.setDisplayAttributes({ label: "heading" });
265
- headingStat = new PolarDamped("headingDamped", heading, 1);
266
- heading.onChange = () => {
267
- headingStat.sample();
268
- };
234
+ smoothedHeading = new SmoothedHeading(app, plugin.id, settings.headingSource, true, MovingAverageSmoother, smootherOptions);
235
+ rawHeading = smoothedHeading.polar.angleHandler;
236
+
237
+ //attitude
238
+ smoothedAttitude = new SmoothedAttitude(app, plugin.id, settings.attitudeSource, true, MovingAverageSmoother, smootherOptions);
239
+ rawAttitude = smoothedAttitude.handler;
269
240
 
270
- // attitude
271
- attitude = new MessageHandler("attitude", "navigation.attitude", options.attitudeSource);
272
- attitude.value = { pitch: 0, roll: 0, yaw: 0 }; //prevents errors when there is no attitude sensor
273
- attitude.setDisplayAttributes({ label: "attitude" });
274
- attitude.subscribe(app, plugin.id, true);
275
- attitudeStat = new PolarDamped("attitudeDamped", attitude, 1);
276
- attitude.onChange = () => {
277
- attitudeStat.sample();
278
- };
279
241
 
280
242
  // current
281
- current = new Polar("current", "self.environment.current.drift", "self.environment.current.setTrue");
282
- current.setDisplayAttributes({ label: "current", plane: "Ground" });
283
- current.setAngleRange('0to2pi');
284
- currentStat = new PolarDamped("currentDamped", current, 1);
285
- currentStat.setAngleRange('0to2pi');
286
- outputs.push(current);
243
+ // send metadata for current
244
+ MessageHandler.setMeta(app, plugin.id, "environment.current.drift", {units: "m/s", type: "number", description: "Speed of the current"});
245
+ MessageHandler.setMeta(app, plugin.id, "environment.current.setTrue", { units: "rad", type: "number", description: "Direction of the current" });
246
+ rawCurrent = new Polar("current", "self.environment.current.drift", "self.environment.current.setTrue");
247
+ rawCurrent.setDisplayAttributes({ label: "current", plane: "Ground" });
248
+ rawCurrent.setAngleRange('0to2pi');
249
+ smoothedCurrent = new PolarSmoother("currentDamped", rawCurrent, KalmanSmoother, { processVariance: 0.000001, measurementVariance: 0.01 });
250
+ smoothedCurrent.setAngleRange('0to2pi');
251
+ smoothedCurrent.setDisplayAttributes({ label: "Current", plane: "Ground" });
287
252
  // Current should be initialised as no current
288
- current.setVectorValue({ x: 0, y: 0 });
289
- // There should be at least two samples, otherwise we can't calculate a valid speed
290
- currentStat.sample();
291
- currentStat.sample();
253
+ rawCurrent.setVectorValue({ x: 0, y: 0 });
254
+ // Strongly assume no current at start
255
+ smoothedCurrent.xSmoother.reset(0, 0.00000001);
256
+ smoothedCurrent.ySmoother.reset(0, 0.00000001);
257
+ // no current
258
+ noCurrent = createSmoothedPolar({
259
+ id: "boatSpeed",
260
+ pathMagnitude: "self.environment.current.drift",
261
+ pathAngle: "self.environment.current.setTrue",
262
+ subscribe: false,
263
+ app,
264
+ pluginId: plugin.id,
265
+ SmootherClass: BaseSmoother,
266
+ smootherOptions: smootherOptions,
267
+ displayAttributes: { label: "NoCurrent", plane: "Ground" },
268
+ });
269
+ noCurrent.xSmoother.reset(0,0);
270
+ noCurrent.ySmoother.reset(0,0);
271
+ PolarSmoother.send(app, plugin.id, [noCurrent]);
292
272
 
293
- // boat speed
294
- boatSpeed = new Polar("boatSpeed", "navigation.speedThroughWater", options.boatSpeedSource);
295
- boatSpeed.subscribe(app, plugin.id, true, false, !options.preventDuplication || !options.estimateBoatSpeed);
296
- boatSpeed.setDisplayAttributes({ label: "boat speed", plane: "Boat" });
297
- boatSpeedStat = new PolarDamped("boatSpeedDamped", boatSpeed, 1);
298
- boatSpeedStat.setDisplayAttributes({ label: "boat speed (damped)", plane: "Boat" });
299
- boatSpeed.onChange = () => {
300
- boatSpeedStat.sample();
301
- };
302
273
 
274
+ // boat speed
275
+ smoothedBoatSpeed = new SmoothedSpeedThroughWater(app, plugin.id, settings.boatSpeedSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
276
+ rawBoatSpeed = smoothedBoatSpeed.polar;
303
277
  correctedBoatSpeed = new Polar("correctedBoatSpeed", "navigation.speedThroughWater", "navigation.leewayAngle");
304
278
  correctedBoatSpeed.setDisplayAttributes({ label: "corrected boat speed", plane: "Boat" });
305
- outputs.push(correctedBoatSpeed);
306
- //const correctedBoatSpeedStat = new PolarDamped("correctedBoatSpeedDamped", correctedBoatSpeed);
307
-
308
- // TODO: Add navigation.speedThroughWaterTransverse and navigation.speedThroughWaterLongitudinal
309
-
310
279
  boatSpeedRefGround = new Polar("boatSpeedRefGround");
311
- boatSpeedRefGround.setDisplayAttributes("boat speed over ground", "Ground");
312
-
280
+ boatSpeedRefGround.setDisplayAttributes("Boat speed over ground", "Ground");
281
+
313
282
  // ground speed
314
- groundSpeed = new Polar("groundSpeed", "navigation.speedOverGround", "navigation.courseOverGroundTrue", options.SOGSource, options.COGSource);
315
- groundSpeed.subscribe(app, plugin.id, true, true, true);
316
- groundSpeed.setDisplayAttributes({ label: "ground speed", plane: "Ground" });
317
- groundSpeed.setAngleRange('0to2pi');
318
- groundSpeedStat = new PolarDamped("groundSpeedDamped", groundSpeed, 1);
319
- groundSpeedStat.setDisplayAttributes({ label: "ground speed (damped)", plane: "Ground" });
320
- groundSpeedStat.setAngleRange('0to2pi');
321
- groundSpeed.onChange = () => {
322
- groundSpeedStat.sample();
323
- };
283
+ smoothedGroundSpeed = new SmoothedGroundSpeed(app, plugin.id, settings.SOGSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
284
+ rawGroundSpeed = smoothedGroundSpeed.polar;
324
285
 
325
286
  // correction vector
326
287
  speedCorrection = new Polar("speedCorrection");
@@ -331,273 +292,93 @@ module.exports = function (app) {
331
292
  residual = new Polar("residual");
332
293
  residual.setDisplayAttributes({ label: "residual", plane: "Ground" });
333
294
 
334
- // Make reporting object for webApp
295
+ //#endregion
296
+
297
+ //#region Reporting
335
298
  reportFull = new Reporter();
336
- reportFull.addDelta(heading);
337
- reportFull.addAttitude(attitude);
338
- reportFull.addPolar(groundSpeed);
339
- reportFull.addPolar(boatSpeed);
340
- //reportFull.addPolar(boatSpeedStat);
341
- //reportFull.addPolar(groundSpeedStat);
342
- reportFull.addPolar(speedCorrection);
343
- reportFull.addPolar(correctedBoatSpeed);
344
- reportFull.addPolar(current);
345
- reportFull.addPolar(residual);
299
+
300
+ if (settings.estimateBoatSpeed) {
301
+ reportFull.addDelta(rawHeading);
302
+ reportFull.addAttitude(rawAttitude);
303
+ reportFull.addPolar(rawBoatSpeed);
304
+ reportFull.addPolar(speedCorrection);
305
+ reportFull.addPolar(correctedBoatSpeed);
306
+ reportFull.addPolar(rawGroundSpeed);
307
+ reportFull.addPolar(smoothedCurrent);
308
+ }
309
+ if (settings.updateCorrectionTable) {
310
+ reportFull.addDelta(smoothedHeading);
311
+ reportFull.addAttitude(smoothedAttitude);
312
+ reportFull.addPolar(smoothedBoatSpeed);
313
+ reportFull.addPolar(smoothedGroundSpeed);
314
+
315
+ if (settings.assumeCurrent) {
316
+ reportFull.addPolar(boatSpeedRefGround);
317
+ reportFull.addPolar(smoothedCurrent);
318
+ }
319
+ reportFull.addPolar(speedCorrection);
320
+ reportFull.addPolar(residual);
321
+ }
346
322
  reportFull.addTable(table);
347
323
 
324
+ // Make reporting object for webApp
325
+
326
+
348
327
  reportVector = new Reporter();
349
- reportVector.addDelta(heading);
328
+ reportVector.addDelta(rawHeading);
350
329
  reportVector.addPolar(residual);
351
330
  reportVector.addPolar(speedCorrection);
352
- reportVector.addPolar(current);
331
+ if (settings.assumeCurrent) reportVector.addPolar(smoothedCurrent);
353
332
  reportVector.addPolar(correctedBoatSpeed);
354
- reportVector.addPolar(boatSpeed);
355
- reportVector.addPolar(groundSpeed);
356
-
357
- boatSpeed.onChange = () => {
358
- boatSpeedStat.sample();
359
- calculate();
360
- };
361
-
362
-
333
+ reportVector.addPolar(rawBoatSpeed);
334
+ reportVector.addPolar(rawGroundSpeed);
335
+ //#endregion
363
336
 
364
337
  isRunning = true;
338
+ started = new Date();
365
339
  app.setPluginStatus("Running");
366
340
  app.debug("Running");
367
341
 
368
-
369
-
370
-
371
-
372
-
373
- // Main function, estimates boatSpeed, leeway and current
374
- function calculate() {
375
- // prepare iteration
376
- heel = attitude.value.roll;
377
- speed = boatSpeed.magnitude;
378
- theta = heading.value;
379
- if (!Number.isFinite(heel)) {
380
- app.debug("Heel is not a valid number, skipping calculation");
381
- return;
382
- }
383
- if (!Number.isFinite(speed)) {
384
- app.debug("Speed is not a valid number, skipping calculation");
385
- return;
386
- }
387
- if (!Number.isFinite(theta)) {
388
- app.debug("Heading is not a valid number, skipping calculation");
389
- return;
390
- }
391
-
392
- // update correction table
393
- if (options.updateCorrectionTable) {
394
- table.update(speed, heel, groundSpeedStat, currentStat, boatSpeedStat, theta);
395
- }
396
-
397
- // correct and estimate boat speed
398
- estimateBoatSpeed();
399
-
400
- if (options.assumeCurrent) {
401
- // estimate current ;
402
- estimateCurrent();
403
- }
404
- calcResidual();
405
-
406
-
407
- // Send calculated delta's
408
- updates = [];
409
- if (options.estimateCurrent) updates.push(current);
410
- if (options.estimateBoatSpeed) updates.push(correctedBoatSpeed);
411
- if (updates.length) Polar.send(app, plugin.id, updates);
412
-
413
- // Save correction table
414
- // periodically
415
- if (options.updateCorrectionTable && new Date() - lastSave > 5 * 60 * 1000) {
416
- lastSave = saveTable(options, table);
417
- }
418
- }
419
-
420
- // estimates boat speed from observed boatSpeed and correction
421
- function estimateBoatSpeed() {
422
- cor = table.getKalmanCorrection(speed, heel);
423
- correctedBoatSpeed.copyFrom(boatSpeed);
424
- speedCorrection.setVectorValue(cor);
425
- correctedBoatSpeed.add(speedCorrection);
426
- }
427
-
428
- // estimates current from observed groundspeed and estimated boatspeed
429
- function estimateCurrent() {
430
- boatSpeedRefGround.copyFrom(correctedBoatSpeed);
431
- boatSpeedRefGround.rotate(theta);
432
- current.copyFrom(groundSpeed);
433
- current.substract(boatSpeedRefGround);
434
- currentStat.sample();
435
- current.copyFrom(currentStat);
436
- }
437
-
438
- // Calculate what ground speed is observed that is not attributed to estimated boat speed or estimated current
439
- function calcResidual() {
440
- residual.copyFrom(groundSpeed);
441
- boatSpeedRefGround.copyFrom(correctedBoatSpeed);
442
- boatSpeedRefGround.rotate(theta);
443
- residual.substract(boatSpeedRefGround);
444
- residual.substract(current);
445
- }
446
-
447
- // Make sure the table matches the settings
448
- function enforceConsistancy(row, col) {
449
- // function is bugged due to IS units vs knots and degrees
450
- if (!options?.correctionTable) return false;
451
- table = options.correctionTable;
452
- if (!table) return false;
453
- if (!table.row) return false;
454
- if (table.row.min != row.min) return false;
455
- if (table.row.max != row.max) return false;
456
- if (table.row.step != row.step) return false;
457
- if (table.col.min != col.min) return false;
458
- if (table.col.max != col.max) return false;
459
- if (table.col.step != col.step) return false;
460
- return true;
461
- }
462
-
463
- function loadTable(options) {
464
- const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
465
- const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
466
- let table;
467
- if (enforceConsistancy(row, col) && !options.doStartFresh) {
468
- table = CorrectionTable.fromJSON(options.correctionTable, options.correctionStability);
469
- app.debug("Correction table loaded");
470
- lastSave = new Date();
471
- }
472
- else {
473
- table = new CorrectionTable("correctionTable", row, col, options.correctionStability);
474
- app.debug("Correction table created");
475
- options.doStartFresh = false;
476
- lastSave = saveTable(options, table);
477
- }
478
- table.setDisplayAttributes({ label: "correction table" });
479
- return table;
480
- }
481
-
482
-
483
- function loadPresets() {
484
- const modeEnum = plugin.schema.properties.mode.enum;
485
- app.debug("Loading presests for mode: " + options.mode);
486
-
487
- switch (modeEnum.indexOf(options.mode)) {
488
- case -1:
489
- app.debug("Invalid mode");
490
- app.debug(options);
491
- //options.mode = modeEnum[2];
492
- break;
493
- case 0:
494
- // new table, no current
495
- options.doStartFresh = true;
496
- options.updateCorrectionTable = true;
497
- options.estimateBoatSpeed = false;
498
- options.assumeCurrent = false;
499
- options.estimateCurrent = false;
500
- options.correctionStability = 5;
501
- options.mode = modeEnum[2];
502
- break;
503
- case 1:
504
- // new table, with current
505
- options.doStartFresh = true;
506
- options.updateCorrectionTable = true;
507
- options.estimateBoatSpeed = false;
508
- options.assumeCurrent = true;
509
- options.estimateCurrent = false;
510
- options.correctionStability = 5;
511
- options.currentStability = 7;
512
- options.mode = modeEnum[3];
513
- break;
514
- case 2:
515
- // fresh table, no current
516
- options.doStartFresh = false;
517
- options.updateCorrectionTable = true;
518
- options.estimateBoatSpeed = false;
519
- options.assumeCurrent = false;
520
- options.estimateCurrent = false;
521
- options.correctionStability = 6;
522
- break;
523
- case 3:
524
- // fresh table, with current
525
- options.doStartFresh = false;
526
- options.updateCorrectionTable = true;
527
- options.estimateBoatSpeed = false;
528
- options.assumeCurrent = true;
529
- options.estimateCurrent = false;
530
- options.correctionStability = 6;
531
- options.currentStability = 6;
532
- break;
533
- case 4:
534
- // mature table, no current
535
- options.doStartFresh = false;
536
- options.updateCorrectionTable = true;
537
- options.estimateBoatSpeed = true;
538
- options.assumeCurrent = false;
539
- options.estimateCurrent = false;
540
- options.correctionStability = 10;
541
- break;
542
- case 5:
543
- // mature table, with current
544
- options.doStartFresh = false;
545
- options.updateCorrectionTable = true;
546
- options.estimateBoatSpeed = true;
547
- options.assumeCurrent = true;
548
- options.estimateCurrent = true;
549
- options.correctionStability = 10;
550
- options.currentStability = 3;
551
- break;
552
- case 6:
553
- // locked table
554
- options.doStartFresh = false;
555
- options.updateCorrectionTable = false;
556
- options.estimateBoatSpeed = true;
557
- options.assumeCurrent = true;
558
- options.estimateCurrent = true;
559
- options.currentStability = 2;
560
- break;
561
- case 7:
562
- // all manual configuration;
563
- break;
564
- }
565
- }
566
-
567
- // compare floats
568
- function isEqual(a, b) {
569
- const TOLERANCE = 1e-5;
570
- if (Math.abs(a - b) < TOLERANCE) {
571
- return true;
342
+ let lastSave = 0;
343
+ smoothedBoatSpeed.onChange = () => {
344
+ const wellUnderway = started < new Date() - 60 * 1000;
345
+ const minSpeed = SI.fromKnots(settings.speedStep / 2);
346
+ if (settings.estimateBoatSpeed) correct(wellUnderway);
347
+ if (settings.updateCorrectionTable && wellUnderway) {
348
+ updateTable(settings.assumeCurrent, minSpeed);
349
+ // Save correction table periodically
350
+ const now = new Date();
351
+ if (now - lastSave > 5 * 1000) {
352
+ saveTable(settings, table);
353
+ lastSave = now;
354
+ }
572
355
  }
573
- return false;
574
- }
356
+ };
575
357
  }
576
358
 
577
-
578
359
  plugin.stop = () => {
579
360
  return new Promise((resolve, reject) => {
580
361
  try {
581
- //corrTable.save(app);
582
- if (corrTable != null) {
583
- lastSave = saveTable(options, corrTable);
584
- }
585
-
586
- heading = heading?.terminate(app);
587
- attitude = attitude?.terminate(app);
588
- current = current?.terminate(app);
589
- currentStat = null;
590
- boatSpeed = boatSpeed?.terminate(app);
591
- boatSpeedStat = null;
362
+ smoothedHeading = smoothedHeading?.terminate(app);
363
+ stability = stability?.terminate?.(app);
364
+ smoothedAttitude = smoothedAttitude?.terminate(app);
365
+ rawCurrent = rawCurrent?.terminate(app);
366
+ smoothedCurrent = smoothedCurrent?.terminate?.(app);
367
+ smoothedBoatSpeed = smoothedBoatSpeed?.terminate(app);
592
368
  correctedBoatSpeed = correctedBoatSpeed?.terminate(app);
593
369
  boatSpeedRefGround = boatSpeedRefGround?.terminate(app);
594
- groundSpeed = groundSpeed?.terminate(app);
595
- groundSpeedStat = null;
370
+ smoothedGroundSpeed = smoothedGroundSpeed?.terminate(app);
596
371
  speedCorrection = speedCorrection?.terminate(app);
597
372
  residual = residual?.terminate(app);
598
373
  reportFull = null;
599
374
  reportVector = null;
600
375
  table = null;
376
+ rawHeading = null;
377
+ rawAttitude = null;
378
+ noCurrent = null;
379
+ rawBoatSpeed = null;
380
+ rawGroundSpeed = null;
381
+ started = null;
601
382
  app.setPluginStatus("Stopped");
602
383
  app.debug("Stopped");
603
384
 
@@ -608,5 +389,157 @@ module.exports = function (app) {
608
389
  }
609
390
  });
610
391
  };
392
+
393
+ /**
394
+ * Estimates and applies corrections to boat speed, leeway, and current.
395
+ *
396
+ * This function uses the current attitude, boat speed, and heading to compute a correction
397
+ * vector from the correction table, applies it to the observed boat speed, and sends the
398
+ * corrected value. If the vessel is well underway, it also estimates the current vector
399
+ * by comparing the corrected boat speed (rotated to heading) with the ground speed.
400
+ *
401
+ * @param {boolean} wellUnderway - True if the vessel is considered underway and current estimation should be performed.
402
+ */
403
+ function correct(wellUnderway) {
404
+ // prepare iteration
405
+ const heel = rawAttitude.value?.roll;
406
+ const speed = rawBoatSpeed.magnitude;
407
+ const theta = rawHeading.value;
408
+ //app.debug(`Heel: ${SI.toDegrees(heel).toFixed(1)}°, Speed: ${SI.toKnots(speed).toFixed(2)} kn, Heading: ${SI.toDegrees(theta).toFixed(1)}°`);
409
+ if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) {
410
+ //app.debug("Invalid input data");
411
+ return;
412
+ }
413
+
414
+ correctedBoatSpeed.copyFrom(rawBoatSpeed);
415
+ speedCorrection.setVectorValue({ x: 0, y: 0 }) ;
416
+ if (speed > 0 && !rawAttitude.stale ) {
417
+ const { correction, variance } = table.getCorrection(speed, heel);
418
+ speedCorrection.setVectorValue(correction, variance);
419
+ correctedBoatSpeed.add(speedCorrection);
420
+ }
421
+ Polar.send(app, plugin.id, [correctedBoatSpeed]);
422
+ // estimate current
423
+ if (wellUnderway && !rawGroundSpeed.stale) {
424
+ boatSpeedRefGround.copyFrom(correctedBoatSpeed);
425
+ boatSpeedRefGround.rotate(theta);
426
+ rawCurrent.copyFrom(rawGroundSpeed);
427
+ rawCurrent.substract(boatSpeedRefGround);
428
+ smoothedCurrent.sample();
429
+ }
430
+ PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
431
+ }
432
+
433
+ /**
434
+ * Updates the correction table and calculates the residual error.
435
+ *
436
+ * This function uses the current smoothed attitude, boat speed, and heading to update the correction table
437
+ * if the boat speed exceeds the specified minimum. It also calculates the residual between the measured ground speed
438
+ * and the expected ground speed (corrected boat speed plus current). The correction table is periodically saved.
439
+ *
440
+ * @param {boolean} [assumeCurrent=false] - If true, uses the smoothed current for correction; otherwise, assumes no current.
441
+ * @param {number} [minSpeed=0] - The minimum speed threshold (in SI units) required to update the correction table.
442
+ */
443
+ function updateTable(assumeCurrent = false, minSpeed = 0) {
444
+ // prepare iteration
445
+ const heel = smoothedAttitude.value.roll;
446
+ const speed = smoothedBoatSpeed.magnitude;
447
+ const theta = smoothedHeading.value;
448
+
449
+ if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) return;
450
+
451
+ // update correction table
452
+ if (speed > minSpeed && !smoothedAttitude.stale && !smoothedHeading.stale && !smoothedGroundSpeed.stale) {
453
+ table.update(speed, heel, smoothedGroundSpeed, assumeCurrent ? smoothedCurrent : noCurrent, smoothedBoatSpeed, theta);
454
+ }
455
+
456
+ // calculate residual
457
+ residual.copyFrom(smoothedGroundSpeed);
458
+ boatSpeedRefGround.copyFrom(correctedBoatSpeed);
459
+ boatSpeedRefGround.rotate(theta);
460
+ residual.substract(boatSpeedRefGround);
461
+ residual.substract(smoothedCurrent);
462
+
463
+ }
464
+
465
+ /**
466
+ * Checks if the correction table in settings matches the expected row and column configuration.
467
+ *
468
+ * This function compares the min, max, and step values of the row and column definitions
469
+ * in the current correction table against the provided row and col objects. Returns true
470
+ * if all values match, otherwise false. If the table is missing or not properly structured,
471
+ * returns false.
472
+ *
473
+ * @param {Object} row - The expected row configuration ({ min, max, step }).
474
+ * @param {Object} col - The expected column configuration ({ min, max, step }).
475
+ * @returns {boolean} True if the table matches the configuration, false otherwise.
476
+ */
477
+ function enforceConsistancy(options, row, col) {
478
+ if (!options?.correctionTable) return false;
479
+ table = options.correctionTable;
480
+ if (!table) return false;
481
+ if (!table.row) return false;
482
+ if (table.row.min != row.min) return false;
483
+ if (table.row.max != row.max) return false;
484
+ if (table.row.step != row.step) return false;
485
+ if (table.col.min != col.min) return false;
486
+ if (table.col.max != col.max) return false;
487
+ if (table.col.step != col.step) return false;
488
+ return true;
489
+ }
490
+
491
+ /**
492
+ * Loads or creates a correction table based on the provided options.
493
+ *
494
+ * This function checks if the correction table in the options matches the expected row and column configuration.
495
+ * If so, it loads the table from JSON; otherwise, it creates a new correction table with the specified parameters.
496
+ * The table is labeled and, if newly created, saved to the plugin options.
497
+ *
498
+ * @param {Object} options - The plugin options containing correction table settings and parameters.
499
+ * @returns {CorrectionTable} The loaded or newly created CorrectionTable instance.
500
+ */
501
+ function loadTable(options) {
502
+ const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
503
+ const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
504
+ let table;
505
+ const stability = (options.stability !== undefined) ? options.stability : 6;
506
+ const VAR_FLOOR = 1e-4;
507
+ if (enforceConsistancy(options, row, col) && !options.startWithNewTable) {
508
+ table = CorrectionTable.fromJSON(options.correctionTable, stability);
509
+ app.debug("Correction table loaded");
510
+ }
511
+ else if (options.correctionTable && !options.startWithNewTable) {
512
+ // Resample existing table to new dimensions conservatively (N=0, diag covariance with floor)
513
+ table = CorrectionTable.resampleFromJSON(options.correctionTable, row, col, stability, VAR_FLOOR);
514
+ app.debug("Correction table migrated to new dimensions");
515
+ options.startWithNewTable = false;
516
+ saveTable(options, table);
517
+ } else {
518
+ table = new CorrectionTable("correctionTable", row, col, stability);
519
+ app.debug("Correction table created");
520
+ // doStartFresh is not user-settable, so no need to reset it here
521
+ options.startWithNewTable = false;
522
+ saveTable(options, table);
523
+ }
524
+ table.setDisplayAttributes({ label: "correction table" });
525
+ return table;
526
+ }
527
+
528
+ /**
529
+ * Saves the correction table to the plugin options and logs the save event.
530
+ *
531
+ * This function serializes the provided correction table, updates the options object,
532
+ * and triggers the plugin's save mechanism. It returns the current date/time to indicate
533
+ * when the save occurred.
534
+ *
535
+ * @param {Object} options - The plugin options object to update.
536
+ * @param {CorrectionTable} correctionTable - The correction table instance to save.
537
+ * @returns {Date} The date and time when the table was saved.
538
+ */
539
+ function saveTable(options, correctionTable) {
540
+ options.correctionTable = correctionTable.toJSON();
541
+ app.savePluginOptions(options, () => { /*app.debug("Correction table saved")*/; });
542
+ }
543
+
611
544
  return plugin;
612
545
  };