speedandcurrent 1.4.2 → 1.5.0

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,62 @@ 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
+ rawCurrent = new Polar("current", "self.environment.current.drift", "self.environment.current.setTrue");
244
+ rawCurrent.setDisplayAttributes({ label: "current", plane: "Ground" });
245
+ rawCurrent.setAngleRange('0to2pi');
246
+ smoothedCurrent = new PolarSmoother("currentDamped", rawCurrent, KalmanSmoother, { processVariance: 0.000001, measurementVariance: 0.01 });
247
+ smoothedCurrent.setAngleRange('0to2pi');
248
+ smoothedCurrent.setDisplayAttributes({ label: "Current", plane: "Ground" });
287
249
  // 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();
250
+ rawCurrent.setVectorValue({ x: 0, y: 0 });
251
+ // Strongly assume no current at start
252
+ smoothedCurrent.xSmoother.reset(0, 0.00000001);
253
+ smoothedCurrent.ySmoother.reset(0, 0.00000001);
254
+ // no current
255
+ noCurrent = createSmoothedPolar({
256
+ id: "boatSpeed",
257
+ pathMagnitude: "self.environment.current.drift",
258
+ pathAngle: "self.environment.current.setTrue",
259
+ subscribe: false,
260
+ app,
261
+ pluginId: plugin.id,
262
+ SmootherClass: BaseSmoother,
263
+ smootherOptions: smootherOptions,
264
+ displayAttributes: { label: "NoCurrent", plane: "Ground" },
265
+ });
266
+ noCurrent.xSmoother.reset(0,0);
267
+ noCurrent.ySmoother.reset(0,0);
268
+ PolarSmoother.send(app, plugin.id, [noCurrent]);
292
269
 
293
270
  // 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
-
271
+ smoothedBoatSpeed = new SmoothedSpeedThroughWater(app, plugin.id, settings.boatSpeedSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
272
+ rawBoatSpeed = smoothedBoatSpeed.polar;
303
273
  correctedBoatSpeed = new Polar("correctedBoatSpeed", "navigation.speedThroughWater", "navigation.leewayAngle");
304
274
  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
275
  boatSpeedRefGround = new Polar("boatSpeedRefGround");
311
- boatSpeedRefGround.setDisplayAttributes("boat speed over ground", "Ground");
312
-
276
+ boatSpeedRefGround.setDisplayAttributes("Boat speed over ground", "Ground");
277
+
313
278
  // 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
- };
279
+ smoothedGroundSpeed = new SmoothedGroundSpeed(app, plugin.id, settings.SOGSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
280
+ rawGroundSpeed = smoothedGroundSpeed.polar;
324
281
 
325
282
  // correction vector
326
283
  speedCorrection = new Polar("speedCorrection");
@@ -331,273 +288,93 @@ module.exports = function (app) {
331
288
  residual = new Polar("residual");
332
289
  residual.setDisplayAttributes({ label: "residual", plane: "Ground" });
333
290
 
334
- // Make reporting object for webApp
291
+ //#endregion
292
+
293
+ //#region Reporting
335
294
  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);
295
+
296
+ if (settings.estimateBoatSpeed) {
297
+ reportFull.addDelta(rawHeading);
298
+ reportFull.addAttitude(rawAttitude);
299
+ reportFull.addPolar(rawBoatSpeed);
300
+ reportFull.addPolar(speedCorrection);
301
+ reportFull.addPolar(correctedBoatSpeed);
302
+ reportFull.addPolar(rawGroundSpeed);
303
+ reportFull.addPolar(smoothedCurrent);
304
+ }
305
+ if (settings.updateCorrectionTable) {
306
+ reportFull.addDelta(smoothedHeading);
307
+ reportFull.addAttitude(smoothedAttitude);
308
+ reportFull.addPolar(smoothedBoatSpeed);
309
+ reportFull.addPolar(smoothedGroundSpeed);
310
+
311
+ if (settings.assumeCurrent) {
312
+ reportFull.addPolar(boatSpeedRefGround);
313
+ reportFull.addPolar(smoothedCurrent);
314
+ }
315
+ reportFull.addPolar(speedCorrection);
316
+ reportFull.addPolar(residual);
317
+ }
346
318
  reportFull.addTable(table);
347
319
 
320
+ // Make reporting object for webApp
321
+
322
+
348
323
  reportVector = new Reporter();
349
- reportVector.addDelta(heading);
324
+ reportVector.addDelta(rawHeading);
350
325
  reportVector.addPolar(residual);
351
326
  reportVector.addPolar(speedCorrection);
352
- reportVector.addPolar(current);
327
+ if (settings.assumeCurrent) reportVector.addPolar(smoothedCurrent);
353
328
  reportVector.addPolar(correctedBoatSpeed);
354
- reportVector.addPolar(boatSpeed);
355
- reportVector.addPolar(groundSpeed);
356
-
357
- boatSpeed.onChange = () => {
358
- boatSpeedStat.sample();
359
- calculate();
360
- };
361
-
362
-
329
+ reportVector.addPolar(rawBoatSpeed);
330
+ reportVector.addPolar(rawGroundSpeed);
331
+ //#endregion
363
332
 
364
333
  isRunning = true;
334
+ started = new Date();
365
335
  app.setPluginStatus("Running");
366
336
  app.debug("Running");
367
337
 
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;
338
+ let lastSave = 0;
339
+ smoothedBoatSpeed.onChange = () => {
340
+ const wellUnderway = started < new Date() - 60 * 1000;
341
+ const minSpeed = SI.fromKnots(settings.speedStep / 2);
342
+ if (settings.estimateBoatSpeed) correct(wellUnderway);
343
+ if (settings.updateCorrectionTable && wellUnderway) {
344
+ updateTable(settings.assumeCurrent, minSpeed);
345
+ // Save correction table periodically
346
+ const now = new Date();
347
+ if (now - lastSave > 5 * 1000) {
348
+ saveTable(settings, table);
349
+ lastSave = now;
350
+ }
572
351
  }
573
- return false;
574
- }
352
+ };
575
353
  }
