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 +11 -12
- package/index.js +95 -102
- 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,8 +44,7 @@ module.exports = function (app) {
|
|
|
45
44
|
showStatistics: false
|
|
46
45
|
};
|
|
47
46
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
470
|
+
updateTable();
|
|
477
471
|
// Save correction table periodically
|
|
478
472
|
const now = new Date();
|
|
479
|
-
if (now - lastSave >
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
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 -
|
|
526
|
+
* @param {boolean} wellUnderway - Gates current estimation (requires 60 s settling).
|
|
540
527
|
*/
|
|
541
528
|
function correct(wellUnderway) {
|
|
542
|
-
//
|
|
543
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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,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(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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.
|
|
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.
|
|
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
|
+
"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>
|