speedandcurrent 1.3.0 → 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
- reportFullheading = null;
15
- reportFullattitude = null;
16
- reportFullcurrent = null;
17
- reportFullcurrentStat = null;
18
- reportFullboatSpeed = null;
19
- reportFullboatSpeedStat = null;
20
- reportFullcorrectedBoatSpeed = null;
21
- reportFullboatSpeedRefGround = null;
22
- reportFullgroundSpeed = null;
23
- reportFullgroundSpeedStat = null;
24
- reportFullspeedCorrection = null;
25
- reportFullresidual = null;
26
- reportFullreportFull = null;
27
- var reportVector = null;
28
- var table = 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;
29
+ let correctedBoatSpeed = null;
30
+ let boatSpeedRefGround = null;
31
+ let smoothedGroundSpeed = null;
32
+ let speedCorrection = null;
33
+ let residual = null;
34
+ let reportFull = null;
35
+ let reportVector = null;
36
+ let table = null;
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;
29
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,73 +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
-
252
- // make options survive to the stop function
253
- options = opts;
254
- loadPresets();
255
-
256
- // correction table
257
- table = loadTable(options);
225
+ plugin.start = (settings) => {
226
+ app.setPluginStatus("Starting");
227
+ app.debug("Starting");
228
+ table = loadTable(settings);
258
229
 
230
+ //#region Handler and Polar Initialization
231
+ let smootherOptions = { timeConstant: 1, processVariance: 1, measurementVariance: 20, timeSpan: 5 };
259
232
 
260
233
  // heading
261
- heading = new MessageHandler("heading", "navigation.headingTrue", options.headingSource);
262
- heading.subscribe(app, plugin.id, true);
263
- heading.setDisplayAttributes({ label: "heading" });
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;
264
240
 
265
- // attitude
266
- attitude = new MessageHandler("attitude", "navigation.attitude", options.attitudeSource);
267
- attitude.value = { pitch: 0, roll: 0, yaw: 0 }; //prevents errors when there is no attitude sensor
268
- attitude.setDisplayAttributes({ label: "attitude" });
269
- attitude.subscribe(app, plugin.id, true);
270
241
 
271
242
  // current
272
- current = new Polar("current", "self.environment.current.drift", "self.environment.current.setTrue");
273
- current.setDisplayAttributes({ label: "current", plane: "Ground" });
274
- current.setAngleRange('0to2pi');
275
- currentStat = new PolarDamped("currentDamped", current);
276
- currentStat.setAngleRange('0to2pi');
277
- currentStat.timeConstant = 60; // Set time constant for damping
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" });
278
249
  // Current should be initialised as no current
279
- current.setVectorValue({ x: 0, y: 0 });
280
- // There should be at least two samples, otherwise we can't calculate a valid speed
281
- currentStat.sample();
282
- 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]);
283
269
 
284
270
  // boat speed
