speedandcurrent 1.7.3 → 1.7.5

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/README.md CHANGED
@@ -32,7 +32,7 @@ After the initial ~60 second stabilization period the plugin starts sending the
32
32
  | Water current set (direction) | self.environment.current.setTrue | rad | True direction (set) the current flows toward. |
33
33
 
34
34
  Boat speed overwrite / duplication behavior:
35
- - When the setting "Prevent duplication of boat speed" = true (default): plugin overwrites navigation.speedThroughWater with corrected value.
35
+ - When the setting "Prevent speed duplication" = true (default): plugin overwrites navigation.speedThroughWater with corrected value.
36
36
  - When false: both raw and corrected updates exist under navigation.speedThroughWater with distinct source attributes (viewable in deltas / data browser) allowing comparison.
37
37
 
38
38
  Current vector representation: Published as polar (drift + set). Set angle is the direction the water moves toward (oceanographic convention). Verify tooling expectations if it assumes FROM direction.
@@ -60,7 +60,7 @@ All settings are managed from the plugin webapp. The Signal K admin UI is not us
60
60
  | Show Statistics | Off | Displays σ (standard deviation) next to smoothed values. | Enable for debugging or to inspect signal quality. |
61
61
 
62
62
  ### Smoother
63
- Controls how raw sensor signals are smoothed before being fed into the correction and learning calculations. The smoother type and its parameter apply to heading, boat speed, ground speed, and attitude.
63
+ Controls how raw sensor signals are smoothed before being fed into the learning calculations. The smoother type and its parameter apply to heading, boat speed, ground speed, and attitude.
64
64
 
65
65
  | Setting | Default | Description |
66
66
  |---------|---------|-------------|
@@ -93,8 +93,8 @@ Leave a source blank or set to *(any)* to accept values from all sources. If mul
93
93
  ### Output Behavior
94
94
  | Setting | Default | Description | Notes |
95
95
  |---------|---------|-------------|-------|
96
- | Prevent duplication of boat speed | On | Overwrites navigation.speedThroughWater with corrected value. | Disable to compare raw vs corrected via distinct source attributes under the same path. |
97
- | SOG fallback | Off | Copy navigation.speedOverGround (GPS) to navigation.speedThroughWater (Paddle wheel) if paddle wheel malfunction is detected. | SOG fallback activates when navigation.speedThroughWater = 0 and navigation.speedOverGround > treshold.|
96
+ | Prevent speed duplication | On | Overwrites navigation.speedThroughWater with corrected value. | Disable to compare raw vs corrected via distinct source attributes under the same path. |
97
+ | Groundspeed fallback | Off | Copy navigation.speedOverGround (GPS) to navigation.speedThroughWater (Paddle wheel) if paddle wheel malfunction is detected. | Activates when navigation.speedThroughWater = 0 and navigation.speedOverGround > threshold. |
98
98
 
99
99
  ### Tips
100
100
  1. Be patient with the correction table. Its corrections will improve over time.
@@ -116,7 +116,7 @@ The "Assume Current" option is experimental. It works best on a stable correctio
116
116
  ## WebApp Usage Guide
117
117
  Open via Signal K Server: Apps → Speed and current.
118
118
 
119
- The webapp has a collapsible sidebar with four sections, and a status message in the top-right of the nav bar (blue = stabilizing, amber = SOG fallback active, red = error or stopped).
119
+ The webapp has a collapsible sidebar with four sections, and a status message in the top-right of the nav bar (blue = stabilizing, amber = SOG fallback active, red = error or stopped). All settings changes take effect immediately — no plugin restart required.
120
120
 
121
121
  ### Inputs
122
122
  The **Inputs** section contains two sub-sections:
@@ -125,17 +125,16 @@ The **Inputs** section contains two sub-sections:
125
125
 
126
126
  ### Boatspeed Estimation
127
127
  The **Boatspeed Estimation** section has an On/Off toggle at the top of the card and contains:
128
- - **Settings**: `SOG fallback` and `Prevent duplication of boat speed`.
128
+ - **Settings**: `Groundspeed fallback` and `Prevent speed duplication`.
129
129
  - **Inputs**: The raw signal values used for correction (heading, raw STW, SOG, attitude).
