speedandcurrent 2.3.4 → 2.5.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 ADDED
@@ -0,0 +1,204 @@
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.5.0] - 2026-10-26
8
+
9
+ ### Added
10
+ - Correction-table files now use version 2 with per-cell update timestamps, capped time-based aging, and silent migration of existing version-1 files.
11
+ - 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`.
12
+ - 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.
13
+ - 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.
14
+
15
+ ### Changed
16
+ - The npm package now includes this changelog.
17
+ - Updated the `signalkutilities` dependency to `^3.2.0`, including non-finite sample protection, explicit-null smoother resets, and subscription-manager-controlled startup values.
18
+ - 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)).
19
+ - Read-only Signal K users can now access live reports, status, and metadata; settings and correction-table listing or changes remain administrator-only.
20
+ - Correction-table learning no longer incorporates the estimated current, and the related setting has been removed.
21
+ - 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.
22
+ - Correction interpolation now includes mature cells within 2.5 normalized grid-cell units.
23
+ - 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.
24
+ - Cells become eligible after one accepted observation; the explicit maturity gate remains in place at zero for future tuning.
25
+
26
+ ### Fixed
27
+ - 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.
28
+ - 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.
29
+ - The learning status Reason row is now hidden when the last observation was accepted, instead of showing a redundant "Observation recorded" message.
30
+ - `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.
31
+ - 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.
32
+ - Webapp report parsing failures now remain visible and still refresh plugin status instead of aborting the polling tick.
33
+ - Disabled current estimation no longer publishes a zero-current value during plugin startup.
34
+ - Malformed correction tables now recover safely at startup, while explicit loads reject invalid files without replacing the active table.
35
+ - 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.
36
+
37
+ ## [2.3.4] - 2026-09-17
38
+
39
+ ### Fixed
40
+ - 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.
41
+ - 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.
42
+ - 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.
43
+
44
+ ## [2.3.3] - 2026-09-06
45
+
46
+ ### Fixed
47
+ - `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.
48
+ - 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.
49
+
50
+ ## [2.3.2] - 2026-09-04
51
+
52
+ ### Fixed
53
+ - 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.
54
+
55
+ ## [2.3.1] - 2026-08-18
56
+
57
+ ### Fixed
58
+ - 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.
59
+
60
+ ## [2.3.0] - 2026-08-16
61
+
62
+ ### Added
63
+ - Learning status now distinguishes between active learning and observations that were skipped, rejected, or invalid, and the webapp reports the current reason more clearly.
64
+
65
+ ### Changed
66
+ - 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.
67
+ - The learning threshold for both STW and SOG now follows the active table speed-step size.
68
+ - The webapp no longer exposes a separate minimum-SOG learning setting.
69
+
70
+ ### Fixed
71
+ - When learning is off or suspended, the webapp no longer keeps showing a stale previous observation result.
72
+
73
+ ## [2.2.1] - 2026-08-14
74
+
75
+ ### Fixed
76
+ - Input subscriptions now recover automatically after idle or stale periods, and the webapp exposes lifecycle warnings for affected paths.
77
+ - 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.
78
+ - The webapp-facing status and report endpoints now reflect the plugin's current runtime state more directly.
79
+ - SOG fallback now checks raw STW (`rawBoatSpeed`) directly when detecting a zero paddlewheel reading, avoiding dependence on the derived corrected speed value.
80
+
81
+ ## [2.1.2] - 2026-07-23
82
+
83
+ ### Fixed
84
+ - 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.
85
+ - Updated `signalkutilities` to 3.0.1, fixing continuous 401 log flooding on SK servers with security enabled and anonymous read access disabled.
86
+
87
+ ## [2.1.1] - 2026-07-20
88
+
89
+ ### Fixed
90
+ - Sidebar toggler was unreachable on mobile viewports.
91
+
92
+ ### Changed
93
+ - Updated `signalkutilities` dependency to `^3.0.0` (removes `PolarTable` from the shared library; no behaviour change for this plugin).
94
+
95
+ ## [2.1.0] - 2026-07-10
96
+
97
+ ### Added
98
+ - `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.
99
+ - `null` is written to all active output paths (current, corrected boat speed) when the plugin stops, preventing stale values on the SK bus.
100
+
101
+ ## [2.0.2] - 2026-6-18
102
+ ### Fixed
103
+ - The plugin icon was not displayed in the Signal K App Store because the icon path in the package metadata was incorrect.
104
+ - Test files are no longer included in the published npm package.
105
+
106
+ ## [2.0.1] - 2026-6-17
107
+ ### Fixed
108
+ - Correction table reporting was doing unnecessary work on every update, causing higher CPU use especially on low-power hardware such as a Raspberry Pi.
109
+ - 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.
110
+
111
+ ## [2.0.0] - 2026-6-17
112
+ ### Changed
113
+ - 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.
114
+
115
+ - **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.
116
+ - Per-path source settings (`headingSource`, `boatSpeedSource`, `SOGSource`, `attitudeSource`) have been removed. Use Signal K Server's built-in source priority configuration instead.
117
+ - The **Prevent Speed Duplication** setting has been removed. The server's `excludeSelf` behaviour and source priorities replace it.
118
+
119
+ ### Removed
120
+ - Source dropdowns in the webapp Inputs section.
121
+ - Source-related fields from persisted plugin configuration. A one-time migration strips these fields automatically on first start with the new version.
122
+
123
+ ### Added
124
+ - Documentation explaining how to configure Signal K source priorities so that the corrected `navigation.speedThroughWater` is delivered to all consumers.
125
+
126
+ ## [1.7.10] - 2026-5-14
127
+ ### Fixed
128
+ - Removed incorrect `self.` prefix from Signal K paths.
129
+
130
+ ## [1.7.9] - 2026-5-14
131
+ ### Added
132
+ - 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.
133
+ - 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.
134
+
135
+ ### Fixed
136
+ - 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.
137
+
138
+ ## [1.7.8] - 2026-5-14
139
+ ### Added
140
+ - Legend below the correction table explaining factor / leeway values, background colour coding, stripe direction, active cell, and neighbour cell indicators.
141
+
142
+ ### Fixed
143
+ - Correction table learning never started when `assumeCurrent` is off.
144
+ - Active cell highlighting (most recently updated cell) was not visible.
145
+
146
+ ## [1.7.7] - 2026-5-13
147
+ ### Added
148
+ - Optional staleness detection for inputs.
149
+ - Precise warnings on state of inputs.
150
+
151
+ ## [1.7.5] - 2025-11-19
152
+ ### Added
153
+ - New webApp.
154
+ - WebApp allows to change plugin configuration while the plugin is running.
155
+ - Support for multiple correction tables (new, copy, delete).
156
+ - Support for altering the dimensions of a correction table (alter).
157
+ - Better edge case handling.
158
+ - User selectable options for smoothing method and smoothing parameters.
159
+ - Support for unitPreferences.
160
+
161
+ ### Fixed
162
+ - Bug where navigation.speedThroughWater was removed from the signalk bus.
163
+
164
+ ### Removed
165
+ - Possibility to set options via the signalk plugin configuration.
166
+
167
+ ## [1.6.0] - 2025-11-19
168
+ ### Added
169
+ - Option to copy navigation.speedOverGround to navigation.speedThroughWater when speedThroughWater is not properly measured. Thanks to Jean-Laurent Girod for the suggestion.
170
+
171
+ ## [1.5.1] - 2025-10-06
172
+ ### Fixed
173
+ - Bug preventing current to be displayed in KIP.
174
+
175
+ ## [1.5.0] - 2025-10-06
176
+ ### Changed
177
+ - Separate loops for correcting speed and for updating correction factors.
178
+ - Speed correction loop uses raw values.
179
+ - Updating correction factors using moving averages.
180
+ - Unified cell rendering driven by "Show" selections; removed the previous Table style toggle.
181
+
182
+ ### Added
183
+ - Option to set the source for input paths.
184
+ - Full integration of uncertainty estimations (variance) in estimations and corrections.
185
+ - Ability to resize the correction table without losing all correction data.
186
+ - WebApp: "Show" multi-select to choose per-cell metrics (correction X/Y, factor, leeway, trace, N) for the correction table.
187
+ - WebApp: Color mode options for the correction table.
188
+ - WebApp: Persistence for speed unit, angle unit, color mode, and shown metrics via localStorage.
189
+
190
+ ### Fixed
191
+ - Angles are now reported in the appropriate range, 0 to 2 * PI or -PI to PI, depending on path.
192
+
193
+ ## [1.1.0] - 2025-8-20
194
+ ### Changed
195
+ - Path names for current. It now is self.environment.current.setTrue and self.environment.current.drift.
196
+
197
+ ### Fixed
198
+ - Case error in filename that prevented the webapp from displaying the correction table.
199
+ - Corrected speed through water is now calculated when the correction table is fixed.
200
+
201
+ ## [1.0.1] - 2025-1-27
202
+ ### Added
203
+ - Basic functionality to estimate speed, leeway and current using Kalman filters.
204
+ - 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
- In the current implementation the plugin is also explicit about runtime state: if an input goes idle or stale, the webapp shows a warning and the handler resubscribes; when estimation is disabled or the plugin stops, the active output paths are cleared with `null` so downstream consumers do not keep stale values.
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.
@@ -66,7 +74,7 @@ Once configured, all consumers on the Signal K bus — KIP, OpenCPN, Instrument
66
74
  | `environment.current.drift` | m/s | Estimated current speed. |
