@meri-imperiumi/signalk-dead-reckoning 0.2.0 → 0.3.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 CHANGED
@@ -5,9 +5,102 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-08-27
9
+
10
+ ### Added
11
+ - **`inertial-polar` DR speed fallback (SPEC §3.1, work doc #18)** —
12
+ when the paddlewheel is unusable (`navigation.speedThroughWater`
13
+ missing, or the debounced §6.3 fouling verdict active), DR integrates
14
+ speed from the polar performance plugin's `performance.polarSpeed`
15
+ delta instead of freezing: requires `signalk-polar-performance-plugin`
16
+ installed and configured with its polar speed output enabled; the
17
+ super-jittery raw delta is running-averaged (default 60 s window,
18
+ 30 s staleness cutoff) before integration. Gated to underway+sailing
19
+ (no wind-on-mast drift at the dock, no meaningless polar under
20
+ power); no matrix corrections while on polar (bins were trained on
21
+ real STW — a model estimate is circular input); uncertainty grows at
22
+ the fallback rate; Training Mode and maneuver detection suspended;
23
+ the divergence advisory keeps watching. `navigation.speedThroughWater`
24
+ stays silent while on polar (a model estimate is not a measurement).
25
+ The DR state value gains `speedSource: "paddlewheel"|"polar"`, and a
26
+ §3.1 sensor-health alert names the switch (paddlewheel
27
+ unavailable/fouled — DR on polar-derived speed).
28
+ - Scalar sibling paths `navigation.deadReckoning.uncertainty.radius`
29
+ and `navigation.deadReckoning.divergence.distance` (metres, with
30
+ nautical-mile display-unit meta) for rule/display engines
31
+ (signalk-status-tiles threshold checks) that cannot read subfields of
32
+ object-valued paths — subscribing to a subfield path never sees a
33
+ delta.
34
+
35
+ ### Changed
36
+ - **All published deltas now follow the Signal K SI unit conventions**
37
+ (breaking):
38
+ - `navigation.deadReckoning.log` / `trip.log` publish metres (was
39
+ nautical miles), with `value/1852` NM display-unit meta.
40
+ - The `environment.current` object is replaced by the standard
41
+ `environment.current.setTrue` (radians) and
42
+ `environment.current.drift` (m/s) paths; the DR-specific
43
+ tier/source enrichment rides REST `/status` instead of the bus.
44
+ - The uncertainty object field `radius_nm` becomes `radius_m`; the
45
+ divergence object fields `distance_nm`/`bearing_true` (deg) become
46
+ `distance_m`/`bearing_true` (rad).
47
+ - `navigation.deadReckoning.elapsedSinceFix` gains duration
48
+ display-unit meta so glance consumers render "3h 05m", not
49
+ seconds.
50
+ - All display-unit meta declares `category: "custom"` — the
51
+ server's unit-preference system rewrites category-less
52
+ `displayUnits` to the user's global preference (a seconds path
53
+ rendered as "0.0 hour"); custom keeps the nautical styling.
54
+ - **Inbound sensor deltas are now interpreted per the Signal K unit
55
+ conventions** (breaking for feeds that were publishing non-SI):
56
+ `speedThroughWater`/`speedApparent` are read as m/s and heading paths
57
+ as radians, converting to the engine's internal knots/degrees at the
58
+ boundary. Previously m/s and radian values were treated as knots and
59
+ degrees — DR under-travelled ~5× and mis-steered on standard feeds.
60
+ The standard-path passthroughs (`navigation.speedThroughWater`,
61
+ `navigation.headingTrue`) publish what they received, unchanged.
62
+ - Logbook fix/tack entries convert SOG/COG/heading at the boundary
63
+ (`_kn`/`_deg` REST fields previously received raw m/s/radians).
64
+ - The webapp converts SI bus values (m, m/s, rad) to nautical displays
65
+ centrally in the view model (`metresToNm`/`msToKn`/`radToDeg`).
66
+ - REST `/status` and `/current/manual` keep the plugin's internal
67
+ nautical units (`logNm`, `current.setTrue` deg, `drift` kn) — they
68
+ are the plugin's own API, not the Signal K bus.
69
+ - `navigation.deadReckoning.method` now reflects the actual speed
70
+ source every tick, completing the SPEC §3.1 enum: the idle branch
71
+ (no usable speed at all) publishes `fallback-zero` instead of
72
+ inheriting the constructor's `inertial-paddlewheel` — the `Polar`/
73
+ `Zero` headline labels added earlier are now driven by real values.
74
+ SPEC §3.1/§3.2/§6.1 aligned with the implemented fallback hierarchy
75
+ (polar first, fault-based selection, speed output silent on polar).
76
+ - The "Active method" headline shows a short watchkeeper-sized label
77
+ (`STW` for `inertial-paddlewheel`, `Polar`/`Zero` for the spec's
78
+ reserved methods) with the full token on hover — one of five figures,
79
+ it previously gave a full spec token permanent large-type real estate
80
+ despite having exactly one possible value today.
81
+ - The DR status no longer claims the vessel is `underway` while
82
+ moored: `navigation.deadReckoning.state.status` gains a `"warm"`
83
+ value (engine integrating on a tied-up boat, SPEC §5 runs it warm for
84
+ instant OVERRIDE handoff — that is not being under way) alongside
85
+ `"underway"` and `"idle"`. The webapp subscribes to the vessel's own
86
+ `navigation.state` (already in the system — nothing is republished
87
+ inside our deltas) and words the line accordingly:
88
+ `DR warm — moored/anchored, integrating sensors` vs
89
+ `Dead reckoning active`. Text logic moved to the pure `drStatusText()`
90
+ view-model helper.
91
+
8
92
  ## [0.2.0] - 2026-08-27