285
- boatSpeed = new Polar("boatSpeed", "navigation.speedThroughWater", options.boatSpeedSource);
286
- boatSpeed.subscribe(app, plugin.id, true, false, !options.preventDuplication || !options.estimateBoatSpeed);
287
- boatSpeed.setDisplayAttributes({ label: "boat speed", plane: "Boat" });
288
- boatSpeedStat = new PolarDamped("boatSpeedDamped", boatSpeed);
289
- boatSpeedStat.setDisplayAttributes({ label: "boat speed (damped)", plane: "Boat" });
290
-
291
- correctedBoatSpeed = new Polar("correctedBoatSpeed", "navigation.speedThroughWater", "environment.wind.directionTruenavigation.leewayAngle");
271
+ smoothedBoatSpeed = new SmoothedSpeedThroughWater(app, plugin.id, settings.boatSpeedSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
272
+ rawBoatSpeed = smoothedBoatSpeed.polar;
273
+ correctedBoatSpeed = new Polar("correctedBoatSpeed", "navigation.speedThroughWater", "navigation.leewayAngle");
292
274
  correctedBoatSpeed.setDisplayAttributes({ label: "corrected boat speed", plane: "Boat" });
293
- //const correctedBoatSpeedStat = new PolarDamped("correctedBoatSpeedDamped", correctedBoatSpeed);
294
-
295
275
  boatSpeedRefGround = new Polar("boatSpeedRefGround");
296
- boatSpeedRefGround.setDisplayAttributes("boat speed over ground", "Ground");
297
-
276
+ boatSpeedRefGround.setDisplayAttributes("Boat speed over ground", "Ground");
277
+
298
278
  // ground speed
299
- groundSpeed = new Polar("groundSpeed", "navigation.speedOverGround", "navigation.courseOverGroundTrue", options.SOGSource, options.COGSource);
300
- groundSpeed.subscribe(app, plugin.id, true, true, true);
301
- groundSpeed.setDisplayAttributes({ label: "ground speed", plane: "Ground" });
302
- groundSpeed.setAngleRange('0to2pi');
303
- groundSpeedStat = new PolarDamped("groundSpeedDamped", groundSpeed);
304
- groundSpeedStat.setDisplayAttributes({ label: "ground speed (damped)", plane: "Ground" });
305
- groundSpeedStat.setAngleRange('0to2pi');
279
+ smoothedGroundSpeed = new SmoothedGroundSpeed(app, plugin.id, settings.SOGSource, true, MovingAverageSmoother, smootherOptions, !settings.preventDuplication);
280
+ rawGroundSpeed = smoothedGroundSpeed.polar;
306
281
 
307
282
  // correction vector
308
283
  speedCorrection = new Polar("speedCorrection");
@@ -313,326 +288,254 @@ module.exports = function (app) {
313
288
  residual = new Polar("residual");
314
289
  residual.setDisplayAttributes({ label: "residual", plane: "Ground" });
315
290
 
316
- // Make reporting object for webApp
291
+ //#endregion
292
+
293
+ //#region Reporting
317
294
  reportFull = new Reporter();
318
- reportFull.addDelta(heading);
319
- reportFull.addAttitude(attitude);
320
- reportFull.addPolar(groundSpeed);
321
- reportFull.addPolar(boatSpeed);
322
- //reportFull.addPolar(boatSpeedStat);
323
- //reportFull.addPolar(groundSpeedStat);
324
- reportFull.addPolar(speedCorrection);
325
- reportFull.addPolar(correctedBoatSpeed);
326
- reportFull.addPolar(current);
327
- 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
+ }
328
318
  reportFull.addTable(table);
329
319
 
320
+ // Make reporting object for webApp
321
+
322
+
330
323
  reportVector = new Reporter();
331
- reportVector.addDelta(heading);
324
+ reportVector.addDelta(rawHeading);
332
325
  reportVector.addPolar(residual);
333
326
  reportVector.addPolar(speedCorrection);
334
- reportVector.addPolar(current);
327
+ if (settings.assumeCurrent) reportVector.addPolar(smoothedCurrent);
335
328
  reportVector.addPolar(correctedBoatSpeed);
336
- reportVector.addPolar(boatSpeed);
337
- reportVector.addPolar(groundSpeed);
338
-
339
-
340
-
341
- app.debug("Analyzing input data");
342
- let candidates = [heading, attitude, boatSpeed, groundSpeed];
343
- // wait for all to settle
344
- setTimeout(() => {
345
- //app.debug(groundSpeed.magnitudeHandler);
346
- //app.debug(groundSpeed.angleHandler);
347
- // Check if all candidates get input data
348
- candidates.forEach(candidate => {
349
- if (candidate.lackingInputData()) {
350
- app.setPluginStatus("Halted: insufficient input data");
351
- app.debug("Insufficient input data for " + candidate.displayAttributes.label );
352
- return;
353
- }
354
- });
355
-
356
- // determing slowest input path and use this as a heartbeat for calculations
357
- candidates.sort((a, b) => (a.frequency || 0) - (b.frequency || 0));
358
-
359
-
360
-
361
- if (!candidates[0].lackingInputData()) {
362
- slowestInput = candidates[0];
363
- app.debug("Using " + slowestInput.displayAttributes.label + " as heartbeat for calculations ( " + Math.round(candidates[0].frequency) + " updates per second)");
364
- slowestInput.onChange = calculate;
365
- if (slowestInput == groundSpeed) {
366
- groundSpeed.onChange = () => {
367
- groundSpeedStat.sample();
368
- calculate();
369
- };
370
- }
371
- else {
372
- groundSpeed.onChange = () => {
373
- groundSpeedStat.sample();
374
- };
329
+ reportVector.addPolar(rawBoatSpeed);
330
+ reportVector.addPolar(rawGroundSpeed);
331
+ //#endregion
332
+
333
+ isRunning = true;
334
+ started = new Date();
335
+ app.setPluginStatus("Running");
336
+ app.debug("Running");
337
+
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;
375
350
  }
376
- if (slowestInput == boatSpeed) {
377
- boatSpeed.onChange = () => {
378
- boatSpeedStat.sample();
379
- calculate();
380
- };
381
- }
382
- else {
383
- boatSpeed.onChange = () => {
384
- boatSpeedStat.sample();
385
- };
386
- }
387
-
388
- isRunning = true;
389
- app.setPluginStatus("Running");
390
- app.debug("Running");
391
- }
392
-
393
- }, 5000);
394
-
395
-
396
-
397
-
398
-
399
- // Main function, estimates boatSpeed, leeway and current
400
- function calculate() {
401
- // prepare iteration
402
- heel = attitude.value.roll;
403
- speed = boatSpeed.magnitude;
404
- theta = heading.value;
405
- if (!Number.isFinite(heel)) {
406
- app.debug("Heel is not a valid number, skipping calculation");
407
- return;
408
- }
409
- if (!Number.isFinite(speed)) {
410
- app.debug("Speed is not a valid number, skipping calculation");
411
- return;
412
- }
413
- if (!Number.isFinite(theta)) {
414
- app.debug("Heading is not a valid number, skipping calculation");
415
- return;
416
- }
417
-
418
- // update correction table
419
- if (options.updateCorrectionTable) {
420
- table.update(speed, heel, groundSpeedStat, currentStat, boatSpeedStat, theta);
421
351
  }
352
+ };
353
+ }
422
354
 