130
- - **Intermediates**: Internally derived quantities such as the correction vector and leeway.
131
- - **Outputs**: Corrected STW, leeway angle, and estimated current.
130
+ - **Intermediates**: Internally derived quantities including the speed correction vector, boat speed over ground, and residual.
131
+ - **Outputs**: Corrected boatspeed & leeway, and estimated current.
132
132
 
133
133
  ### Correction Table Learning
134
134
  The **Correction Table Learning** section has an On/Off toggle and contains:
135
135
  - **Settings**: Stability, Assume Current, and Show Statistics (σ display toggle).
136
136
  - **Smoother**: Type selector (Moving average / Exponential / Kalman) and its single tuning parameter. Only the parameter relevant to the selected smoother is shown.
137
137
  - **Inputs**: Smoothed values fed into the learning algorithm. When Show Statistics is on, σ values are shown.
138
- - **Intermediates**: Derived observations used to update correction table cells.
139
138
 
140
139
  ### Correction Table
141
140
  The **Correction Table** section shows the current correction table grid. Table management buttons appear top-right:
@@ -146,11 +145,11 @@ The **Correction Table** section shows the current correction table grid. Table
146
145
 
147
146
  Each populated cell shows:
148
147
  - **Factor** (±%): how much the paddle wheel over- or under-reads at this speed and heel (green = reads slow, orange = reads fast).
149
- - **Leeway** (°): the estimated sideways drift angle at this speed and heel.
148
+ - **Leeway** (°): the estimated sideways drift angle; also encoded visually as the stripe direction in the cell background.
150
149
 
151
150
  Cell highlighting:
152
- - **Bold border**: the most recently updated table cell.
153
- - **Faint blue tint**: cells contributing to the current interpolation. Deeper = higher weight.
151
+ - **Blue outline**: the most recently updated table cell.
152
+ - **Dashed outline**: cells contributing to the current interpolation.
154
153
  - **No content**: not yet populated by learning.
155
154
 
156
155
  ### Correction Table Health Checklist