9
93
 
10
94
  ### Fixed
95
+ - **Map no longer opens blank — defaults to the first chart provider.**
96
+ The webapp previously kept its tile-less offline-first default unless
97
+ the server had *configured* charts, so on servers without any (the
98
+ common case: the layers control showed only "OpenStreetMap (online)",
99
+ unselected) the plot opened on an empty dark canvas. The first entry
100
+ of the chart list — first configured chart when the server has them,
101
+ otherwise the OSM online fallback — is now auto-selected on load, and
102
+ duplicate chart names no longer clobber each other's entry in the
103
+ layers control.
11
104
  - **Dockside false positives while moored/anchored** — three alerts
12
105
  that misfire on a tied-up boat are now suppressed in the moored/
13
106
  anchored regime (consistent with the divergence monitor's existing
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Signal K Dead Reckoning
2
2
 
3
- An offline-first dead reckoning and sensor fusion engine for Signal K that maintains a continuously computed "shadow boat" position from water-track sensors (speed through water, compass heading, and learned leeway and current corrections), so you always have a navigational fallback when GPS becomes unreliable — whether from jamming, spoofing, or plain receiver failure. While GPS is trusted, the engine learns vessel-specific calibration corrections against ground truth and watches for GPS anomalies; when it isn't, the same learned model keeps the dead-reckoned position, its uncertainty polygon, and a water-track log going. Fixes from celestial sights, compass bearings, and vertical angles are entered through a unified pipeline and can snap dead reckoning back on track, with optional write-through to `signalk-logbook`. Historical data can be backfilled to train the calibration model and backtest it against past passages. Licensed under the EUPL-1.2.
3
+ An offline-first dead reckoning and sensor fusion engine for Signal K that maintains a continuously computed "shadow boat" position from water-track sensors (speed through water, compass heading, and learned leeway and current corrections), so you always have a navigational fallback when GPS becomes unreliable — whether from jamming, spoofing, or plain receiver failure. While GPS is trusted, the engine learns vessel-specific calibration corrections against ground truth and watches for GPS anomalies; when it isn't, the same learned model keeps the dead-reckoned position, its uncertainty polygon, and a water-track log going. Fixes from celestial sights, compass bearings, and vertical angles are entered through a unified pipeline and can snap dead reckoning back on track, with optional write-through to `signalk-logbook`.
4
4
 
5
5
  **Note:** This is just a toy. Make your own navigation calculations and decisions.
package/SPEC.md CHANGED
@@ -38,10 +38,10 @@ The three motivations are causally linked: (1) generates the fix data and crew e
38
38
  |---|---|---|
39
39
  | `navigation.deadReckoning.position` | `{ latitude, longitude, altitude }` | **Always-on** 1Hz inertial "shadow boat" position — computed continuously regardless of mode. |
40
40
  | `navigation.deadReckoning.active` | boolean | Whether DR is currently the *authoritative* source feeding `navigation.position` (i.e. OVERRIDE engaged), as distinct from merely running in the background. |
41
- | `navigation.deadReckoning.method` | string: `inertial-polar` \| `inertial-paddlewheel` \| `fallback-zero` | Active state calculation mode. |
41
+ | `navigation.deadReckoning.method` | string: `inertial-polar` \| `inertial-paddlewheel` \| `fallback-zero` | Active speed-source mode, re-published every tick: `inertial-paddlewheel` while the paddlewheel serves (raw STW present, not fouled); `inertial-polar` when it doesn't and the polar fallback can engage (§6.1); `fallback-zero` when no usable speed source exists (DR holds position). Selection is fault-based, never merit-based — a working measurement always outranks a model. |
42
42
  | `navigation.deadReckoning.log` | number (nm) | Cumulative **water-track** distance, integrated from STW. Independent of GPS. |
43
43
  | `navigation.deadReckoning.trip.log` | number (nm) | Same, reset at trip boundaries (see §9.2). |
44
- | `navigation.speedThroughWater` | number | Calibrated STW output (matrix-corrected). |
44
+ | `navigation.speedThroughWater` | number | Calibrated STW output (matrix-corrected). Silent while the polar fallback is active — a model estimate must not masquerade as a measurement. |
45
45
  | `navigation.headingTrue` | number | Calibrated true heading (corrected for dynamic deviation). |
46
46
  | `environment.current` | `{ setTrue, drift, meta: { source, expiresAt } }` | Current vector broadcast, per §6.2 hierarchy. |
47
47
  | `notifications.navigation.gpsSpoofed` | alarm state | High-severity: sudden position discontinuity inconsistent with DR/physics, or at-anchor/moored displacement beyond plausible bound. See §7. |
@@ -55,6 +55,7 @@ The three motivations are causally linked: (1) generates the fix data and crew e
55
55
  | `navigation.position` | GPS baseline for training mode and anomaly detection. |
56
56
  | `navigation.speedThroughWater`, `navigation.headingMagnetic`, `navigation.attitude` | Raw sensor inputs (heel/pitch). |
57
57
  | `environment.wind.angleApparent`, `environment.wind.speedApparent` | Wind inputs for leeway/upwash modeling. |
58
+ | `performance.polarSpeed` | Polar-derived boat speed (m/s) from `signalk-polar-performance-plugin` — requirement for the §6.1 `inertial-polar` fallback: that plugin installed and configured with its polar-speed output enabled. The raw delta is a step-function polar lookup driven by gusty wind and is **running-averaged** before integration; sustained nulls (wind out-of-table) age the average out to staleness rather than decaying it toward zero. |
58
59
  | `navigation.sails` | Active sail configuration, from `signalk-logbook`. |
59
60
  | `environment.seaState` | Sea state tier, from logbook watch entries. |
60
61
  | `navigation.state` | `anchored` \| `moored` \| `sailing` \| `motoring` \| ... — trip boundaries, and the anchored/moored anomaly-detection gates. |
@@ -273,7 +274,14 @@ Worker Thread (DR Physics)
273
274
 
274
275
  **Inference Mode** — active when `isGpsReliable = false` OR OVERRIDE is manually engaged:
275
276
  - Freezes matrix learning. Reads raw sensors, looks up matching bins, applies corrections, integrates the resolved current vector, publishes `navigation.deadReckoning.position` as authoritative (`navigation.deadReckoning.active = true`).
276
- - If the paddlewheel is fouled during Inference Mode, falls back to GPS-SOG-derived speed (if GPS is at least partially available) or holds last-known-good STW with explicitly faster-growing uncertainty (if not) — distinct fallback branch from the "GPS unreliable" case, since the two can occur independently or together.
277
+
278
+ **Speed-source fallback** — mode-independent (the shadow boat integrates regardless of GPS trust, §5, so a sensor lost in NORMAL mode is handled the same way as one lost in Inference/OVERRIDE). A fouled or silent paddlewheel degrades the speed source by preference:
279
+
280
+ 1. **Polar-derived speed** — the running-averaged `performance.polarSpeed` (§3.2). Gated to underway + sailing: wind on a moored mast must not sail the shadow boat off the dock, and a polar is meaningless under power. While on this source: no matrix corrections (leeway/speed-loss bins were trained on real paddlewheel STW — feeding them a model estimate is circular), uncertainty grows at the fallback rate, and matrix training / maneuver detection are suspended (never train on synthetic input — same principle as the §6.3/§6.4 write gates). The divergence advisory (§7.3) keeps watching: model drift must be detected even though it never re-selects the method.
281
+ 2. **GPS-SOG-derived speed** — if no polar source is available and GPS is at least partially available.
282
+ 3. **Hold last-known-good STW** — with explicitly faster-growing uncertainty, if neither.
283
+
284
+ The paddlewheel-failure fallback is a distinct branch from the "GPS unreliable" case, since the two can occur independently or together.
277
285
 
278
286
  ### 6.2 Current Hierarchy of Truth
279
287
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meri-imperiumi/signalk-dead-reckoning",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Offline-first dead reckoning and sensor fusion engine for Signal K",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
@@ -33,7 +33,8 @@
33
33
  ],
34
34
  "recommends": [
35
35
  "@meri-imperiumi/signalk-autostate",
36
- "@meri-imperiumi/signalk-logbook"
36
+ "@meri-imperiumi/signalk-logbook",
37
+ "signalk-polar-performance-plugin"
37
38
  ],
38
39
  "screenshots": [
39
40
  "./doc/dr-map.png",