423
- // correct and estimate boat speed
424
- estimateBoatSpeed();
355
+ plugin.stop = () => {
356
+ return new Promise((resolve, reject) => {
357
+ try {
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);
364
+ correctedBoatSpeed = correctedBoatSpeed?.terminate(app);
365
+ boatSpeedRefGround = boatSpeedRefGround?.terminate(app);
366
+ smoothedGroundSpeed = smoothedGroundSpeed?.terminate(app);
367
+ speedCorrection = speedCorrection?.terminate(app);
368
+ residual = residual?.terminate(app);
369
+ reportFull = null;
370
+ reportVector = null;
371
+ table = null;
372
+ rawHeading = null;
373
+ rawAttitude = null;
374
+ noCurrent = null;
375
+ rawBoatSpeed = null;
376
+ rawGroundSpeed = null;
377
+ started = null;
378
+ app.setPluginStatus("Stopped");
379
+ app.debug("Stopped");
425
380
 
426
- if (options.assumeCurrent) {
427
- // estimate current ;
428
- estimateCurrent();
381
+ isRunning = false;
382
+ resolve();
383
+ } catch (error) {
384
+ reject(error);
429
385
  }
430
- calcResidual();
431
-
432
-
433
- // Send calculated delta's
434
- updates = [];
435
- if (options.estimateCurrent) updates.push(current);
436
- if (options.estimateBoatSpeed) updates.push(correctedBoatSpeed);
437
- if (updates.length) Polar.send(app, plugin.id, updates);
386
+ });
387
+ };
438
388
 
