speedandcurrent 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/README.md +18 -5
- package/index.js +79 -20
- package/package.json +4 -3
- package/public/app.js +7 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
|
|
7
|
+
## [2.6.0] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Current estimates are also published as an `environment.current` object, with magnetic set calculated from `navigation.magneticVariation` and published at `environment.current.setMagnetic`.
|
|
11
|
+
- The webapp now shows magnetic variation as an estimation input and magnetic current set as an output.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Magnetic variation is no longer marked idle or stale, since it may update too infrequently for telemetry staleness checks.
|
|
15
|
+
|
|
16
|
+
## [2.5.0] - 2026-10-26
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Correction-table files now use version 2 with per-cell update timestamps, capped time-based aging, and silent migration of existing version-1 files.
|
|
20
|
+
- The webapp now polls at the plugin's actual sample rate instead of a fixed 1s interval, reported via a new `pollIntervalMs` field on `/api/report`.
|
|
21
|
+
- Correction estimates now fuse mature nearby table cells using their full covariance and normalized distance, with an automatically estimated spatial variance, a two-cell search radius, and fusion-weight diagnostics in the webapp.
|
|
22
|
+
- The correction table now overlays the current uncorrected (raw) and corrected speed/heel as red and blue dots positioned directly on the grid, with a matching legend entry.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- The npm package now includes this changelog.
|
|
26
|
+
- Updated the `signalkutilities` dependency to `^3.2.0`, including non-finite sample protection, explicit-null smoother resets, and subscription-manager-controlled startup values.
|
|
27
|
+
- Requires signalk-server >=2.35.0 for the upstream metadata-first subscription fix ([SignalK/signalk-server#3216](https://github.com/SignalK/signalk-server/pull/3216)).
|
|
28
|
+
- Read-only Signal K users can now access live reports, status, and metadata; settings and correction-table listing or changes remain administrator-only.
|
|
29
|
+
- Correction-table learning no longer incorporates the estimated current, and the related setting has been removed.
|
|
30
|
+
- Correction-table process noise is now configured as a user-facing correction drift rate in knots/month, with a conservative default of 0.3 and an SI conversion internally. Legacy stability settings migrate silently.
|
|
31
|
+
- Correction interpolation now includes mature cells within 2.5 normalized grid-cell units.
|
|
32
|
+
- Correction learning now always uses a moving-average window of at least 5 seconds, with a one-second gap between observation windows. Older smoother selections migrate silently.
|
|
33
|
+
- Cells become eligible after one accepted observation; the explicit maturity gate remains in place at zero for future tuning.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Removed idle-triggered input resubscriptions, including boat-speed inputs, to avoid replaying cached values during sensor outages. Idle/stale warnings remain, and existing subscriptions receive data when the source resumes. Synchronous startup safeguards are preserved.
|
|
37
|
+
- The boat-speed estimation inputs now prioritize Boatspeed unless groundspeed fallback is selected, and the correction table marks its most recently updated cell with a thin black border reflected in the legend.
|
|
38
|
+
- The learning status Reason row is now hidden when the last observation was accepted, instead of showing a redundant "Observation recorded" message.
|
|
39
|
+
- `navigation.state` no longer misreports as "not ready" a few seconds after each update; it is an event path that only publishes on transition, so the generic telemetry staleness timer no longer applies to it. The Inputs tab now also displays its current value.
|
|
40
|
+
- Learning and leeway are now gated on the vessel's own SOG-based moving/not-moving state, rather than solely on `navigation.state` freshness. Previously, an infrequent `navigation.state` update could go stale within seconds, silently letting learning continue while anchored or moored. `navigation.state`, when known and `suspendLearningOnNavigationState` is enabled, can still override to "not moving" (anchored/moored/motoring), but can no longer force "moving" against the vessel's own SOG.
|
|
41
|
+
- Webapp report parsing failures now remain visible and still refresh plugin status instead of aborting the polling tick.
|
|
42
|
+
- Disabled current estimation no longer publishes a zero-current value during plugin startup.
|
|
43
|
+
- Malformed correction tables now recover safely at startup, while explicit loads reject invalid files without replacing the active table.
|
|
44
|
+
- The correction-table Kalman filter now uses each observation's own measurement-noise covariance (already computed from live sensor variances) instead of a fixed identity matrix, so anisotropic and rotated observation uncertainty is correctly reflected in cell covariances. Persisted tables store a serializable model descriptor rather than the runtime model; existing table files still load unchanged.
|
|
45
|
+
|
|
46
|
+
## [2.3.4] - 2026-09-17
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- Boat-speed startup now preserves synchronously replayed cached values, avoids restart errors, and keeps current estimation behind the startup stabilization period regardless of table-learning settings.
|
|
50
|
+
- Attitude roll handling now treats incomplete or stale values as invalid during correction-table learning, preventing fabricated heel-based corrections while the input is not yet usable.
|
|
51
|
+
- Webapp values and warnings now share per-path lifecycle status, distinguish subscribed and computed data, report polar magnitude and angle independently, and show stale raw inputs consistently across all pages.
|
|
52
|
+
|
|
53
|
+
## [2.3.3] - 2026-09-06
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- `navigation.leewayAngle` no longer reports a correction-table artifact when the vessel is stationary. The lateral component of the table correction is now suppressed below the table speed step and while `navigation.state` is `anchored` or `moored`, so leeway is published as 0 instead of an angle derived from a near-zero speed. The longitudinal speed correction, and leeway while motoring, are unaffected.
|
|
57
|
+
- Updated `signalkutilities` to 3.1.2, which completes the 3.1.1 recovery fix for polar-backed inputs. Smoothed heading and the ground-speed vector are built on `Polar`/`PolarSmoother`, where the idle timer was still cleared rather than re-armed on each delta, so those inputs did not recover after going silent following a healthy start.
|
|
58
|
+
|
|
59
|
+
## [2.3.2] - 2026-09-04
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- Updated `signalkutilities` to 3.1.1, so an input that goes silent *after* having delivered data is now detected and resubscribed. Previously recovery only ran for subscriptions that had never received a delta, meaning an upstream outage after a healthy start could leave the plugin dead until it was restarted.
|
|
63
|
+
|
|
64
|
+
## [2.3.1] - 2026-08-18
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- Signal K admin styling now loads from standalone CSS records in current Vite manifests while retaining support for entry-associated CSS and older server fallbacks.
|
|
68
|
+
|
|
69
|
+
## [2.3.0] - 2026-08-16
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- Learning status now distinguishes between active learning and observations that were skipped, rejected, or invalid, and the webapp reports the current reason more clearly.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
- Correction-table learning now uses automatic gates based on `navigation.state`, with `anchored` and `moored` always suspending learning when available and `motoring` optionally suspending it.
|
|
76
|
+
- The learning threshold for both STW and SOG now follows the active table speed-step size.
|
|
77
|
+
- The webapp no longer exposes a separate minimum-SOG learning setting.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- When learning is off or suspended, the webapp no longer keeps showing a stale previous observation result.
|
|
81
|
+
|
|
82
|
+
## [2.2.1] - 2026-08-14
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
- Input subscriptions now recover automatically after idle or stale periods, and the webapp exposes lifecycle warnings for affected paths.
|
|
86
|
+
- Corrected output paths are now explicitly cleared with `null` when estimation is disabled or when the plugin stops, preventing stale values from lingering on the Signal K bus.
|
|
87
|
+
- The webapp-facing status and report endpoints now reflect the plugin's current runtime state more directly.
|
|
88
|
+
- SOG fallback now checks raw STW (`rawBoatSpeed`) directly when detecting a zero paddlewheel reading, avoiding dependence on the derived corrected speed value.
|
|
89
|
+
|
|
90
|
+
## [2.1.2] - 2026-07-23
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
- App Store icon was missing (monogram fallback shown): added `icon.png` at the package root so the App Store CDN can fetch it from the npm tarball via unpkg.com. The runtime `signalk.appIcon: ./icon.png` path is unchanged - the server still serves it from `public/icon.png` via the webapp mount.
|
|
94
|
+
- Updated `signalkutilities` to 3.0.1, fixing continuous 401 log flooding on SK servers with security enabled and anonymous read access disabled.
|
|
95
|
+
|
|
96
|
+
## [2.1.1] - 2026-07-20
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
- Sidebar toggler was unreachable on mobile viewports.
|
|
100
|
+
|
|
101
|
+
### Changed
|
|
102
|
+
- Updated `signalkutilities` dependency to `^3.0.0` (removes `PolarTable` from the shared library; no behaviour change for this plugin).
|
|
103
|
+
|
|
104
|
+
## [2.1.0] - 2026-07-10
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- `null` is written to corrected boat speed (`navigation.speedThroughWater`, `navigation.leewayAngle`) and current (`environment.current.drift`, `environment.current.setTrue`) paths when `estimateBoatSpeed` is toggled off.
|
|
108
|
+
- `null` is written to all active output paths (current, corrected boat speed) when the plugin stops, preventing stale values on the SK bus.
|
|
109
|
+
|
|
110
|
+
## [2.0.2] - 2026-6-18
|
|
111
|
+
### Fixed
|
|
112
|
+
- The plugin icon was not displayed in the Signal K App Store because the icon path in the package metadata was incorrect.
|
|
113
|
+
- Test files are no longer included in the published npm package.
|
|
114
|
+
|
|
115
|
+
## [2.0.1] - 2026-6-17
|
|
116
|
+
### Fixed
|
|
117
|
+
- Correction table reporting was doing unnecessary work on every update, causing higher CPU use especially on low-power hardware such as a Raspberry Pi.
|
|
118
|
+
- The variance values used for interpolating between correction table cells were calculated with an incorrect formula, causing them to be overestimated. This made the Kalman outlier gate slightly less accurate.
|
|
119
|
+
|
|
120
|
+
## [2.0.0] - 2026-6-17
|
|
121
|
+
### Changed
|
|
122
|
+
- Moving average smoother is now O(1) in both CPU and memory: it no longer slows down or uses more memory as the window size grows.
|
|
123
|
+
|
|
124
|
+
- **Breaking:** Requires `signalkutilities` v2.0.0. Source-selection and pass-through machinery has been removed from the library; Signal K Server now handles source priorities natively.
|
|
125
|
+
- Per-path source settings (`headingSource`, `boatSpeedSource`, `SOGSource`, `attitudeSource`) have been removed. Use Signal K Server's built-in source priority configuration instead.
|
|
126
|
+
- The **Prevent Speed Duplication** setting has been removed. The server's `excludeSelf` behaviour and source priorities replace it.
|
|
127
|
+
|
|
128
|
+
### Removed
|
|
129
|
+
- Source dropdowns in the webapp Inputs section.
|
|
130
|
+
- Source-related fields from persisted plugin configuration. A one-time migration strips these fields automatically on first start with the new version.
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
- Documentation explaining how to configure Signal K source priorities so that the corrected `navigation.speedThroughWater` is delivered to all consumers.
|
|
134
|
+
|
|
135
|
+
## [1.7.10] - 2026-5-14
|
|
136
|
+
### Fixed
|
|
137
|
+
- Removed incorrect `self.` prefix from Signal K paths.
|
|
138
|
+
|
|
139
|
+
## [1.7.9] - 2026-5-14
|
|
140
|
+
### Added
|
|
141
|
+
- Learning status indicator in the webapp showing whether the correction table is active, stabilising, or off, and whether the last observation was accepted, rejected, invalid, or below threshold.
|
|
142
|
+
- Outlier rejection using Mahalanobis distance: bad observations are discarded before being fed into the Kalman filter, preventing a single bad sample from corrupting a cell.
|
|
143
|
+
|
|
144
|
+
### Fixed
|
|
145
|
+
- Smoothed learning inputs are now always registered in the reporter regardless of whether `updateCorrectionTable` is enabled, so warnings clear correctly when learning is toggled on at runtime.
|
|
146
|
+
|
|
147
|
+
## [1.7.8] - 2026-5-14
|
|
148
|
+
### Added
|
|
149
|
+
- Legend below the correction table explaining factor / leeway values, background colour coding, stripe direction, active cell, and neighbour cell indicators.
|
|
150
|
+
|
|
151
|
+
### Fixed
|
|
152
|
+
- Correction table learning never started when `assumeCurrent` is off.
|
|
153
|
+
- Active cell highlighting (most recently updated cell) was not visible.
|
|
154
|
+
|
|
155
|
+
## [1.7.7] - 2026-5-13
|
|
156
|
+
### Added
|
|
157
|
+
- Optional staleness detection for inputs.
|
|
158
|
+
- Precise warnings on state of inputs.
|
|
159
|
+
|
|
160
|
+
## [1.7.5] - 2025-11-19
|
|
161
|
+
### Added
|
|
162
|
+
- New webApp.
|
|
163
|
+
- WebApp allows to change plugin configuration while the plugin is running.
|
|
164
|
+
- Support for multiple correction tables (new, copy, delete).
|
|
165
|
+
- Support for altering the dimensions of a correction table (alter).
|
|
166
|
+
- Better edge case handling.
|
|
167
|
+
- User selectable options for smoothing method and smoothing parameters.
|
|
168
|
+
- Support for unitPreferences.
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
- Bug where navigation.speedThroughWater was removed from the signalk bus.
|
|
172
|
+
|
|
173
|
+
### Removed
|
|
174
|
+
- Possibility to set options via the signalk plugin configuration.
|
|
175
|
+
|
|
176
|
+
## [1.6.0] - 2025-11-19
|
|
177
|
+
### Added
|
|
178
|
+
- Option to copy navigation.speedOverGround to navigation.speedThroughWater when speedThroughWater is not properly measured. Thanks to Jean-Laurent Girod for the suggestion.
|
|
179
|
+
|
|
180
|
+
## [1.5.1] - 2025-10-06
|
|
181
|
+
### Fixed
|
|
182
|
+
- Bug preventing current to be displayed in KIP.
|
|
183
|
+
|
|
184
|
+
## [1.5.0] - 2025-10-06
|
|
185
|
+
### Changed
|
|
186
|
+
- Separate loops for correcting speed and for updating correction factors.
|
|
187
|
+
- Speed correction loop uses raw values.
|
|
188
|
+
- Updating correction factors using moving averages.
|
|
189
|
+
- Unified cell rendering driven by "Show" selections; removed the previous Table style toggle.
|
|
190
|
+
|
|
191
|
+
### Added
|
|
192
|
+
- Option to set the source for input paths.
|
|
193
|
+
- Full integration of uncertainty estimations (variance) in estimations and corrections.
|
|
194
|
+
- Ability to resize the correction table without losing all correction data.
|
|
195
|
+
- WebApp: "Show" multi-select to choose per-cell metrics (correction X/Y, factor, leeway, trace, N) for the correction table.
|
|
196
|
+
- WebApp: Color mode options for the correction table.
|
|
197
|
+
- WebApp: Persistence for speed unit, angle unit, color mode, and shown metrics via localStorage.
|
|
198
|
+
|
|
199
|
+
### Fixed
|
|
200
|
+
- Angles are now reported in the appropriate range, 0 to 2 * PI or -PI to PI, depending on path.
|
|
201
|
+
|
|
202
|
+
## [1.1.0] - 2025-8-20
|
|
203
|
+
### Changed
|
|
204
|
+
- Path names for current. It now is self.environment.current.setTrue and self.environment.current.drift.
|
|
205
|
+
|
|
206
|
+
### Fixed
|
|
207
|
+
- Case error in filename that prevented the webapp from displaying the correction table.
|
|
208
|
+
- Corrected speed through water is now calculated when the correction table is fixed.
|
|
209
|
+
|
|
210
|
+
## [1.0.1] - 2025-1-27
|
|
211
|
+
### Added
|
|
212
|
+
- Basic functionality to estimate speed, leeway and current using Kalman filters.
|
|
213
|
+
- Basic functionality for a webapp that shows what is going on.
|
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ Most paddle wheel logs carry a systematic error that varies with boat speed and
|
|
|
6
6
|
|
|
7
7
|
This plugin builds and maintains a **2-dimensional correction table** — indexed by speed and heel — that is populated automatically from real sailing observations. No calibration runs, no spreadsheets, no manual entry. Once the table has enough coverage the plugin applies per-point corrections to `navigation.speedThroughWater`, derives leeway from the lateral component of the correction, and estimates water current by subtracting the corrected boat speed vector from the GPS ground speed vector.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Requires signalk-server >=2.35.0 for reliable metadata-first input subscriptions.
|
|
10
|
+
|
|
11
|
+
If an input goes idle or stale, the webapp shows a warning while the subscription stays attached, ready for the next delta; when estimation is disabled or the plugin stops, the active output paths are cleared with `null` so downstream consumers do not keep stale values.
|
|
10
12
|
|
|
11
13
|
In broad terms the plugin:
|
|
12
14
|
|
|
@@ -31,6 +33,12 @@ The plugin requires no further configuration in the Signal K admin UI. All setti
|
|
|
31
33
|
|
|
32
34
|
---
|
|
33
35
|
|
|
36
|
+
## HTTP endpoint access
|
|
37
|
+
|
|
38
|
+
Signal K read-only users can access the live report (`/api/report`), metadata (`/api/meta`), and status (`/api/status`) endpoints. Settings (`/api/settings`) and correction-table listing or management (`/api/tables...`) require administrator access. Anonymous requests still follow the server's configured security policy; this plugin does not enable anonymous access.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
34
42
|
## Configuring Signal K source priorities
|
|
35
43
|
|
|
36
44
|
Signal K Server manages source priorities natively. When multiple devices publish the same path, the server delivers the highest-priority source to all subscribers — including this plugin. No per-path source selection is needed inside the plugin itself.
|
|
@@ -54,6 +62,7 @@ Once configured, all consumers on the Signal K bus — KIP, OpenCPN, Instrument
|
|
|
54
62
|
| `navigation.courseOverGroundTrue` | GPS ground speed direction |
|
|
55
63
|
| `navigation.headingTrue` | True heading — used to rotate the boat-frame correction into the ground frame |
|
|
56
64
|
| `navigation.attitude` | Roll angle — provides the heel index into the correction table |
|
|
65
|
+
| `navigation.magneticVariation` | Magnetic variation — used to convert estimated current set to magnetic north |
|
|
57
66
|
|
|
58
67
|
---
|
|
59
68
|
|
|
@@ -65,6 +74,8 @@ Once configured, all consumers on the Signal K bus — KIP, OpenCPN, Instrument
|
|
|
65
74
|
| `navigation.leewayAngle` | rad | Leeway angle (starboard positive). Derived from the lateral correction component. |
|
|
66
75
|
| `environment.current.drift` | m/s | Estimated current speed. |
|
|
67
76
|
| `environment.current.setTrue` | rad | Estimated current direction (the direction the water moves *toward*). |
|
|
77
|
+
| `environment.current.setMagnetic` | rad | Estimated current direction relative to magnetic north, calculated from `environment.current.setTrue` and `navigation.magneticVariation`. |
|
|
78
|
+
| `environment.current` | object | Current estimate containing `drift`, `setTrue`, and, when magnetic variation is available, `setMagnetic`. |
|
|
68
79
|
|
|
69
80
|
A 60-second stabilisation period applies after startup. Corrected boatspeed may be published during this window, but current estimation and table learning wait until it ends.
|
|
70
81
|
|
|
@@ -99,6 +110,7 @@ The **Inputs** section shows live readings from the raw sensors as they arrive f
|
|
|
99
110
|
| Boat speed | `navigation.speedThroughWater` |
|
|
100
111
|
| Ground speed | `navigation.speedOverGround` + `navigation.courseOverGroundTrue` |
|
|
101
112
|
| Attitude (heel) | `navigation.attitude` (roll) |
|
|
113
|
+
| Magnetic variation | `navigation.magneticVariation` |
|
|
102
114
|
|
|
103
115
|
### Settings
|
|
104
116
|
|
|
@@ -116,7 +128,7 @@ The **Boatspeed Estimation** section is enabled with the toggle in the panel hea
|
|
|
116
128
|
|
|
117
129
|
The panel is organised into three groups:
|
|
118
130
|
|
|
119
|
-
**Inputs** — the raw
|
|
131
|
+
**Inputs** — the raw signals used by the estimation calculations: heading, boat speed, ground speed, attitude, and magnetic variation. Warnings appear here if any are unavailable.
|
|
120
132
|
|
|
121
133
|
**Intermediates** — computed vectors that give insight into what the plugin is doing:
|
|
122
134
|
- *Speed correction* — the correction vector currently being applied (longitudinal + lateral components).
|
|
@@ -126,6 +138,7 @@ The panel is organised into three groups:
|
|
|
126
138
|
**Outputs** — what is being published to Signal K:
|
|
127
139
|
- *Corrected boatspeed / Leeway* — corrected STW magnitude and leeway angle.
|
|
128
140
|
- *Current* — estimated current drift and set.
|
|
141
|
+
- *Magnetic set* — estimated current direction relative to magnetic north, calculated using magnetic variation.
|
|
129
142
|
|
|
130
143
|
### Settings
|
|
131
144
|
|
|
@@ -289,7 +302,7 @@ Every time conditions are right — plugin running for >60 seconds, smoothers se
|
|
|
289
302
|
observation = R(ψ)⁻¹ · V_SOG − R(ψ)⁻¹ · V_current − V_STW
|
|
290
303
|
```
|
|
291
304
|
|
|
292
|
-
where `R(ψ)` rotates from ground frame to boat frame using true heading `ψ`. In plain terms: rotate GPS velocity into the boat frame, subtract the raw paddle wheel velocity. The residual is the implied sensor error for the current speed and heel.
|
|
305
|
+
where `R(ψ)` rotates from ground frame to boat frame using true heading `ψ`. In plain terms: rotate GPS velocity into the boat frame, subtract the current estimate (also rotated), subtract the raw paddle wheel velocity. The residual is the implied sensor error for the current speed and heel.
|
|
293
306
|
|
|
294
307
|
The Kalman update combines this observation with the cell's existing belief:
|
|
295
308
|
|
|
@@ -298,11 +311,11 @@ K = P · (P + R_obs)⁻¹
|
|
|
298
311
|
x_new = x_old + K · (observation − x_old)
|
|
299
312
|
```
|
|
300
313
|
|
|
301
|
-
where P is the cell's current covariance and R_obs is the observation covariance derived from the measurement uncertainty of all contributing signals (SOG variance + STW variance, rotated appropriately). It also includes heading uncertainty in radians squared. For $u = R(-heading)(groundSpeed - current)$, the heading contribution is $J \sigma_h^2 J^T$, where $J = [u_y, -u_x]^T$. This is applied once to the combined ground-speed-minus-current vector because both vectors share the same heading error. **Noisy observations produce a smaller gain and move the cell estimate less.**
|
|
314
|
+
where P is the cell's current covariance and R_obs is the observation covariance derived from the measurement uncertainty of all contributing signals (SOG variance + current variance + STW variance, rotated appropriately). It also includes heading uncertainty in radians squared. For $u = R(-heading)(groundSpeed - current)$, the heading contribution is $J \sigma_h^2 J^T$, where $J = [u_y, -u_x]^T$. This is applied once to the combined ground-speed-minus-current vector because both vectors share the same heading error. **Noisy observations produce a smaller gain and move the cell estimate less.**
|
|
302
315
|
|
|
303
316
|
### Time-scaled aging and correction drift
|
|
304
317
|
|
|
305
|
-
Each cell has a small **process-noise rate** that allows it to drift slowly over time, reflecting that a paddle wheel's error can change. For an elapsed interval `dt`, the process covariance added to each diagonal is:
|
|
318
|
+
Each cell has a small **process-noise rate** that allows it to drift slowly over time, reflecting that a paddle wheel's error can change with fouling, recalibration, or crew weight distribution. For an elapsed interval `dt`, the process covariance added to each diagonal is:
|
|
306
319
|
|
|
307
320
|
```text
|
|
308
321
|
effectiveDt = min(dt, 90 days)
|
package/index.js
CHANGED
|
@@ -295,6 +295,9 @@ module.exports = function (app) {
|
|
|
295
295
|
let smoothedAttitude = null;
|
|
296
296
|
let rawCurrent = null;
|
|
297
297
|
let smoothedCurrent = null;
|
|
298
|
+
let magneticVariationHandler = null;
|
|
299
|
+
let setMagneticHandler = null;
|
|
300
|
+
let setMagneticPublished = false;
|
|
298
301
|
let smoothedBoatSpeed = null;
|
|
299
302
|
let correctedBoatSpeed = null;
|
|
300
303
|
let lrnBoatSpeed = null;
|
|
@@ -385,7 +388,7 @@ module.exports = function (app) {
|
|
|
385
388
|
function setLifecycleWarning(id, status, path) {
|
|
386
389
|
const safePath = path || 'unknown path';
|
|
387
390
|
const message = status === 'idle'
|
|
388
|
-
? `Input ${id} is idle on ${safePath}
|
|
391
|
+
? `Input ${id} is idle on ${safePath}`
|
|
389
392
|
: status === 'incomplete'
|
|
390
393
|
? `Input ${id} is missing ${safePath}`
|
|
391
394
|
: `Input ${id} is stale on ${safePath}`;
|
|
@@ -407,7 +410,7 @@ module.exports = function (app) {
|
|
|
407
410
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
408
411
|
}
|
|
409
412
|
|
|
410
|
-
function buildLifecycleCallbacks(id, getPath
|
|
413
|
+
function buildLifecycleCallbacks(id, getPath) {
|
|
411
414
|
return {
|
|
412
415
|
onDelta: () => {
|
|
413
416
|
clearLifecycleWarning(id);
|
|
@@ -421,13 +424,8 @@ module.exports = function (app) {
|
|
|
421
424
|
onIdle: () => {
|
|
422
425
|
if (!isRunning) return;
|
|
423
426
|
const path = getPath();
|
|
424
|
-
app.debug(`[${plugin.id}] idle input ${id} on ${path}
|
|
427
|
+
app.debug(`[${plugin.id}] idle input ${id} on ${path}`);
|
|
425
428
|
setLifecycleWarning(id, 'idle', path);
|
|
426
|
-
try {
|
|
427
|
-
resubscribe();
|
|
428
|
-
} catch (e) {
|
|
429
|
-
app.debug(`[${plugin.id}] resubscribe failed for ${id}: ${e.message}`);
|
|
430
|
-
}
|
|
431
429
|
}
|
|
432
430
|
};
|
|
433
431
|
}
|
|
@@ -448,7 +446,7 @@ module.exports = function (app) {
|
|
|
448
446
|
app.debug('registerWithRouter');
|
|
449
447
|
readOptions(); // pre-load so /api/settings works before start()
|
|
450
448
|
|
|
451
|
-
router.get('/api/report', (req, res) => {
|
|
449
|
+
router.access('readonly').get('/api/report', (req, res) => {
|
|
452
450
|
if (!isRunning) {
|
|
453
451
|
res.status(503).json({ error: "Plugin is not running" });
|
|
454
452
|
} else {
|
|
@@ -462,7 +460,7 @@ module.exports = function (app) {
|
|
|
462
460
|
}
|
|
463
461
|
});
|
|
464
462
|
|
|
465
|
-
router.get('/api/meta', (req, res) => {
|
|
463
|
+
router.access('readonly').get('/api/meta', (req, res) => {
|
|
466
464
|
if (!isRunning) {
|
|
467
465
|
res.status(503).json({ error: "Plugin is not running" });
|
|
468
466
|
} else {
|
|
@@ -472,7 +470,7 @@ module.exports = function (app) {
|
|
|
472
470
|
|
|
473
471
|
|
|
474
472
|
|
|
475
|
-
router.get('/api/status', (req, res) => {
|
|
473
|
+
router.access('readonly').get('/api/status', (req, res) => {
|
|
476
474
|
res.json({ status: pluginStatus, isRunning, lifecycleWarnings, learningState: getLearningStatePayload() });
|
|
477
475
|
});
|
|
478
476
|
|
|
@@ -653,8 +651,7 @@ module.exports = function (app) {
|
|
|
653
651
|
smootherOptions,
|
|
654
652
|
...buildLifecycleCallbacks(
|
|
655
653
|
'heading.angle',
|
|
656
|
-
() => smoothedHeading?.handler?.path || 'navigation.headingTrue'
|
|
657
|
-
() => { smoothedHeading?.unsubscribe(); smoothedHeading?.subscribe(false, true); }
|
|
654
|
+
() => smoothedHeading?.handler?.path || 'navigation.headingTrue'
|
|
658
655
|
)
|
|
659
656
|
});
|
|
660
657
|
rawHeading = smoothedHeading.handler;
|
|
@@ -669,8 +666,7 @@ module.exports = function (app) {
|
|
|
669
666
|
smootherOptions,
|
|
670
667
|
...buildLifecycleCallbacks(
|
|
671
668
|
'attitude.smoothed',
|
|
672
|
-
() => smoothedAttitude?.handler?.path || 'navigation.attitude'
|
|
673
|
-
() => { smoothedAttitude?.unsubscribe(); smoothedAttitude?.subscribe(); }
|
|
669
|
+
() => smoothedAttitude?.handler?.path || 'navigation.attitude'
|
|
674
670
|
)
|
|
675
671
|
});
|
|
676
672
|
rawAttitude = smoothedAttitude.handler;
|
|
@@ -691,6 +687,25 @@ module.exports = function (app) {
|
|
|
691
687
|
// send metadata for current
|
|
692
688
|
MessageHandler.setMeta(app, plugin.id, "environment.current.drift", {units: "m/s", type: "number", description: "Speed of the current"});
|
|
693
689
|
MessageHandler.setMeta(app, plugin.id, "environment.current.setTrue", { units: "rad", type: "number", description: "Direction of the current" });
|
|
690
|
+
MessageHandler.setMeta(app, plugin.id, "environment.current", { type: "object", description: "Estimated water current" });
|
|
691
|
+
MessageHandler.setMeta(app, plugin.id, "environment.current.setMagnetic", {
|
|
692
|
+
units: "rad",
|
|
693
|
+
type: "number",
|
|
694
|
+
description: "Direction of the current relative to magnetic north",
|
|
695
|
+
displayName: "Magnetic set",
|
|
696
|
+
displayUnits: { category: "angle" }
|
|
697
|
+
});
|
|
698
|
+
magneticVariationHandler = new MessageHandler(app, plugin.id, "magneticVariation");
|
|
699
|
+
magneticVariationHandler.configure("navigation.magneticVariation");
|
|
700
|
+
magneticVariationHandler.idlePeriod = 0;
|
|
701
|
+
magneticVariationHandler.stalePeriod = 0;
|
|
702
|
+
magneticVariationHandler.onDelta = () => {
|
|
703
|
+
clearLifecycleWarning("magneticVariation");
|
|
704
|
+
publishCurrentObject();
|
|
705
|
+
};
|
|
706
|
+
magneticVariationHandler.subscribe();
|
|
707
|
+
setMagneticHandler = new MessageHandler(app, plugin.id, "setMagnetic");
|
|
708
|
+
setMagneticHandler.configure("environment.current.setMagnetic");
|
|
694
709
|
rawCurrent = new Polar(app, plugin.id, "current");
|
|
695
710
|
rawCurrent.configureMagnitude("environment.current.drift");
|
|
696
711
|
rawCurrent.configureAngle("environment.current.setTrue");
|
|
@@ -776,10 +791,8 @@ module.exports = function (app) {
|
|
|
776
791
|
onIdle: () => {
|
|
777
792
|
if (!isRunning) return;
|
|
778
793
|
const path = smoothedBoatSpeed?.handler?.path || 'navigation.speedThroughWater';
|
|
779
|
-
app.debug(`[${plugin.id}] idle input boatSpeed.smoothed on ${path}
|
|
794
|
+
app.debug(`[${plugin.id}] idle input boatSpeed.smoothed on ${path}`);
|
|
780
795
|
setLifecycleWarning('boatSpeed.smoothed', 'idle', path);
|
|
781
|
-
smoothedBoatSpeed?.unsubscribe();
|
|
782
|
-
smoothedBoatSpeed?.subscribe();
|
|
783
796
|
},
|
|
784
797
|
onStale: () => {
|
|
785
798
|
if (!isRunning) return;
|
|
@@ -820,8 +833,7 @@ module.exports = function (app) {
|
|
|
820
833
|
smootherOptions,
|
|
821
834
|
...buildLifecycleCallbacks(
|
|
822
835
|
'groundSpeed.smoothed',
|
|
823
|
-
() => `${smoothedGroundSpeed?.polar?.pathMagnitude || 'navigation.speedOverGround'}, ${smoothedGroundSpeed?.polar?.pathAngle || 'navigation.courseOverGroundTrue'}
|
|
824
|
-
() => { smoothedGroundSpeed?.unsubscribe(); smoothedGroundSpeed?.subscribe(true, true); }
|
|
836
|
+
() => `${smoothedGroundSpeed?.polar?.pathMagnitude || 'navigation.speedOverGround'}, ${smoothedGroundSpeed?.polar?.pathAngle || 'navigation.courseOverGroundTrue'}`
|
|
825
837
|
)
|
|
826
838
|
});
|
|
827
839
|
rawGroundSpeed = smoothedGroundSpeed.polar;
|
|
@@ -853,6 +865,8 @@ module.exports = function (app) {
|
|
|
853
865
|
reportFull.addPolar(residual);
|
|
854
866
|
reportFull.addPolar(smoothedResidual);
|
|
855
867
|
}
|
|
868
|
+
reportFull.addDelta(magneticVariationHandler);
|
|
869
|
+
reportFull.addDelta(setMagneticHandler);
|
|
856
870
|
// Smoothed learning inputs are always subscribed regardless of updateCorrectionTable,
|
|
857
871
|
// so always add them to the report — otherwise warnings never clear when
|
|
858
872
|
// learning was disabled at startup and later toggled on.
|
|
@@ -884,12 +898,14 @@ module.exports = function (app) {
|
|
|
884
898
|
}
|
|
885
899
|
// Clear all active output paths from the SK bus before teardown.
|
|
886
900
|
if (smoothedCurrent) PolarSmoother.clear(app, plugin.id, [smoothedCurrent]);
|
|
901
|
+
clearCurrentObjectOutputs();
|
|
887
902
|
if (options.estimateBoatSpeed && correctedBoatSpeed) Polar.clear(app, plugin.id, [correctedBoatSpeed]);
|
|
888
903
|
smoothedHeading = smoothedHeading?.terminate();
|
|
889
904
|
smoothedAttitude = smoothedAttitude?.terminate();
|
|
890
905
|
rawCurrent = rawCurrent?.terminate();
|
|
891
906
|
smoothedCurrent = smoothedCurrent?.terminate?.();
|
|
892
907
|
navigationStateHandler = navigationStateHandler?.terminate();
|
|
908
|
+
magneticVariationHandler = magneticVariationHandler?.terminate();
|
|
893
909
|
smoothedBoatSpeed = smoothedBoatSpeed?.terminate();
|
|
894
910
|
correctedBoatSpeed = correctedBoatSpeed?.terminate();
|
|
895
911
|
lrnBoatSpeed = lrnBoatSpeed?.terminate();
|
|
@@ -905,6 +921,8 @@ module.exports = function (app) {
|
|
|
905
921
|
noCurrent = null;
|
|
906
922
|
rawBoatSpeed = null;
|
|
907
923
|
rawGroundSpeed = null;
|
|
924
|
+
setMagneticHandler = null;
|
|
925
|
+
setMagneticPublished = false;
|
|
908
926
|
lastNavigationStateValue = null;
|
|
909
927
|
learningStabilizingUntil = 0;
|
|
910
928
|
learningStabilizingReason = 'startup';
|
|
@@ -996,10 +1014,50 @@ module.exports = function (app) {
|
|
|
996
1014
|
}
|
|
997
1015
|
|
|
998
1016
|
PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
|
|
1017
|
+
publishCurrentObject();
|
|
999
1018
|
|
|
1000
1019
|
Polar.send(app, plugin.id, [correctedBoatSpeed]);
|
|
1001
1020
|
}
|
|
1002
1021
|
|
|
1022
|
+
function publishCurrentObject() {
|
|
1023
|
+
if (!options.estimateBoatSpeed || !smoothedCurrent?.ready) return;
|
|
1024
|
+
const drift = smoothedCurrent.magnitude;
|
|
1025
|
+
const setTrue = smoothedCurrent.angle;
|
|
1026
|
+
if (!Number.isFinite(drift) || !Number.isFinite(setTrue)) return;
|
|
1027
|
+
|
|
1028
|
+
const current = { drift, setTrue };
|
|
1029
|
+
const values = [{ path: "environment.current", value: current }];
|
|
1030
|
+
const magneticVariation = magneticVariationHandler?.value;
|
|
1031
|
+
if (magneticVariationHandler?.ready && Number.isFinite(magneticVariation)) {
|
|
1032
|
+
const setMagnetic = ((setTrue - magneticVariation) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
|
|
1033
|
+
current.setMagnetic = setMagnetic;
|
|
1034
|
+
setMagneticHandler.value = setMagnetic;
|
|
1035
|
+
values.push({ path: "environment.current.setMagnetic", value: setMagnetic });
|
|
1036
|
+
setMagneticPublished = true;
|
|
1037
|
+
} else {
|
|
1038
|
+
setMagneticHandler.invalidate();
|
|
1039
|
+
if (setMagneticPublished) {
|
|
1040
|
+
values.push({ path: "environment.current.setMagnetic", value: null });
|
|
1041
|
+
}
|
|
1042
|
+
setMagneticPublished = false;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
app.handleMessage(plugin.id, { updates: [{ values }] });
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function clearCurrentObjectOutputs() {
|
|
1049
|
+
setMagneticHandler?.invalidate();
|
|
1050
|
+
setMagneticPublished = false;
|
|
1051
|
+
app.handleMessage(plugin.id, {
|
|
1052
|
+
updates: [{
|
|
1053
|
+
values: [
|
|
1054
|
+
{ path: "environment.current", value: null },
|
|
1055
|
+
{ path: "environment.current.setMagnetic", value: null }
|
|
1056
|
+
]
|
|
1057
|
+
}]
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1003
1061
|
/**
|
|
1004
1062
|
* Updates the correction table from the current smoothed inputs.
|
|
1005
1063
|
* Reads minSpeed from module-level state;
|
|
@@ -1198,6 +1256,7 @@ module.exports = function (app) {
|
|
|
1198
1256
|
if (key === 'estimateBoatSpeed' && !value && correctedBoatSpeed) {
|
|
1199
1257
|
Polar.clear(app, plugin.id, [correctedBoatSpeed]);
|
|
1200
1258
|
PolarSmoother.clear(app, plugin.id, [smoothedCurrent]);
|
|
1259
|
+
clearCurrentObjectOutputs();
|
|
1201
1260
|
}
|
|
1202
1261
|
|
|
1203
1262
|
delete changedOptions[key];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speedandcurrent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
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": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"homepage": "https://github.com/Asw1n/speedandcurrent#readme",
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=22.0.0",
|
|
21
|
-
"signalk-server": ">=2.
|
|
21
|
+
"signalk-server": ">=2.35.0"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"signalk-webapp",
|
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"kalman-filter": "^2.3.0",
|
|
44
|
-
"signalkutilities": "^3.
|
|
44
|
+
"signalkutilities": "^3.2.0"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
|
+
"CHANGELOG.md",
|
|
47
48
|
"icon.png",
|
|
48
49
|
"index.js",
|
|
49
50
|
"correctionTable.js",
|
package/public/app.js
CHANGED
|
@@ -118,7 +118,7 @@ async function loadMeta() {
|
|
|
118
118
|
|
|
119
119
|
const computedItemIds = new Set([
|
|
120
120
|
'current.smoothed', 'boatSpeedRefGround', 'speedCorrection',
|
|
121
|
-
'correctedBoatSpeed', 'residual', 'residual.smoothed'
|
|
121
|
+
'correctedBoatSpeed', 'residual', 'residual.smoothed', 'setMagnetic'
|
|
122
122
|
]);
|
|
123
123
|
|
|
124
124
|
function getPathStatus(pathState, hasValue, computed = false, computedReady = false) {
|
|
@@ -564,6 +564,8 @@ function renderLiveSections() {
|
|
|
564
564
|
const inputAttitudes = filterById(state.attitudesAll, ['attitude']);
|
|
565
565
|
const fallbackInputPolars = inputPolars.length ? inputPolars : filterById(state.polarsAll, ['groundSpeed.smoothed']);
|
|
566
566
|
const fallbackInputDeltas = inputDeltas.length ? inputDeltas : filterById(state.deltasAll, ['heading.smoothed', 'boatSpeed.smoothed']);
|
|
567
|
+
const magneticVariationInput = filterById(state.deltasAll, ['magneticVariation']);
|
|
568
|
+
fallbackInputDeltas.push(...magneticVariationInput);
|
|
567
569
|
const fallbackInputAttitudes = inputAttitudes.length ? inputAttitudes : filterById(state.attitudesAll, ['attitude.smoothed']);
|
|
568
570
|
const displayedInputs = [...fallbackInputPolars, ...fallbackInputDeltas, ...fallbackInputAttitudes];
|
|
569
571
|
|
|
@@ -584,7 +586,7 @@ function renderLiveSections() {
|
|
|
584
586
|
? [...groundSpeedInputs, ...boatSpeedInputs]
|
|
585
587
|
: [...boatSpeedInputs, ...groundSpeedInputs];
|
|
586
588
|
estimationInputs.push(
|
|
587
|
-
...filterById(state.deltasAll, ['heading.angle']),
|
|
589
|
+
...filterById(state.deltasAll, ['heading.angle', 'magneticVariation']),
|
|
588
590
|
...filterById(state.attitudesAll, ['attitude'])
|
|
589
591
|
);
|
|
590
592
|
renderItemsInto('estimation-inputs', estimationInputs);
|
|
@@ -599,9 +601,10 @@ function renderLiveSections() {
|
|
|
599
601
|
renderGroupInto('estimation-intermediates', estimationIntermediates, [], [], statisticRows);
|
|
600
602
|
// Estimation — outputs
|
|
601
603
|
const estimationOutputs = filterById(state.polarsAll, ['correctedBoatSpeed', 'current.smoothed']);
|
|
602
|
-
|
|
604
|
+
const estimationOutputDeltas = filterById(state.deltasAll, ['setMagnetic']);
|
|
605
|
+
renderGroupInto('estimation-outputs', estimationOutputs, estimationOutputDeltas, []);
|
|
603
606
|
// Estimation — warnings
|
|
604
|
-
renderWarnings('estimation-warnings', [...estimationInputs, ...estimationIntermediates, ...estimationOutputs]);
|
|
607
|
+
renderWarnings('estimation-warnings', [...estimationInputs, ...estimationIntermediates, ...estimationOutputs, ...estimationOutputDeltas]);
|
|
605
608
|
|
|
606
609
|
// Learning — inputs
|
|
607
610
|
const learningPolars = filterById(state.polarsAll, ['groundSpeed.smoothed']);
|