package/index.js CHANGED
@@ -33,7 +33,6 @@ module.exports = function (app) {
33
33
  assumeCurrent: false,
34
34
  headingSource: ' ',
35
35
  boatSpeedSource: ' ',
36
- COGSource: ' ',
37
36
  SOGSource: ' ',
38
37
  attitudeSource: ' ',
39
38
  preventDuplication: true,
@@ -45,8 +44,7 @@ module.exports = function (app) {
45
44
  showStatistics: false
46
45
  };
47
46
 
48
- const isReadyAndFresh = (input) => !!input && input.ready === true && !input.stale;
49
-
47
+
50
48
 
51
49
  function readOptions() {
52
50
  const stored = app.readPluginOptions();
@@ -67,6 +65,11 @@ module.exports = function (app) {
67
65
  saveOptions();
68
66
  }
69
67
 
68
+ // only allow prevent duplication when correctedBoatSpeed is enabled, otherwise it would prevent any boat speed from being published at all
69
+ const preventDuplication = () => {
70
+ return options.preventDuplication && options.estimateBoatSpeed;
71
+ }
72
+
70
73
  /**
71
74
  * Derives { SmootherClass, smootherOptions } from the current options.
72
75
  * Enforces minimums so the smoother always has at least two observations for
@@ -99,6 +102,7 @@ module.exports = function (app) {
99
102
 
100
103
  function swapTable(newTable) {
101
104
  table = newTable;
105
+ minSpeed = table.step[0] / 2;
102
106
  if (reportFull) reportFull.setTables([table]);
103
107
  }
104
108
 
@@ -111,12 +115,12 @@ module.exports = function (app) {
111
115
  let smoothedCurrent = null;
112
116
  let smoothedBoatSpeed = null;
113
117
  let correctedBoatSpeed = null;
118
+ let lrnBoatSpeed = null;
114
119
  let boatSpeedRefGround = null;
115
120
  let smoothedGroundSpeed = null;
116
121
  let speedCorrection = null;
117
122
  let residual = null;
118
123
  let reportFull = null;
119
- let reportVector = null;
120
124
  let table = null;
121
125
 
122
126
  // add missing declarations
@@ -126,6 +130,7 @@ module.exports = function (app) {
126
130
  let rawBoatSpeed = null;
127
131
  let rawGroundSpeed = null;
128
132
  let started = null;
133
+ let minSpeed = 0;
129
134
 
130
135
  const plugin = {};
131
136
  plugin.id = "SpeedAndCurrent";
@@ -159,13 +164,7 @@ module.exports = function (app) {
159
164
  }
160
165
  });
161
166
 
162
- router.get('/getVectors', (req, res) => {
163
- if (!isRunning) {
164
- res.status(503).json({ error: "Plugin is not running" });
165
- } else {
166
- res.json(reportVector.report());
167
- }
168
- });
167
+
169
168
 
170
169
  router.get('/api/status', (req, res) => {
171
170
  res.json({ status: pluginStatus, isRunning });
@@ -300,6 +299,7 @@ module.exports = function (app) {
300
299
  const tableName = options.tableName || 'correctionTable';
301
300
  const tableFilePath = path.join(app.getDataDirPath(), tableName + '.json');
302
301
  table = loadTable(options, tableFilePath);
302
+ minSpeed = table.step[0] / 2;
303
303
 
304
304
  //#region Handler and Polar Initialization
305
305
  const { SmootherClass, smootherOptions } = resolveSmootherConfig();
@@ -369,21 +369,34 @@ module.exports = function (app) {
369
369
  }
370
370
  });
371
371
 
372
+ // boatspeed
373
+ smoothedBoatSpeed = createSmoothedHandler({
374
+ app, pluginId: plugin.id,
375
+ id: 'boatSpeed',
376
+ path: 'navigation.speedThroughWater',
377
+ source: options.boatSpeedSource,
378
+ passOn: !preventDuplication(),
379
+ subscribe: true,
380
+ SmootherClass,
381
+ smootherOptions
382
+ });
383
+ rawBoatSpeed = smoothedBoatSpeed.handler;
384
+
385
+ // Learning polar — used for updating the correction table
386
+ lrnBoatSpeed = new Polar(app, plugin.id, "lrnBoatSpeed");
387
+ lrnBoatSpeed.configureMagnitude("navigation.speedThroughWater");
388
+ lrnBoatSpeed.configureAngle("navigation.leewayAngle");
389
+ lrnBoatSpeed.setMeta({ displayName: "Learning boat speed", plane: "Boat" });
390
+ lrnBoatSpeed.setAngleRange('-piToPi');
372
391
 
373
- // boat speed — STW magnitude is optionally forwarded; leeway angle passOn is always false
374
- const rawBoatSpeedPolar = new Polar(app, plugin.id, 'boatSpeed');
375
- rawBoatSpeedPolar.configureMagnitude('navigation.speedThroughWater', options.boatSpeedSource, !options.preventDuplication);
376
- rawBoatSpeedPolar.configureAngle('navigation.leewayAngle', options.boatSpeedSource, false);
377
- rawBoatSpeedPolar.setMeta({ displayName: 'Boat speed', plane: 'Boat' });
378
- rawBoatSpeedPolar.setAngleRange('-piToPi');
379
- rawBoatSpeedPolar.subscribe(true, false);
380
- smoothedBoatSpeed = new PolarSmoother(rawBoatSpeedPolar, SmootherClass, smootherOptions);
381
- rawBoatSpeedPolar.onChange = () => { smoothedBoatSpeed.sample(); };
382
- rawBoatSpeed = rawBoatSpeedPolar;
392
+
393
+ // corrected boatspeed holds both corrected boatspeed and estimated leeway
383
394
  correctedBoatSpeed = new Polar(app, plugin.id, "correctedBoatSpeed");
384
395
  correctedBoatSpeed.configureMagnitude("navigation.speedThroughWater");
385
396
  correctedBoatSpeed.configureAngle("navigation.leewayAngle");
386
397
  correctedBoatSpeed.setMeta({ displayName: "Corrected boatspeed / Leeway", plane: "Boat" });
398
+
399
+ // boatspeed vector in ground frame, used for current estimation and residual calculation
387
400
  boatSpeedRefGround = new Polar(app, plugin.id, "boatSpeedRefGround");
388
401
  boatSpeedRefGround.setMeta({ displayName: "Boat speed over ground", plane: "Ground" });
389
402
 
@@ -418,34 +431,25 @@ module.exports = function (app) {
418
431
  if (options.estimateBoatSpeed) {
419
432
  reportFull.addDelta(rawHeading);
420
433
  reportFull.addAttitude(rawAttitude);
421
- reportFull.addPolar(rawBoatSpeed);
434
+ reportFull.addDelta(rawBoatSpeed);
422
435
  reportFull.addPolar(speedCorrection);
423
436
  reportFull.addPolar(boatSpeedRefGround);
424
437
  reportFull.addPolar(correctedBoatSpeed);
425
438
  reportFull.addPolar(rawGroundSpeed);
426
439
  reportFull.addPolar(smoothedCurrent);
440
+ reportFull.addPolar(residual);
427
441
  }
428
442
  if (options.updateCorrectionTable) {
429
443
  reportFull.addDelta(smoothedHeading);
430
444
  reportFull.addAttitude(smoothedAttitude);
431
- reportFull.addPolar(smoothedBoatSpeed);
445
+ reportFull.addDelta(smoothedBoatSpeed);
432
446
  reportFull.addPolar(smoothedGroundSpeed);
433
447
  if (options.assumeCurrent) {
434
448
  reportFull.addPolar(smoothedCurrent);
435
449
  }
436
- reportFull.addPolar(residual);
437
450
  }
438
451
  reportFull.addTable(table);
439
452
 
440
- // Make reporting object for webApp
441
- reportVector = new Reporter();
442
- reportVector.addDelta(rawHeading);
443
- reportVector.addPolar(residual);
444
- reportVector.addPolar(speedCorrection);
445
- if (options.assumeCurrent) reportVector.addPolar(smoothedCurrent);
446
- reportVector.addPolar(correctedBoatSpeed);
447
- reportVector.addPolar(rawBoatSpeed);
448
- reportVector.addPolar(rawGroundSpeed);
449
453
  //#endregion
450
454
 
451
455
  isRunning = true;
@@ -459,24 +463,14 @@ module.exports = function (app) {
459
463
  if (Object.keys(changedOptions).length) applyOptionChanges();
460
464
 
461
465
  const wellUnderway = started < new Date() - 60 * 1000;
462
- const minSpeed = table.step[0] / 2;
463
466
 
464
- if (options.sogFallback && fallingBackToSog(minSpeed)) {
465
- setStatus('Falling back to Speed Over Ground');
466
- app.debug("Falling back");
467
- return;
468
- }
469
- if (!wellUnderway) {
470
- setStatus('Stabilizing');
471
- } else {
472
- setStatus('Running');
473
- }
467
+ setStatus(wellUnderway ? 'Running' : 'Stabilizing');
474
468
  if (options.estimateBoatSpeed) correct(wellUnderway);
475
469
  if (options.updateCorrectionTable && wellUnderway) {
476
- updateTable(options.assumeCurrent, minSpeed);
470
+ updateTable();
477
471
  // Save correction table periodically
478
472
  const now = new Date();
479
- if (now - lastSave > 5 * 1000) {
473
+ if (now - lastSave > 60 * 1000) {
480
474
  saveTable(table, path.join(app.getDataDirPath(), table.id + '.json'));
481
475
  lastSave = now;
482
476
  }
@@ -494,12 +488,12 @@ module.exports = function (app) {
494
488
  smoothedCurrent = smoothedCurrent?.terminate?.();
495
489
  smoothedBoatSpeed = smoothedBoatSpeed?.terminate();
496
490
  correctedBoatSpeed = correctedBoatSpeed?.terminate();
491
+ lrnBoatSpeed = lrnBoatSpeed?.terminate();
497
492
  boatSpeedRefGround = boatSpeedRefGround?.terminate();
498
493
  smoothedGroundSpeed = smoothedGroundSpeed?.terminate();
499
494
  speedCorrection = speedCorrection?.terminate();
500
495
  residual = residual?.terminate();
501
496
  reportFull = null;
502
- reportVector = null;
503
497
  table = null;
504
498
  rawHeading = null;
505
499
  rawAttitude = null;
@@ -519,50 +513,56 @@ module.exports = function (app) {
519
513
  });
520
514
  };
521
515
 
522
- function fallingBackToSog( minSpeed) {
523
- if (!rawBoatSpeed.stale && rawBoatSpeed.magnitude > 0) return false;
524
- if (rawGroundSpeed.stale || rawGroundSpeed.magnitude < minSpeed) return false;
525
- // Fallback to SOG
526
- correctedBoatSpeed.setVectorValue({ x: rawGroundSpeed.magnitude, y: 0 });
527
- Polar.send(app, plugin.id, [correctedBoatSpeed]);
528
- return true;
529
- }
530
-
531
- /**
532
- * Estimates and applies corrections to boat speed, leeway, and current.
516
+ /**
517
+ * Corrects and publishes boat speed. Handles SOG fallback and missing-input
518
+ * silent fallback so that navigation.speedThroughWater is always written
519
+ * when estimateBoatSpeed is on, as long as any usable speed source exists.
533
520
  *
534
- * This function uses the current attitude, boat speed, and heading to compute a correction
535
- * vector from the correction table, applies it to the observed boat speed, and sends the
536
- * corrected value. If the vessel is well underway, it also estimates the current vector
537
- * by comparing the corrected boat speed (rotated to heading) with the ground speed.
521
+ * Priority order:
522
+ * 1. STW zero + sogFallback enabled + SOG available → publish SOG magnitude
523
+ * 2. attitude/heading not ready → publish raw STW unchanged (silent fallback)
524
+ * 3. All inputs ready → apply table correction; estimate current if wellUnderway
538
525
  *
539
- * @param {boolean} wellUnderway - True if the vessel is considered underway and current estimation should be performed.
526
+ * @param {boolean} wellUnderway - Gates current estimation (requires 60 s settling).
540
527
  */
541
528
  function correct(wellUnderway) {
542
- // prepare iteration
543
- if (!isReadyAndFresh(rawAttitude) || !isReadyAndFresh(rawBoatSpeed) || !isReadyAndFresh(rawHeading)) return;
544
- const heel = rawAttitude.value?.roll;
545
- const speed = rawBoatSpeed.magnitude;
546
- const theta = rawHeading.value;
547
- //app.debug(`Heel: ${SI.toDegrees(heel).toFixed(1)}°, Speed: ${SI.toKnots(speed).toFixed(2)} kn, Heading: ${SI.toDegrees(theta).toFixed(1)}°`);
529
+ // copy the boatspeed delta to polar
530
+ correctedBoatSpeed.setVectorValue({ x: rawBoatSpeed.value, y: 0 });
548
531
 
549
- correctedBoatSpeed.copyFrom(rawBoatSpeed);
550
- speedCorrection.setVectorValue({ x: 0, y: 0 }) ;
551
- if (speed > 0 ) {
552
- const { correction, variance } = table.getCorrection(speed, heel);
553
- speedCorrection.setVectorValue(correction, variance);
554
- correctedBoatSpeed.add(speedCorrection);
532
+ speedCorrection.setVectorValue({ x: 0, y: 0 });
533
+
534
+ // Priority 1: SOG fallback when STW is stuck at zero
535
+ // (onChange only fires when STW is ready, so a missing STW cannot reach here)
536
+ if (options.sogFallback && rawGroundSpeed.ready && correctedBoatSpeed.magnitude === 0 && rawGroundSpeed.magnitude >= minSpeed) {
537
+ correctedBoatSpeed.setVectorValue({ x: rawGroundSpeed.magnitude, y: 0 });
555
538
  }
556
- Polar.send(app, plugin.id, [correctedBoatSpeed]);
557
- // estimate current
558
- if (wellUnderway && isReadyAndFresh(rawGroundSpeed)) {
559
- boatSpeedRefGround.copyFrom(correctedBoatSpeed);
560
- boatSpeedRefGround.rotate(theta);
561
- rawCurrent.copyFrom(rawGroundSpeed);
562
- rawCurrent.substract(boatSpeedRefGround);
563
- smoothedCurrent.sample();
539
+ else if (rawAttitude.ready) {
540
+ if (correctedBoatSpeed.magnitude > 0) {
541
+ const { correction, variance } = table.getCorrection(correctedBoatSpeed.magnitude, rawAttitude.value?.roll);
542
+ speedCorrection.setVectorValue(correction, variance);
543
+ correctedBoatSpeed.add(speedCorrection);
544
+ }
545
+ // Current estimation and residual also require heading (to rotate into ground frame)
546
+ if (rawHeading.ready && rawGroundSpeed.ready) {
547
+ boatSpeedRefGround.copyFrom(correctedBoatSpeed);
548
+ boatSpeedRefGround.rotate(rawHeading.value);
549
+ // Current estimation gated by wellUnderway (smoothers need to settle first)
550
+ if (wellUnderway) {
551
+ rawCurrent.copyFrom(rawGroundSpeed);
552
+ rawCurrent.substract(boatSpeedRefGround);
553
+ smoothedCurrent.sample();
554
+ }
555
+ // Residual: how well correctedBoatSpeed + smoothedCurrent explains groundSpeed
556
+ residual.copyFrom(rawGroundSpeed);
557
+ residual.substract(boatSpeedRefGround);
558
+ residual.substract(smoothedCurrent);
559
+ }
564
560
  }
561
+ // Implicit fallback: attitude not ready — correctedBoatSpeed = raw STW, no correction
562
+
565
563
  PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
564
+
565
+ Polar.send(app, plugin.id, [correctedBoatSpeed]);
566
566
  }
567
567
 
568
568
  /**
@@ -575,25 +575,15 @@ module.exports = function (app) {
575
575
  * @param {boolean} [assumeCurrent=false] - If true, uses the smoothed current for correction; otherwise, assumes no current.
576
576
  * @param {number} [minSpeed=0] - The minimum speed threshold (in SI units) required to update the correction table.
577
577
  */
578
- function updateTable(assumeCurrent = false, minSpeed = 0) {
579
- if (!isReadyAndFresh(smoothedAttitude) || !isReadyAndFresh(smoothedBoatSpeed) || !isReadyAndFresh(smoothedHeading) || !isReadyAndFresh(smoothedGroundSpeed)) return;
580
- // prepare iteration
581
- const heel = smoothedAttitude.value.roll;
582
- const speed = smoothedBoatSpeed.magnitude;
583
- const theta = smoothedHeading.value;
584
-
585
- // update correction table
586
- if (speed > minSpeed ) {
587
- table.update(speed, heel, smoothedGroundSpeed, assumeCurrent ? smoothedCurrent : noCurrent, smoothedBoatSpeed, theta);
578
+ function updateTable() {
579
+ lrnBoatSpeed.setVectorValue({ x: smoothedBoatSpeed.value, y: 0 });
580
+ if (!smoothedAttitude.ready || !smoothedBoatSpeed.ready || !smoothedHeading.ready || !smoothedGroundSpeed.ready || (options.assumeCurrent ? !smoothedCurrent.ready : !noCurrent.ready)) return;
581
+
582
+ // update correction table
583
+ if (smoothedBoatSpeed.value > minSpeed) {
584
+ table.update(smoothedBoatSpeed.value, smoothedAttitude.value?.roll, smoothedGroundSpeed, options.assumeCurrent ? smoothedCurrent : noCurrent, lrnBoatSpeed, smoothedHeading.value);
588
585
  }
589
586
 
590
- // calculate residual
591
- residual.copyFrom(smoothedGroundSpeed);
592
- boatSpeedRefGround.copyFrom(correctedBoatSpeed);
593
- boatSpeedRefGround.rotate(theta);
594
- residual.substract(boatSpeedRefGround);
595
- residual.substract(smoothedCurrent);
596
-
597
587
  }
598
588
 
599
589
  /**
@@ -680,12 +670,15 @@ module.exports = function (app) {
680
670
  } else if (smoothedAttitude && key === 'attitudeSource') {
681
671
  smoothedAttitude.handler.source = value;
682
672
  } else if (smoothedBoatSpeed && key === 'boatSpeedSource') {
683
- smoothedBoatSpeed.polar.magnitudeHandler.source = value;
673
+ smoothedBoatSpeed.handler.source = value;
684
674
  } else if (smoothedGroundSpeed && key === 'SOGSource') {
685
675
  smoothedGroundSpeed.polar.magnitudeHandler.source = value;
686
676
  } else if (key === 'preventDuplication') {
687
- if (smoothedBoatSpeed) smoothedBoatSpeed.polar.magnitudeHandler.passOn = !value;
677
+ if (smoothedBoatSpeed) smoothedBoatSpeed.handler.passOn = !preventDuplication();
678
+ } else if (key === 'estimateBoatSpeed' ) {
679
+ if (smoothedBoatSpeed) smoothedBoatSpeed.handler.passOn = !preventDuplication();
688
680
  }
681
+
689
682
  // All other keys (sogFallback, estimateBoatSpeed, updateCorrectionTable,
690
683
  // assumeCurrent, stability, startWithNewTable, COGSource) are read
691
684
  // directly from options.* so no extra action needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speedandcurrent",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "kalman-filter": "^2.3.0",
34
- "signalkutilities": "^1.9.1"
34
+ "signalkutilities": "^1.11.0"
35
35
  },
36
36
  "files": [
37
37
  "index.js",
package/public/app.js CHANGED
@@ -121,6 +121,36 @@ function isStale(item) {
121
121
  return item.state?.stale === true;
122
122
  }
123
123
 
124
+ function isNotReady(item) {
125
+ if (!item) return true;
126
+ return item.state?.ready !== true;
127
+ }
128
+
129
+ function getAnyItem(id) {
130
+ return state.polarsById[id] || state.deltasById[id] || state.attitudesById[id] || null;
131
+ }
132
+
133
+ function renderWarnings(elId, ids) {
134
+ const el = document.getElementById(elId);
135
+ if (!el) return;
136
+ el.innerHTML = '';
137
+ const notReady = ids.filter(id => isNotReady(getAnyItem(id)));
138
+ if (notReady.length === 0) return;
139
+ const h = document.createElement('h6');
140
+ h.className = 'text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small';
141
+ h.textContent = 'Warnings';
142
+ el.appendChild(h);
143
+ const ul = document.createElement('ul');
144
+ ul.className = 'list-unstyled text-danger small ps-3';
145
+ notReady.forEach(id => {
146
+ const label = metaById[id]?.displayName ?? id;
147
+ const li = document.createElement('li');
148
+ li.textContent = `"${label}" — not available`;
149
+ ul.appendChild(li);
150
+ });
151
+ el.appendChild(ul);
152
+ }
153
+
124
154
  // ─── Config (live settings mirror) ───────────────────────────────────────────
125
155
  let config = null;
126
156
 
@@ -216,7 +246,7 @@ const paramMeta = {
216
246
  stability: { label: 'Stability (1–20)', type: 'number', min: 1, max: 20, step: 1, default: 7, description: 'How quickly the correction table adapts to new observations. Higher values mean slower, more stable changes.' },
217
247
  showStatistics: { label: 'Show statistics (σ)', type: 'boolean', description: 'Display standard deviation alongside smoothed values for debugging.' },
218
248
  headingSource: { label: 'Heading source', type: 'source', sourceOf: { type: 'delta', id: 'heading.smoothed' } },
219
- boatSpeedSource:{ label: 'Boat speed source', type: 'source', sourceOf: { type: 'polar', id: 'boatSpeed.smoothed' } },
249
+ boatSpeedSource:{ label: 'Boat speed source', type: 'source', sourceOf: { type: 'delta', id: 'boatSpeed.smoothed' } },
220
250
  SOGSource: { label: 'Groundspeed source', type: 'source', sourceOf: { type: 'polar', id: 'groundSpeed.smoothed' } },
221
251
  attitudeSource: { label: 'Attitude source', type: 'source', sourceOf: { type: 'attitude',id: 'attitude.smoothed' } },
222
252
  smootherClass: {
@@ -492,9 +522,9 @@ function renderGroupInto(elId, polars, deltas, attitudes) {
492
522
  if (!el) return;
493
523
  el.innerHTML = '';
494
524
  const rows = [
495
- ...polars .map(p => ({ label: itemLabel(p), value: formatPolarValue(p), stale: isStale(p) })),
496
- ...deltas .map(d => ({ label: itemLabel(d), value: formatDeltaValue(d), stale: isStale(d) })),
497
- ...attitudes.map(a => ({ label: itemLabel(a), value: formatAttitudeValue(a), stale: isStale(a) }))
525
+ ...polars .map(p => ({ label: itemLabel(p), value: formatPolarValue(p), stale: isNotReady(p) })),
526
+ ...deltas .map(d => ({ label: itemLabel(d), value: formatDeltaValue(d), stale: isNotReady(d) })),
527
+ ...attitudes.map(a => ({ label: itemLabel(a), value: formatAttitudeValue(a), stale: isNotReady(a) }))
498
528
  ];
499
529
  if (rows.length) el.appendChild(buildDataTable(rows));
500
530
  }
@@ -502,20 +532,20 @@ function renderGroupInto(elId, polars, deltas, attitudes) {
502
532
  function renderLiveSections() {
503
533
  // Inputs section — raw sensor readings only (smoothing is internal to the plugin)
504
534
  renderGroupInto('inputs-values',
505
- filterById(state.polarsAll, ['boatSpeed', 'groundSpeed']),
506
- filterById(state.deltasAll, ['heading.angle']),
535
+ filterById(state.polarsAll, ['groundSpeed']),
536
+ filterById(state.deltasAll, ['heading.angle', 'boatSpeed']),
507
537
  filterById(state.attitudesAll, ['attitude'])
508
538
  );
509
539
 
510
540
  // Estimation — inputs (raw sensor data used for boat speed estimation)
511
541
  renderGroupInto('estimation-inputs',
512
- filterById(state.polarsAll, ['boatSpeed', 'groundSpeed']),
513
- filterById(state.deltasAll, ['heading.angle']),
542
+ filterById(state.polarsAll, ['groundSpeed']),
543
+ filterById(state.deltasAll, ['heading.angle', 'boatSpeed']),
514
544
  filterById(state.attitudesAll, ['attitude'])
515
545
  );
516
546
  // Estimation — intermediates
517
547
  renderGroupInto('estimation-intermediates',
518
- filterById(state.polarsAll, ['boatSpeedRefGround', 'speedCorrection']),
548
+ filterById(state.polarsAll, ['boatSpeedRefGround', 'speedCorrection', 'residual']),
519
549
  [], []
520
550
  );
521
551
  // Estimation — outputs
@@ -523,21 +553,24 @@ function renderLiveSections() {
523
553
  filterById(state.polarsAll, ['correctedBoatSpeed', 'current.smoothed']),
524
554
  [], []
525
555
  );
556
+ // Estimation — warnings
557
+ renderWarnings('estimation-warnings',
558
+ ['boatSpeed', 'attitude', 'heading.angle', 'groundSpeed']
559
+ );
526
560
 
527
561
  // Learning — inputs: smoothed sensors + current if assumeCurrent
528
562
  const learningCurrentPolars = (config && config.assumeCurrent)
529
563
  ? filterById(state.polarsAll, ['current.smoothed'])
530
564
  : [];
531
565
  renderGroupInto('learning-inputs',
532
- [...filterById(state.polarsAll, ['boatSpeed.smoothed', 'groundSpeed.smoothed']), ...learningCurrentPolars],
533
- filterById(state.deltasAll, ['heading.smoothed']),
566
+ [...filterById(state.polarsAll, ['groundSpeed.smoothed']), ...learningCurrentPolars],
567
+ filterById(state.deltasAll, ['heading.smoothed', 'boatSpeed.smoothed']),
534
568
  filterById(state.attitudesAll, ['attitude.smoothed'])
535
569
  );
536
- // Learning — intermediates
537
- renderGroupInto('learning-intermediates',
538
- filterById(state.polarsAll, ['residual']),
539
- [], []
540
- );
570
+ // Learning — warnings
571
+ const learningWarningIds = ['boatSpeed.smoothed', 'groundSpeed.smoothed', 'heading.smoothed', 'attitude.smoothed'];
572
+ if (config && config.assumeCurrent) learningWarningIds.push('current.smoothed');
573
+ renderWarnings('learning-warnings', learningWarningIds);
541
574
  // Correction table
542
575
  const tableEl = document.getElementById('table-container');
543
576
  if (tableEl) {
package/public/index.html CHANGED
@@ -104,6 +104,7 @@
104
104
  <div id="estimation-intermediates"></div>
105
105
  <h6 class="text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small">Outputs</h6>
106
106
  <div id="estimation-outputs"></div>
107
+ <div id="estimation-warnings"></div>
107
108
  </div>
108
109
  </div>
109
110
  </div>
@@ -130,8 +131,7 @@
130
131
  </table>
131
132
  <h6 class="text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small">Inputs</h6>
132
133
  <div id="learning-inputs"></div>
133
- <h6 class="text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small">Intermediates</h6>
134
- <div id="learning-intermediates"></div>
134
+ <div id="learning-warnings"></div>
135
135
  </div>
136
136
  </div>
137
137
  </div>