439
- // Save correction table
440
- // periodically
441
- if (options.updateCorrectionTable && new Date() - lastSave > 5 * 60 * 1000) {
442
- lastSave = saveTable(options, table);
443
- }
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;
444
408
  }
445
409
 
446
- // estimates boat speed from observed boatSpeed and correction
447
- function estimateBoatSpeed() {
448
- cor = table.getKalmanCorrection(speed, heel);
449
- correctedBoatSpeed.copyFrom(boatSpeed);
450
- speedCorrection.setVectorValue(cor);
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);
451
415
  correctedBoatSpeed.add(speedCorrection);
452
416
  }
453
-
454
- // estimates current from observed groundspeed and estimated boatspeed
455
- function estimateCurrent() {
417
+ Polar.send(app, plugin.id, [correctedBoatSpeed]);
418
+ // estimate current
419
+ if (wellUnderway && !rawGroundSpeed.stale) {
456
420
  boatSpeedRefGround.copyFrom(correctedBoatSpeed);
457
421
  boatSpeedRefGround.rotate(theta);
458
- current.copyFrom(groundSpeed);
459
- current.substract(boatSpeedRefGround);
460
- currentStat.sample();
461
- current.copyFrom(currentStat);
422
+ rawCurrent.copyFrom(rawGroundSpeed);
423
+ rawCurrent.substract(boatSpeedRefGround);
424
+ smoothedCurrent.sample();
462
425
  }
426
+ PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
427
+ }
463
428
 
464
- // Calculate what ground speed is observed that is not attributed to estimated boat speed or estimated current
465
- function calcResidual() {
466
- residual.copyFrom(groundSpeed);
467
- boatSpeedRefGround.copyFrom(correctedBoatSpeed);
468
- boatSpeedRefGround.rotate(theta);
469
- residual.substract(boatSpeedRefGround);
470
- residual.substract(current);
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);
471
450
  }
472
451
 
473
- // Make sure the table matches the settings
474
- function enforceConsistancy(row, col) {
475
- // function is bugged due to IS units vs knots and degrees
476
- if (!options?.correctionTable) return false;
477
- table = options.correctionTable;
478
- if (!table) return false;
479
- if (!table.row) return false;
480
- if (table.row.min != row.min) return false;
481
- if (table.row.max != row.max) return false;
482
- if (table.row.step != row.step) return false;
483
- if (table.col.min != col.min) return false;
484
- if (table.col.max != col.max) return false;
485
- if (table.col.step != col.step) return false;
486
- return true;
487
- }
452
+ // calculate residual
453
+ residual.copyFrom(smoothedGroundSpeed);
454
+ boatSpeedRefGround.copyFrom(correctedBoatSpeed);
455
+ boatSpeedRefGround.rotate(theta);
456
+ residual.substract(boatSpeedRefGround);
457
+ residual.substract(smoothedCurrent);
488
458
 
489
- function loadTable(options) {
490
- const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
491
- const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
492
- let table;
493
- if (enforceConsistancy(row, col) && !options.doStartFresh) {
494
- table = CorrectionTable.fromJSON(options.correctionTable, options.correctionStability);
495
- app.debug("Correction table loaded");
496
- lastSave = new Date();
497
- }
498
- else {
499
- table = new CorrectionTable("correctionTable", row, col, options.correctionStability);
500
- app.debug("Correction table created");
501
- options.doStartFresh = false;
502
- lastSave = saveTable(options, table);
503
- }
504
- table.setDisplayAttributes({ label: "correction table" });
505
- return table;
506
- }
459
+ }
507
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
+ }
508
486
 