67
75
  | `environment.current.setTrue` | rad | Estimated current direction (the direction the water moves *toward*). |
68
76
 
69
- A 60-second stabilisation period applies after startup. No output is published during this window.
77
+ 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
78
 
71
79
  ---
72
80
 
@@ -104,7 +112,7 @@ The **Inputs** section shows live readings from the raw sensors as they arrive f
104
112
 
105
113
  | Setting | Default | Description |
106
114
  |---------|---------|-------------|
107
- | **Staleness Detection** | On | Mark an input as unavailable when it stops updating. Disable when testing the plugin with simulated or replayed data, where timing is irregular and would otherwise trigger spurious warnings. |
115
+ | **Input staleness handling** | Built in | Inputs are marked stale when they stop updating, and the webapp displays a warning. `navigation.state` is an event path and is not treated as stale between state transitions. |
108
116
 
109
117
  ---
110
118
 
@@ -144,23 +152,20 @@ The **Correction Table Learning** section has its own independent toggle. Learni
144
152
 
145
153
  The panel shows the **smoothed** sensor inputs used for table updates — heading, boat speed, ground speed, and attitude. These are the same signals as in the Estimation panel, but averaged over the smoother window before being fed into the learning algorithm. This averaging reduces the influence of short-term fluctuations on the table update.