576
354
 
577
-
578
355
  plugin.stop = () => {
579
356
  return new Promise((resolve, reject) => {
580
357
  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;
358
+ smoothedHeading = smoothedHeading?.terminate(app);
359
+ stability = stability?.terminate?.(app);
360
+ smoothedAttitude = smoothedAttitude?.terminate(app);
361
+ rawCurrent = rawCurrent?.terminate(app);
362
+ smoothedCurrent = smoothedCurrent?.terminate?.(app);
363
+ smoothedBoatSpeed = smoothedBoatSpeed?.terminate(app);
592
364
  correctedBoatSpeed = correctedBoatSpeed?.terminate(app);
593
365
  boatSpeedRefGround = boatSpeedRefGround?.terminate(app);
594
- groundSpeed = groundSpeed?.terminate(app);
595
- groundSpeedStat = null;
366
+ smoothedGroundSpeed = smoothedGroundSpeed?.terminate(app);
596
367
  speedCorrection = speedCorrection?.terminate(app);
597
368
  residual = residual?.terminate(app);
598
369
  reportFull = null;
599
370
  reportVector = null;
600
371
  table = null;
372
+ rawHeading = null;
373
+ rawAttitude = null;
374
+ noCurrent = null;
375
+ rawBoatSpeed = null;
376
+ rawGroundSpeed = null;
377
+ started = null;
601
378
  app.setPluginStatus("Stopped");
602
379
  app.debug("Stopped");
603
380
 
@@ -608,5 +385,157 @@ module.exports = function (app) {
608
385
  }
609
386
  });
610
387
  };