509
- function loadPresets() {
510
- const modeEnum = plugin.schema.properties.mode.enum;
511
- app.debug("Loading presests for mode: " + options.mode);
512
-
513
- switch (modeEnum.indexOf(options.mode)) {
514
- case -1:
515
- app.debug("Invalid mode");
516
- app.debug(options);
517
- //options.mode = modeEnum[2];
518
- break;
519
- case 0:
520
- // new table, no current
521
- options.doStartFresh = true;
522
- options.updateCorrectionTable = true;
523
- options.estimateBoatSpeed = false;
524
- options.assumeCurrent = false;
525
- options.estimateCurrent = false;
526
- options.correctionStability = 5;
527
- options.mode = modeEnum[2];
528
- break;
529
- case 1:
530
- // new table, with current
531
- options.doStartFresh = true;
532
- options.updateCorrectionTable = true;
533
- options.estimateBoatSpeed = false;
534
- options.assumeCurrent = true;
535
- options.estimateCurrent = false;
536
- options.correctionStability = 5;
537
- options.currentStability = 7;
538
- options.mode = modeEnum[3];
539
- break;
540
- case 2:
541
- // fresh table, no current
542
- options.doStartFresh = false;
543
- options.updateCorrectionTable = true;
544
- options.estimateBoatSpeed = false;
545
- options.assumeCurrent = false;
546
- options.estimateCurrent = false;
547
- options.correctionStability = 6;
548
- break;
549
- case 3:
550
- // fresh table, with current
551
- options.doStartFresh = false;
552
- options.updateCorrectionTable = true;
553
- options.estimateBoatSpeed = false;
554
- options.assumeCurrent = true;
555
- options.estimateCurrent = false;
556
- options.correctionStability = 6;
557
- options.currentStability = 6;
558
- break;
559
- case 4:
560
- // mature table, no current
561
- options.doStartFresh = false;
562
- options.updateCorrectionTable = true;
563
- options.estimateBoatSpeed = true;
564
- options.assumeCurrent = false;
565
- options.estimateCurrent = false;
566
- options.correctionStability = 8;
567
- break;
568
- case 5:
569
- // mature table, with current
570
- options.doStartFresh = false;
571
- options.updateCorrectionTable = true;
572
- options.estimateBoatSpeed = true;
573
- options.assumeCurrent = true;
574
- options.estimateCurrent = true;
575
- options.correctionStability = 8;
576
- options.currentStability = 3;
577
- break;
578
- case 6:
579
- // locked table
580
- options.doStartFresh = false;
581
- options.updateCorrectionTable = false;
582
- options.estimateBoatSpeed = true;
583
- options.assumeCurrent = true;
584
- options.estimateCurrent = true;
585
- options.currentStability = 2;
586
- break;
587
- case 7:
588
- // all manual configuration;
589
- break;
590
- }
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");
591
506
  }
592
-
593
- // compare floats
594
- function isEqual(a, b) {
595
- const TOLERANCE = 1e-5;
596
- if (Math.abs(a - b) < TOLERANCE) {
597
- return true;
598
- }
599
- return false;
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);
600
519
  }
520
+ table.setDisplayAttributes({ label: "correction table" });
521
+ return table;
601
522
  }
602
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
+ }
603
539
 
604
- plugin.stop = () => {
605
- return new Promise((resolve, reject) => {
606
- try {
607
- //corrTable.save(app);
608
- if (corrTable != null) {
609
- lastSave = saveTable(options, corrTable);
610
- }
611
-
612
- heading = null;
613
- attitude = null;
614
- current = null;
615
- currentStat = null;
616
- boatSpeed = null;
617
- boatSpeedStat = null;
618
- correctedBoatSpeed = null;
619
- boatSpeedRefGround = null;
620
- groundSpeed = null;
621
- groundSpeedStat = null;
622
- speedCorrection = null;
623
- residual = null;
624
- reportFull = null;
625
- reportVector = null;
626
- table =
627
- app.setPluginStatus("Stopped");
628
- app.debug("Stopped");
629
-
630
- isRunning = false;
631
- resolve();
632
- } catch (error) {
633
- reject(error);
634
- }
635
- });
636
- };
637
540
  return plugin;
638
541
  };