146
154
 
147
- When **Assume Current** is enabled, the smoothed current estimate is also shown as a learning input.
148
-
149
155
  Warnings appear here if any smoothed input is unavailable.
150
156
 
151
- The panel also shows whether learning is currently active, suspended, or skipped for the latest observation. Learning is automatically suspended when `navigation.state` is `anchored` or `moored` when that path is available. It can also be suspended for `motoring` if enabled in the settings. Observations below the current table speed-step threshold are skipped.
157
+ The panel also shows whether learning is currently active, suspended, or skipped for the latest observation. Learning is normally gated by SOG: observations below the current table speed-step threshold are skipped. When **Suspend on navigation.state = motoring** is enabled and `navigation.state` is known, `anchored`, `moored`, and `motoring` suspend learning; an unavailable or unknown state falls back to the SOG gate.
152
158
 
153
159
  ### Settings
154
160
 
155
161
  | Setting | Default | Description |
156
162
  |---------|---------|-------------|
157
163
  | **Update Correction Table** | On | Master toggle. Allow the table to update from current observations. |
158
- | **Suspend on navigation.state = motoring** | Off | Suspend learning when `navigation.state` reports `motoring`. `anchored` and `moored` always suspend learning when that path is available. |
159
- | **Stability (1–20)** | 7 | How quickly the correction table adapts to new observations. Higher = slower, more conservative. Lower = faster but noisier. See the technical section for detail. |
160
- | **Assume Current (experimental)** | Off | Include the running current estimate in the table update calculation. Only enable once the current estimate has had time to stabilise and tidal conditions are relatively steady. |
164
+ | **Suspend on navigation.state = motoring** | Off | When enabled, suspend learning when a known `navigation.state` reports `anchored`, `moored`, or `motoring`. When disabled, `navigation.state` does not override the SOG-based moving check. |
165
+ | **Correction drift** | 0.3 knots/month | How quickly the correction may change over time (knots/month). 0.3 knots/month means the correction can typically change by about 0.3 knots over a month. Higher values adapt faster but may be less stable. Range: 0.0–3.0 knots/month. |
161
166
  | **Show Statistics (σ)** | Off | Display standard deviation alongside each smoothed value. Useful for spotting noisy sensors. |
