speedandcurrent 1.7.2 → 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,6 +44,8 @@ module.exports = function (app) {
45
44
  showStatistics: false
46
45
  };
47
46
 
47
+
48
+
48
49
  function readOptions() {
49
50
  const stored = app.readPluginOptions();
50
51
  const raw = stored && stored.configuration ? stored.configuration : (stored || {});
@@ -64,6 +65,11 @@ module.exports = function (app) {
64
65
  saveOptions();
65
66
  }
66
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
+
67
73
  /**
68
74
  * Derives { SmootherClass, smootherOptions } from the current options.
69
75
  * Enforces minimums so the smoother always has at least two observations for
@@ -96,6 +102,7 @@ module.exports = function (app) {
96
102
 
97
103
  function swapTable(newTable) {
98
104
  table = newTable;
105
+ minSpeed = table.step[0] / 2;
99
106
  if (reportFull) reportFull.setTables([table]);
100
107
  }
101
108
 
@@ -108,12 +115,12 @@ module.exports = function (app) {
108
115
  let smoothedCurrent = null;
109
116
  let smoothedBoatSpeed = null;
110
117
  let correctedBoatSpeed = null;
118
+ let lrnBoatSpeed = null;
111
119
  let boatSpeedRefGround = null;
112
120
  let smoothedGroundSpeed = null;
113
121
  let speedCorrection = null;
114
122
  let residual = null;
115
123
  let reportFull = null;
116
- let reportVector = null;
117
124
  let table = null;
118
125
 
119
126
  // add missing declarations
@@ -123,6 +130,7 @@ module.exports = function (app) {
123
130
  let rawBoatSpeed = null;
124
131
  let rawGroundSpeed = null;
125
132
  let started = null;
133
+ let minSpeed = 0;
126
134
 
127
135
  const plugin = {};
128
136
  plugin.id = "SpeedAndCurrent";
@@ -156,13 +164,7 @@ module.exports = function (app) {
156
164
  }
157
165
  });
158
166
 
159
- router.get('/getVectors', (req, res) => {
160
- if (!isRunning) {
161
- res.status(503).json({ error: "Plugin is not running" });
162
- } else {
163
- res.json(reportVector.report());
164
- }
165
- });
167
+
166
168
 
167
169
  router.get('/api/status', (req, res) => {
168
170
  res.json({ status: pluginStatus, isRunning });
@@ -297,6 +299,7 @@ module.exports = function (app) {
297
299
  const tableName = options.tableName || 'correctionTable';
298
300
  const tableFilePath = path.join(app.getDataDirPath(), tableName + '.json');
299
301
  table = loadTable(options, tableFilePath);
302
+ minSpeed = table.step[0] / 2;
300
303
 
301
304
  //#region Handler and Polar Initialization
302
305
  const { SmootherClass, smootherOptions } = resolveSmootherConfig();
@@ -366,21 +369,34 @@ module.exports = function (app) {
366
369
  }
367
370
  });
368
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');
369
391
 
370
- // boat speed — STW magnitude is optionally forwarded; leeway angle passOn is always false
371
- const rawBoatSpeedPolar = new Polar(app, plugin.id, 'boatSpeed');
372
- rawBoatSpeedPolar.configureMagnitude('navigation.speedThroughWater', options.boatSpeedSource, !options.preventDuplication);
373
- rawBoatSpeedPolar.configureAngle('navigation.leewayAngle', options.boatSpeedSource, false);
374
- rawBoatSpeedPolar.setMeta({ displayName: 'Boat speed', plane: 'Boat' });
375
- rawBoatSpeedPolar.setAngleRange('-piToPi');
376
- rawBoatSpeedPolar.subscribe(true, false);
377
- smoothedBoatSpeed = new PolarSmoother(rawBoatSpeedPolar, SmootherClass, smootherOptions);
378
- rawBoatSpeedPolar.onChange = () => { smoothedBoatSpeed.sample(); };
379
- rawBoatSpeed = rawBoatSpeedPolar;
392
+
393
+ // corrected boatspeed holds both corrected boatspeed and estimated leeway
380
394
  correctedBoatSpeed = new Polar(app, plugin.id, "correctedBoatSpeed");
381
395
  correctedBoatSpeed.configureMagnitude("navigation.speedThroughWater");
382
396
  correctedBoatSpeed.configureAngle("navigation.leewayAngle");
383
397
  correctedBoatSpeed.setMeta({ displayName: "Corrected boatspeed / Leeway", plane: "Boat" });
398
+
399
+ // boatspeed vector in ground frame, used for current estimation and residual calculation
384
400
  boatSpeedRefGround = new Polar(app, plugin.id, "boatSpeedRefGround");
385
401
  boatSpeedRefGround.setMeta({ displayName: "Boat speed over ground", plane: "Ground" });
386
402
 
@@ -415,34 +431,25 @@ module.exports = function (app) {
415
431
  if (options.estimateBoatSpeed) {
416
432
  reportFull.addDelta(rawHeading);
417
433
  reportFull.addAttitude(rawAttitude);
418
- reportFull.addPolar(rawBoatSpeed);
434
+ reportFull.addDelta(rawBoatSpeed);
419
435
  reportFull.addPolar(speedCorrection);
420
436
  reportFull.addPolar(boatSpeedRefGround);
421
437
  reportFull.addPolar(correctedBoatSpeed);
422
438
  reportFull.addPolar(rawGroundSpeed);
423
439
  reportFull.addPolar(smoothedCurrent);
440
+ reportFull.addPolar(residual);
424
441
  }
425
442
  if (options.updateCorrectionTable) {
426
443
  reportFull.addDelta(smoothedHeading);
427
444
  reportFull.addAttitude(smoothedAttitude);
428
- reportFull.addPolar(smoothedBoatSpeed);
445
+ reportFull.addDelta(smoothedBoatSpeed);
429
446
  reportFull.addPolar(smoothedGroundSpeed);
430
447
  if (options.assumeCurrent) {
431
448
  reportFull.addPolar(smoothedCurrent);
432
449
  }
433
- reportFull.addPolar(residual);
434
450
  }
435
451
  reportFull.addTable(table);
436
452
 
437
- // Make reporting object for webApp
438
- reportVector = new Reporter();
439
- reportVector.addDelta(rawHeading);
440
- reportVector.addPolar(residual);
441
- reportVector.addPolar(speedCorrection);
442
- if (options.assumeCurrent) reportVector.addPolar(smoothedCurrent);
443
- reportVector.addPolar(correctedBoatSpeed);
444
- reportVector.addPolar(rawBoatSpeed);
445
- reportVector.addPolar(rawGroundSpeed);
446
453
  //#endregion
447
454
 
448
455
  isRunning = true;
@@ -456,24 +463,14 @@ module.exports = function (app) {
456
463
  if (Object.keys(changedOptions).length) applyOptionChanges();
457
464
 
458
465
  const wellUnderway = started < new Date() - 60 * 1000;
459
- const minSpeed = table.step[0] / 2;
460
466
 
461
- if (options.sogFallback && fallingBackToSog(minSpeed)) {
462
- setStatus('Falling back to Speed Over Ground');
463
- app.debug("Falling back");
464
- return;
465
- }
466
- if (!wellUnderway) {
467
- setStatus('Stabilizing');
468
- } else {
469
- setStatus('Running');
470
- }
467
+ setStatus(wellUnderway ? 'Running' : 'Stabilizing');
471
468
  if (options.estimateBoatSpeed) correct(wellUnderway);
472
469
  if (options.updateCorrectionTable && wellUnderway) {
473
- updateTable(options.assumeCurrent, minSpeed);
470
+ updateTable();
474
471
  // Save correction table periodically
475
472
  const now = new Date();
476
- if (now - lastSave > 5 * 1000) {
473
+ if (now - lastSave > 60 * 1000) {
477
474
  saveTable(table, path.join(app.getDataDirPath(), table.id + '.json'));
478
475
  lastSave = now;
479
476
  }
@@ -491,12 +488,12 @@ module.exports = function (app) {
491
488
  smoothedCurrent = smoothedCurrent?.terminate?.();
492
489
  smoothedBoatSpeed = smoothedBoatSpeed?.terminate();
493
490
  correctedBoatSpeed = correctedBoatSpeed?.terminate();
491
+ lrnBoatSpeed = lrnBoatSpeed?.terminate();
494
492
  boatSpeedRefGround = boatSpeedRefGround?.terminate();
495
493
  smoothedGroundSpeed = smoothedGroundSpeed?.terminate();
496
494
  speedCorrection = speedCorrection?.terminate();
497
495
  residual = residual?.terminate();
498
496
  reportFull = null;
499
- reportVector = null;
500
497
  table = null;
501
498
  rawHeading = null;
502
499
  rawAttitude = null;
@@ -516,53 +513,56 @@ module.exports = function (app) {
516
513
  });
517
514
  };
518
515
 
519
- function fallingBackToSog( minSpeed) {
520
- if (!rawBoatSpeed.stale && rawBoatSpeed.magnitude > 0) return false;
521
- if (rawGroundSpeed.stale || rawGroundSpeed.magnitude < minSpeed) return false;
522
- // Fallback to SOG
523
- correctedBoatSpeed.setVectorValue({ x: rawGroundSpeed.magnitude, y: 0 });
524
- Polar.send(app, plugin.id, [correctedBoatSpeed]);
525
- return true;
526
- }
527
-
528
- /**
529
- * 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.
530
520
  *
531
- * This function uses the current attitude, boat speed, and heading to compute a correction
532
- * vector from the correction table, applies it to the observed boat speed, and sends the
533
- * corrected value. If the vessel is well underway, it also estimates the current vector
534
- * 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
535
525
  *
536
- * @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).
537
527
  */
538
528
  function correct(wellUnderway) {
539
- // prepare iteration
540
- const heel = rawAttitude.value?.roll;
541
- const speed = rawBoatSpeed.magnitude;
542
- const theta = rawHeading.value;
543
- //app.debug(`Heel: ${SI.toDegrees(heel).toFixed(1)}°, Speed: ${SI.toKnots(speed).toFixed(2)} kn, Heading: ${SI.toDegrees(theta).toFixed(1)}°`);
544
- if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) {
545
- //app.debug("Invalid input data");
546
- return;
547
- }
548
-
549
- correctedBoatSpeed.copyFrom(rawBoatSpeed);
550
- speedCorrection.setVectorValue({ x: 0, y: 0 }) ;
551
- if (speed > 0 && !rawAttitude.stale ) {
552
- const { correction, variance } = table.getCorrection(speed, heel);
553
- speedCorrection.setVectorValue(correction, variance);
554
- correctedBoatSpeed.add(speedCorrection);
529
+ // copy the boatspeed delta to polar
530
+ correctedBoatSpeed.setVectorValue({ x: rawBoatSpeed.value, y: 0 });
531
+
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 && !rawGroundSpeed.stale) {
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,26 +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
- // prepare iteration
580
- const heel = smoothedAttitude.value.roll;
581
- const speed = smoothedBoatSpeed.magnitude;
582
- const theta = smoothedHeading.value;
583
-
584
- if (!Number.isFinite(heel) || !Number.isFinite(speed) || !Number.isFinite(theta)) return;
585
-
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
+
586
582
  // update correction table
587
- if (speed > minSpeed && !smoothedAttitude.stale && !smoothedHeading.stale && !smoothedGroundSpeed.stale) {
588
- table.update(speed, heel, smoothedGroundSpeed, assumeCurrent ? smoothedCurrent : noCurrent, smoothedBoatSpeed, theta);
583
+ if (smoothedBoatSpeed.value > minSpeed) {
584
+ table.update(smoothedBoatSpeed.value, smoothedAttitude.value?.roll, smoothedGroundSpeed, options.assumeCurrent ? smoothedCurrent : noCurrent, lrnBoatSpeed, smoothedHeading.value);
589
585
  }
590
586
 
591
- // calculate residual
592
- residual.copyFrom(smoothedGroundSpeed);
593
- boatSpeedRefGround.copyFrom(correctedBoatSpeed);
594
- boatSpeedRefGround.rotate(theta);
595
- residual.substract(boatSpeedRefGround);
596
- residual.substract(smoothedCurrent);
597
-
598
587
  }
599
588
 
600
589
  /**
@@ -681,12 +670,15 @@ module.exports = function (app) {
681
670
  } else if (smoothedAttitude && key === 'attitudeSource') {
682
671
  smoothedAttitude.handler.source = value;
683
672
  } else if (smoothedBoatSpeed && key === 'boatSpeedSource') {
684
- smoothedBoatSpeed.polar.magnitudeHandler.source = value;
673
+ smoothedBoatSpeed.handler.source = value;
685
674
  } else if (smoothedGroundSpeed && key === 'SOGSource') {
686
675
  smoothedGroundSpeed.polar.magnitudeHandler.source = value;
687
676
  } else if (key === 'preventDuplication') {
688
- 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();
689
680
  }
681
+
690
682
  // All other keys (sogFallback, estimateBoatSpeed, updateCorrectionTable,
691
683
  // assumeCurrent, stability, startWithNewTable, COGSource) are read
692
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.2",
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.8.0"
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>