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 +11 -12
- package/index.js +96 -104
- package/package.json +2 -2
- package/public/app.js +49 -16
- package/public/index.html +2 -2
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
|
|
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
|
|
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
|
|
97
|
-
|
|
|
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**: `
|
|
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
|
|
131
|
-
- **Outputs**: Corrected
|
|
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
|
|
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
|
-
- **
|
|
153
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
470
|
+
updateTable();
|
|
474
471
|
// Save correction table periodically
|
|
475
472
|
const now = new Date();
|
|
476
|
-
if (now - lastSave >
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
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 -
|
|
526
|
+
* @param {boolean} wellUnderway - Gates current estimation (requires 60 s settling).
|
|
537
527
|
*/
|
|
538
528
|
function correct(wellUnderway) {
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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 (
|
|
588
|
-
table.update(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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: '
|
|
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:
|
|
496
|
-
...deltas .map(d => ({ label: itemLabel(d), value: formatDeltaValue(d), stale:
|
|
497
|
-
...attitudes.map(a => ({ label: itemLabel(a), value: formatAttitudeValue(a), stale:
|
|
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, ['
|
|
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, ['
|
|
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, ['
|
|
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 —
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
<
|
|
134
|
-
<div id="learning-intermediates"></div>
|
|
134
|
+
<div id="learning-warnings"></div>
|
|
135
135
|
</div>
|
|
136
136
|
</div>
|
|
137
137
|
</div>
|