162
167
 
163
- ### Smoother settings
168
+ ### Smoothing and learning cadence
164
169
 
165
170
  Controls how raw sensor values are averaged before being used for table updates. These settings have no effect on the published corrected values — only on the inputs to the learning algorithm.
166
171
 
@@ -168,12 +173,9 @@ The smoother serves a second purpose beyond noise reduction: its output variance
168
173
 
169
174
  | Setting | Default | Description |
170
175
  |---------|---------|-------------|
171
- | **Smoother Type** | Moving average | Moving Average (window), Exponential decay (τ), or Kalman filter. Moving average is the most predictable. |
172
- | **Window size** | 5 s | (Moving average) Integration window. Larger = smoother but slower to respond. Minimum 2 s. |
173
- | **Time constant (τ)** | 3 s | (Exponential) Decay time constant. Larger = smoother. Minimum 1 s. |
174
- | **Kalman gain** | 0.2 | (Kalman) Steady-state gain. Lower = smoother / slower. Range 0.01–0.99. |
176
+ | **Window size** | 5 s | Moving-average integration window. Larger = smoother but slower to respond. Minimum 5 s. |
175
177
 
176
- A 5-second moving average window suits typical 1 Hz instrument update rates. In rough conditions with high sensor noise, increasing it to 8–10 seconds may improve table quality at the cost of temporal resolution.
178
+ A 5-second moving average window suits typical 1 Hz instrument update rates. Learning attempts occur every `window + 1 second`, leaving a one-second gap between observation windows. In rough conditions with high sensor noise, increasing the window to 8–10 seconds may improve table quality at the cost of temporal resolution. Older configurations that selected another smoother are silently converted to moving average; their valid window duration is retained and shorter values are clamped to 5 seconds.
177
179
 
178
180
  ---
179
181
 
@@ -183,16 +185,19 @@ The **Correction Table** section displays the active correction table. Rows are
183
185
 
184
186
  ### Reading the table
185
187
 
186
- Each cell that has received at least one observation shows two values:
188
+ Each cell that has received at least one observation shows its correction values. Cells participating in the latest estimate also show their normalized fusion weight as a blue bar along the bottom:
189
+
190
+ - **Speed factor** (e.g. `+3.2%` or `−1.5%`): how much faster or slower the true speed is compared to what the paddle wheel reads.
191
+ - **Leeway** (e.g. `+4°`): the observed lateral correction for that speed/heel combination. Positive is starboard.
192
+ - **Fusion weight**: the blue bar is that cell's normalized share of the current precision-weighted estimate. A full-width bar represents 100%, and the displayed weights sum to approximately 100%.
187
193
 