388
+
389
+ /**
390
+ * Estimates and applies corrections to boat speed, leeway, and current.
391
+ *
392
+ * This function uses the current attitude, boat speed, and heading to compute a correction
393
+ * vector from the correction table, applies it to the observed boat speed, and sends the
394
+ * corrected value. If the vessel is well underway, it also estimates the current vector
395
+ * by comparing the corrected boat speed (rotated to heading) with the ground speed.
396
+ *
397
+ * @param {boolean} wellUnderway - True if the vessel is considered underway and current estimation should be performed.
398
+ */
399
+ function correct(wellUnderway) {
400
+ // prepare iteration
401
+ const heel = rawAttitude.value?.roll;
402
+ const speed = rawBoatSpeed.magnitude;
403
+ const theta = rawHeading.value;
404
+ //app.debug(`Heel: ${SI.toDegrees(heel).toFixed(1)}°, Speed: ${SI.toKnots(speed).toFixed(2)} kn, Heading: ${SI.toDegrees(theta).toFixed(1)}°`);
405
+ if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) {
406
+ //app.debug("Invalid input data");
407
+ return;
408
+ }
409
+
410
+ correctedBoatSpeed.copyFrom(rawBoatSpeed);
411
+ speedCorrection.setVectorValue({ x: 0, y: 0 }) ;
412
+ if (speed > 0 && !rawAttitude.stale ) {
413
+ const { correction, variance } = table.getCorrection(speed, heel);
414
+ speedCorrection.setVectorValue(correction, variance);
415
+ correctedBoatSpeed.add(speedCorrection);
416
+ }
417
+ Polar.send(app, plugin.id, [correctedBoatSpeed]);
418
+ // estimate current
419
+ if (wellUnderway && !rawGroundSpeed.stale) {
420
+ boatSpeedRefGround.copyFrom(correctedBoatSpeed);
421
+ boatSpeedRefGround.rotate(theta);
422
+ rawCurrent.copyFrom(rawGroundSpeed);
423
+ rawCurrent.substract(boatSpeedRefGround);
424
+ smoothedCurrent.sample();
425
+ }
426
+ PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
427
+ }
428
+
429
+ /**
430
+ * Updates the correction table and calculates the residual error.
431
+ *
432
+ * This function uses the current smoothed attitude, boat speed, and heading to update the correction table
433
+ * if the boat speed exceeds the specified minimum. It also calculates the residual between the measured ground speed
434
+ * and the expected ground speed (corrected boat speed plus current). The correction table is periodically saved.
435
+ *
436
+ * @param {boolean} [assumeCurrent=false] - If true, uses the smoothed current for correction; otherwise, assumes no current.
437
+ * @param {number} [minSpeed=0] - The minimum speed threshold (in SI units) required to update the correction table.
438
+ */
439
+ function updateTable(assumeCurrent = false, minSpeed = 0) {
440
+ // prepare iteration
441
+ const heel = smoothedAttitude.value.roll;
442
+ const speed = smoothedBoatSpeed.magnitude;
443
+ const theta = smoothedHeading.value;
444
+
445
+ if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) return;
446
+
447
+ // update correction table
448
+ if (speed > minSpeed && !smoothedAttitude.stale && !smoothedHeading.stale && !smoothedGroundSpeed.stale) {
449
+ table.update(speed, heel, smoothedGroundSpeed, assumeCurrent ? smoothedCurrent : noCurrent, smoothedBoatSpeed, theta);
450
+ }
451
+
452
+ // calculate residual
453
+ residual.copyFrom(smoothedGroundSpeed);
454
+ boatSpeedRefGround.copyFrom(correctedBoatSpeed);
455
+ boatSpeedRefGround.rotate(theta);
456
+ residual.substract(boatSpeedRefGround);
457
+ residual.substract(smoothedCurrent);
458
+
459
+ }
460
+
461
+ /**
462
+ * Checks if the correction table in settings matches the expected row and column configuration.
463
+ *
464
+ * This function compares the min, max, and step values of the row and column definitions
465
+ * in the current correction table against the provided row and col objects. Returns true
466
+ * if all values match, otherwise false. If the table is missing or not properly structured,
467
+ * returns false.
468
+ *
469
+ * @param {Object} row - The expected row configuration ({ min, max, step }).
470
+ * @param {Object} col - The expected column configuration ({ min, max, step }).
471
+ * @returns {boolean} True if the table matches the configuration, false otherwise.
472
+ */
473
+ function enforceConsistancy(options, row, col) {
474
+ if (!options?.correctionTable) return false;
475
+ table = options.correctionTable;
476
+ if (!table) return false;
477
+ if (!table.row) return false;
478
+ if (table.row.min != row.min) return false;
479
+ if (table.row.max != row.max) return false;
480
+ if (table.row.step != row.step) return false;
481
+ if (table.col.min != col.min) return false;
482
+ if (table.col.max != col.max) return false;
483
+ if (table.col.step != col.step) return false;
484
+ return true;
485
+ }
486
+
487
+ /**
488
+ * Loads or creates a correction table based on the provided options.
489
+ *
490
+ * This function checks if the correction table in the options matches the expected row and column configuration.
491
+ * If so, it loads the table from JSON; otherwise, it creates a new correction table with the specified parameters.
492
+ * The table is labeled and, if newly created, saved to the plugin options.
493
+ *
494
+ * @param {Object} options - The plugin options containing correction table settings and parameters.
495
+ * @returns {CorrectionTable} The loaded or newly created CorrectionTable instance.
496
+ */
497
+ function loadTable(options) {
498
+ const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
499
+ const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
500
+ let table;
501
+ const stability = (options.stability !== undefined) ? options.stability : 6;
502
+ const VAR_FLOOR = 1e-4;
503
+ if (enforceConsistancy(options, row, col) && !options.startWithNewTable) {
504
+ table = CorrectionTable.fromJSON(options.correctionTable, stability);
505
+ app.debug("Correction table loaded");
506
+ }
507
+ else if (options.correctionTable && !options.startWithNewTable) {
508
+ // Resample existing table to new dimensions conservatively (N=0, diag covariance with floor)
509
+ table = CorrectionTable.resampleFromJSON(options.correctionTable, row, col, stability, VAR_FLOOR);
510
+ app.debug("Correction table migrated to new dimensions");
511
+ options.startWithNewTable = false;
512
+ saveTable(options, table);
513
+ } else {
514
+ table = new CorrectionTable("correctionTable", row, col, stability);
515
+ app.debug("Correction table created");
516
+ // doStartFresh is not user-settable, so no need to reset it here
517
+ options.startWithNewTable = false;
518
+ saveTable(options, table);
519
+ }
520
+ table.setDisplayAttributes({ label: "correction table" });
521
+ return table;
522
+ }
523
+
524
+ /**
525
+ * Saves the correction table to the plugin options and logs the save event.
526
+ *
527
+ * This function serializes the provided correction table, updates the options object,
528
+ * and triggers the plugin's save mechanism. It returns the current date/time to indicate
529
+ * when the save occurred.
530
+ *
531
+ * @param {Object} options - The plugin options object to update.
532
+ * @param {CorrectionTable} correctionTable - The correction table instance to save.
533
+ * @returns {Date} The date and time when the table was saved.
534
+ */
535
+ function saveTable(options, correctionTable) {
536
+ options.correctionTable = correctionTable.toJSON();
537
+ app.savePluginOptions(options, () => { /*app.debug("Correction table saved")*/; });
538
+ }
539
+
611
540
  return plugin;
612
541
  };