188
- - **Speed factor** (e.g. `+3.2%` or `−1.5%`): how much faster or slower the true speed is compared to what the paddle wheel reads. **Green** = paddle wheel reads slow (the correction adds speed). **Orange** = paddle wheel reads fast (the correction reduces speed). Colour intensity scales with the magnitude of the factor relative to the largest factor in the table.
189
- - **Leeway** (e.g. `+4°`): the observed lateral correction for that speed/heel combination. Positive is starboard. When leeway is non-zero the cell background shows diagonal stripes; the stripe angle encodes the leeway direction visually.
194
+ Cell backgrounds encode the speed factor: green means the paddlewheel reads slow and orange means it reads fast. When leeway is available, stripe angle encodes its direction.
190
195
 
191
196
  Empty cells have not yet received any observations and show no correction.
192
197
 
193
- ### Active cell and neighbours
198
+ ### Active cell
194
199
 
195
- The **active cell** — the one most recently updated by an incoming STW sample — is highlighted with a bold border and blue text. The **neighbour cells** contributing to the current interpolation are shown with a faint tint. Watching these as you sail shows exactly which part of the table is being applied and updated at any moment.
200
+ The **active cell** — the one most recently updated by an incoming STW sample — is shown with a black border.
196
201
 
197
202
  ### Table management
198
203
 
@@ -205,13 +210,19 @@ The **active cell** — the one most recently updated by an incoming STW sample
205
210
 
206
211
  Multiple tables can coexist on disk; only the active one is used. This makes it straightforward to keep separate tables for different configurations such as racing vs. cruising sails, or before and after antifouling.
207
212
 
213
+ ### Correction-table file formats
214
+
215
+ The original format is schema version 1. It remains available as [`docs/correction-table-v1.schema.json`](docs/correction-table-v1.schema.json), while [`docs/correction-table.schema.json`](docs/correction-table.schema.json) is retained as the compatible legacy filename. New and saved tables use schema version 2, documented in [`docs/correction-table-v2.schema.json`](docs/correction-table-v2.schema.json). Version 2 adds a top-level `schemaVersion: 2`, the current serializable model descriptor, and `lastAcceptedAt` epoch milliseconds on every cell; empty cells use `null`.
216
+
217
+ Version 1 files, including older files without a version field, are migrated silently on load. Since individual legacy cell ages are unknown, the file modification time is assigned to every learned cell as a conservative approximation. If that timestamp is unavailable, load time is used and the fallback is written only to debug logging. Migration preserves table identity, dimensions, display attributes, means, covariance, and indices, and is persisted atomically only after the table has loaded successfully.
218
+
208
219
  ---
209
220
 
210
221
  ## Operating notes
211
222
 
212
223
  **Be patient with the correction table.** A fresh table has no data and produces no corrections. Cover a range of speeds and heel angles over a few sails and the table fills in progressively. Upwind sailing covers the heel bins well; downwind and reaching fill the low-heel, varying-speed bins.
213
224
 
214
- **The table learns while sailing normally.** No dedicated calibration runs are needed. Just sail with **Update Correction Table** on. Learning pauses automatically during startup stabilisation, when `navigation.state` indicates `anchored` or `moored`, optionally when it indicates `motoring`, and when SOG or STW are below the current table speed-step threshold.
225
+ **The table learns while sailing normally.** No dedicated calibration runs are needed. Just sail with **Update Correction Table** on. Learning pauses automatically during startup stabilisation and when SOG or STW are below the current table speed-step threshold. If **Suspend on navigation.state = motoring** is enabled, a known `navigation.state` of `anchored`, `moored`, or `motoring` also pauses learning.
215
226
 
216
227
  **Port and starboard are tracked independently.** Heel is signed: starboard positive, port negative. An asymmetric paddle wheel installation will show different corrections on each tack, and the table captures this naturally.
217
228
 
@@ -239,17 +250,42 @@ When a new paddle wheel sample arrives the plugin:
239
250
 
240
251
  ### Neighbour interpolation
241
252
 
242
- Rather than bi-linear interpolation the plugin uses **inverse-distance weighting** over the five nearest cells in speed/heel space:
253
+ The plugin combines learned cells using both their Kalman covariance and their distance from the requested speed and heel. Speed and heel distances are divided by their respective grid steps, so distance is measured in dimensionless cell units. A cell participates only when:
243
254
 
244
- > wᵢ = 1 / (dᵢ + ε)
255
+ ```text
256
+ cell index > 0
257
+ cell distance <= 2.5
258
+ ```
245
259
 
246
- where dᵢ is the Euclidean distance from the query point to cell centre i in normalised speed/heel space. Only cells that have received at least one observation (N > 0) contribute.
260
+ For each participating cell, distance is converted into additional uncertainty:
247
261
 
248
- The final correction is the normalised weighted average:
262
+ ```text
263
+ effective covariance = cell covariance + q * distance² * identity matrix
264
+ ```
249
265
 
250
- > x̂ = Σ(wᵢ · xᵢ) / Σwᵢ, ŷ = Σ(wᵢ · yᵢ) / Σwᵢ
266
+ The inverse effective covariance is the cell's precision. The correction and its covariance are obtained by full 2×2 precision fusion:
251
267
 
252
- The variance of each axis is propagated through the same weighting, so cells with low confidence (high covariance) influence the result less. If only one or two cells have data, the correction extrapolates gradually to nearby conditions. As more cells fill in, the correction becomes tighter and more local.
268
+ ```text
269
+ combined precision = sum(inverse(effective covariance))
270
+ correction covariance = inverse(combined precision)
271
+ correction = correction covariance
272
+ * sum(inverse(effective covariance) * cell correction)
273
+ ```
274
+
275
+ This gives nearby, mature cells the greatest influence. A fresh table borrows information more broadly because cell covariance is still large; a mature table becomes more local as its cell covariance falls. If no eligible cell exists, the plugin applies zero correction rather than extrapolating beyond the configured radius.
276
+
277
+ #### Automatic spatial variance (`q`)
278
+
279
+ `q` describes how much genuine correction variation is expected between adjacent cells, in correction variance per squared cell. It is calculated without user input from horizontal and vertical pairs where both cells have an index greater than 0. A cell with one accepted observation is therefore eligible while the explicit maturity threshold remains available for future tuning. For each adjacent pair:
280
+
281
+ ```text
282
+ pair q = (
283
+ squared distance between the two correction vectors
284
+ - trace(cell covariance 1 + cell covariance 2)
285
+ ) / 2
286
+ ```
287
+
288
+ The table uses the median pair estimate, which limits the effect of isolated rough or immature cells. At least three eligible adjacent pairs and a positive median are required. A zero or negative estimate means the observed differences do not resolve spatial variation above the cells' estimated uncertainty; because using zero would disable distance weighting, the plugin then uses a conservative fallback of `0.002 (m/s)²` per squared cell. `q` is calculated when a table is loaded and recalculated after every 600 accepted learning observations. The applied `q`, raw estimate, source, and supporting pair count are runtime diagnostics and are not saved in the correction-table file.
253
289
 
254
290
  ### How the correction table is populated
255
291
 
@@ -257,43 +293,44 @@ Each cell is an independent **2-dimensional Kalman filter** tracking the correct
257
293
 
258
294
  Every time conditions are right — plugin running for >60 seconds, smoothers settled, speed above minimum threshold — the plugin computes an **observation** of what the correction should be:
259
295
 
260
- > observation = R(ψ)⁻¹ · V_SOG − R(ψ)⁻¹ · V_current − V_STW
296
+ ```text
297
+ observation = R(ψ)⁻¹ · V_SOG − R(ψ)⁻¹ · V_current − V_STW
298
+ ```
261
299
 
262
- 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.
300
+ 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.
263
301
 
264
302
  The Kalman update combines this observation with the cell's existing belief:
265
303
 
266
- > K = P · (P + R_obs)⁻¹
267
- > x_new = x_old + K · (observation − x_old)
304
+ ```text
305
+ K = P · (P + R_obs)⁻¹
306
+ x_new = x_old + K · (observation − x_old)
307
+ ```
268
308
 
269
- 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). **Noisy observations produce a smaller gain and move the cell estimate less.**
309
+ 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.**
270
310
 
271
- ### The stability setting in detail
311
+ ### Time-scaled aging and correction drift
272
312
 
273
- Each cell has a small **process noise** that allows it to drift slowly over time, reflecting that a paddle wheel's error can change with fouling, recalibration, or crew weight distribution. Process noise is:
313
+ 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:
274
314
 
275
- > Q = 10^(−stability)
315
+ ```text
316
+ effectiveDt = min(dt, 90 days)
317
+ Q(dt) = Q_rate × effectiveDt × I
318
+ ```
276
319
 
277
- With stability = 7 (default), Q = 10⁻⁷, making the cell very resistant to change — it effectively averages hundreds of observations before settling. Stability 4–5 makes the table react faster to recent conditions; stability 10–12 is appropriate for a well-characterised boat that changes rarely.
320
+ The user-facing `Correction drift` value is expressed in knots per month, using a 30-day month. Internally, it is converted to the SI process-noise rate in $(m/s)^2/s$. The default 0.3 knots/month corresponds to approximately $9.2 × 10^{-9}\ (m/s)^2/s$. Offline time counts toward aging, but aging stops after the internal 90-day cap. Aged covariance is calculated when gating, filtering, interpolation, spatial-q estimation, and reporting; stored covariance is not mutated by reads. Rejected observations do not advance timestamps; accepted observations store the posterior covariance and current UTC epoch-millisecond timestamp.
278
321
 
279
- In practical terms: high stability = trust the accumulated history; low stability = trust recent observations more.
322
+ In practical terms: a lower correction-drift rate trusts accumulated history more; a higher rate allows the table to adapt more quickly to long-term sensor changes.
280
323
 
281
324
  ### How current is estimated
282
325
 
283
326
  Current is estimated as:
284
327
 
285
- > V_current = V_SOG − R(ψ) · V_STW_corrected
328
+ ```text
329
+ V_current = V_SOG − R(ψ) · V_STW_corrected
330
+ ```
286
331
 
287
332
  The corrected STW vector is rotated into the ground frame using heading, then subtracted from the GPS velocity. The residual is the water velocity.
288
333
 
289
334
  This raw estimate is fed into a **Kalman smoother with very low process noise** (process variance ≈ 10⁻⁶), so it changes very slowly, integrating over many minutes rather than chasing individual GPS fluctuations. At startup the estimate is strongly initialised to zero.
290
335
 
291
336
  Current estimation requires an accurate boat speed, so it is gated by the 60-second stabilisation period and only runs when **Estimate Boat Speed** is enabled.
292
-
293
- ### Current and table learning
294
-
295
- Current estimation always runs alongside speed correction — it is a direct byproduct of comparing the GPS velocity with the corrected STW vector. You cannot have one without the other.
296
-
297
- For table *learning*, however, current can be left out of the equation without significant harm. When a boat sails different headings over time — tacking, gybing, reaching — any steady current appears as an error in one direction on one heading and the opposite direction on another. These errors cancel in the long-term average, so the table converges on the correct speed correction regardless. This is why the default (**Assume Current** off) uses a zero-current placeholder rather than the live estimate.
298
-
299
- Including the current estimate (**Assume Current** on) would be more accurate in principle, but it introduces a dependency: the current estimate is only as good as the speed correction that produced it. Early in the table's life, when corrections are rough, the current estimate is also rough, and feeding it back into learning can amplify rather than reduce error. There is also a circularity: better speed correction → better current estimate → better speed correction. Enabling this before the table has had time to settle can cause the two to pull each other in the wrong direction, particularly in changing tidal conditions. Use it only once the table is reasonably well populated and current conditions are stable.