@meri-imperiumi/signalk-dead-reckoning 0.2.0 → 0.4.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +37 -1
  3. package/SPEC.md +11 -3
  4. package/package.json +3 -2
  5. package/plugin/index.js +480 -64
  6. package/plugin/plotterext.js +194 -0
  7. package/plugin/polar.js +85 -0
  8. package/plugin/shadow-vessel.js +179 -0
  9. package/plugin/statustilesexamples.js +109 -0
  10. package/public/dr-app.js +55 -32
  11. package/public/dr-ext-model.js +84 -0
  12. package/public/dr-ext-widget.html +29 -0
  13. package/public/dr-ext-widget.js +275 -0
  14. package/public/dr-map-view.js +250 -36
  15. package/public/dr-sight-panel.js +11 -3
  16. package/public/dr-viewmodel.js +469 -17
  17. package/public/index.html +5 -0
  18. package/public/vendor/maplibre-gl/LICENSE-leaflet-maplibre-gl.txt +15 -0
  19. package/public/vendor/maplibre-gl/LICENSE-maplibre-gl.txt +116 -0
  20. package/public/vendor/maplibre-gl/README.md +19 -0
  21. package/public/vendor/maplibre-gl/leaflet-maplibre-gl.js +234 -0
  22. package/public/vendor/maplibre-gl/maplibre-gl.css +1 -0
  23. package/public/vendor/maplibre-gl/maplibre-gl.js +59 -0
  24. package/public/vendor/plotterext-bus/LICENSE +21 -0
  25. package/public/vendor/plotterext-bus/README.md +12 -0
  26. package/public/vendor/plotterext-bus/chunk-4W6N34SD.js +318 -0
  27. package/public/vendor/plotterext-bus/chunk-7XRFPDQL.js +267 -0
  28. package/public/vendor/plotterext-bus/extension.js +31 -0
  29. package/status-tiles-examples.json +120 -0
  30. package/tests/dr-current.test.js +2 -1
  31. package/tests/dr-ext-model.test.js +96 -0
  32. package/tests/dr-viewmodel.test.js +78 -4
  33. package/tests/fake-app.js +21 -0
  34. package/tests/plotterext.test.js +188 -0
  35. package/tests/plugin.test.js +426 -34
  36. package/tests/polar.test.js +106 -0
  37. package/tests/shadow-vessel-integration.test.js +201 -0
  38. package/tests/shadow-vessel.test.js +209 -0
  39. package/tests/statustilesexamples.test.js +174 -0
  40. package/tests/tile-paths.test.js +258 -0
  41. package/tests/vector-charts.test.js +516 -0
package/CHANGELOG.md CHANGED
@@ -5,9 +5,189 @@ 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.4.0] - 2026-08-28
9
+
10
+ ### Added
11
+ - **Example Status Tiles set (work doc #22)** — the plugin now ships a
12
+ ready-made Status Tiles set (`status-tiles-examples.json`) that a
13
+ boat owner copies into their panel with one tap — no JSON editing,
14
+ no hand-authoring predicates. The set exposes the underway DR
15
+ integrity tile: the sensor-health and divergence-advisory
16
+ notifications, the DR-override (`navigation.deadReckoning.active`)
17
+ state, and banded checks on the uncertainty radius (1 nm warn / 3 nm
18
+ crit) and time-since-fix (3 h / 6 h), with footer readouts for
19
+ uncertainty, DR–GPS divergence, and the DR method. It is advertised
20
+ through the standard resources API as a read-only
21
+ `statusTileExamples` provider (keyed by the plugin id, returning `{}`
22
+ when stopped so a disabled plugin contributes no stale sets),
23
+ which the signalk-status-tiles webapp discovers and offers as a
24
+ one-tap "Add" that merges into the user's config (skipping existing
25
+ ids — idempotent, never overwrites user edits). Mirrors the existing
26
+ plotter-extension provider pattern; no running server is required to
27
+ ship or consume it.
28
+ - **Shadow vessel on chart plotters (work doc #21)** — the DR
29
+ shadow-boat position is now published as a synthetic vessel on a
30
+ stable `vessels.<uuid>` context, so chart plotters that source
31
+ vessels from the Signal K stream (Freeboard-SK's AIS layer) render
32
+ it on the chart alongside the own vessel and AIS traffic, with a
33
+ projected COG line. Freeboard-SK draws it as a green `ais_buddy`
34
+ target — the only per-target data-driven visual lever in its AIS
35
+ style decision tree is the `buddy` flag, which the plugin sets on the
36
+ root-value delta. The gap between the shadow's heading (bow) and its
37
+ COG line visualizes set + leeway, mirroring how the plotter treats
38
+ the own vessel. Opt-in via *Shadow Vessel* in the plugin settings
39
+ (off by default); the configurable label defaults to "DR Shadow".
40
+ The plugin stays the source of truth — the shadow is an additional
41
+ chart-visible target, not a navigational authority. The context UUID
42
+ is generated once and persisted so the plotter's target id is
43
+ continuous across restarts. While DR is idle (moored, no speed or
44
+ heading) the shadow keeps publishing the last position at SOG 0 so
45
+ it doesn't vanish or go stale, and the COG line clears. No
46
+ Freeboard-SK change is required.
47
+ - **Vector chart tiles in the webapp map (work doc #20)** — charts
48
+ with `format: 'pbf'` in the server's configured charts resource
49
+ (NOAA ENC-derived S-57 MBTiles, an Open Waters passage cache, any
50
+ vector MBTiles served by `signalk-charts-provider-simple`) now
51
+ render instead of failing silently as broken image tiles. MapLibre
52
+ GL JS 5.24.0 and the official Leaflet bridge are vendored under
53
+ `public/vendor/maplibre-gl/` (same policy as Leaflet — no CDN, no
54
+ build step) and mounted via `L.maplibreGL`, so all DR overlays stay
55
+ plain Leaflet. When the corridor downloader's asset manifest
56
+ (`/plugins/signalk-corridor-tile-downloader/assets/manifest.json`)
57
+ advertises a mirrored Open Waters `style` URL, that style is mounted
58
+ wholesale — full marine symbology (buoys by shape and colour,
59
+ lights, restricted-area hatching), bathymetry contours and soundings
60
+ with labels, base map and hillshade, online and offline alike —
61
+ while terrarium-DEM (`webp`) stores stay mirror internals and never
62
+ appear as overlays. For sources without a manifest the style is
63
+ composed client-side by the pure `maplibreStyleFor` (view-model):
64
+ one vector source on the chart's absolute tile URL (MapLibre's
65
+ blob-URL workers can't resolve relative paths) with native max zoom
66
+ — vector charts keep rendering past the raster maxNativeZoom
67
+ ceiling — plus a dark sea background and geometry-only
68
+ fill/line/circle layers with marine styling for known source-layer
69
+ families (S-57 LNDARE/DEPARE/DEPCNT/COALNE/SOUNDG ids and the
70
+ OSM-marine names the corridor cache carries: seamark, waterway,
71
+ wetland, sea_area, light as amber dots). Chart-mount failures are
72
+ logged instead of vanishing. Right-clicking a charted symbol
73
+ (lighthouse, seamark, peak, island) resolves its charted name and
74
+ seeds the sight form's *Object* field — bearings are taken to
75
+ identified objects, not bare coordinates.
76
+ - **Plotter-extension status tile for Freeboard-SK (work doc #19)** —
77
+ the plugin now registers a `plotterExtensions` resource provider
78
+ (Plotter Extensions API v1) so chart plotters that host the
79
+ mechanism (Freeboard-SK ≥ 3.0) offer a "Dead Reckoning" widget:
80
+ a 2×1 glanceable tile showing GPS↔DR divergence, the DR uncertainty
81
+ radius, fix cadence and DR health (tracking / maneuver / fouled /
82
+ stale / warm), colored by severity like the webapp status line, with
83
+ an OVERRIDE badge when DR is authoritative and an amber night-mode
84
+ palette following the host's `nightMode` capability. Live values
85
+ arrive through the host's multiplexed Signal K relay (the tile opens
86
+ no connection of its own). The iframe assets are served from a new
87
+ publicly readable `/plotterext/signalk-dead-reckoning/` route (a
88
+ minimal in-repo static handler — the plugin stays zero-dependency);
89
+ discovery is the authenticated resources API, enablement is the
90
+ plugin's own enable switch. The bus client
91
+ (`signalk-plotterext-bus` 0.11.0, MIT) is vendored under
92
+ `public/vendor/plotterext-bus/` like Leaflet. The standalone webapp
93
+ is unaffected.
94
+
95
+ ## [0.3.0] - 2026-08-27
96
+
97
+ ### Added
98
+ - **`inertial-polar` DR speed fallback (SPEC §3.1, work doc #18)** —
99
+ when the paddlewheel is unusable (`navigation.speedThroughWater`
100
+ missing, or the debounced §6.3 fouling verdict active), DR integrates
101
+ speed from the polar performance plugin's `performance.polarSpeed`
102
+ delta instead of freezing: requires `signalk-polar-performance-plugin`
103
+ installed and configured with its polar speed output enabled; the
104
+ super-jittery raw delta is running-averaged (default 60 s window,
105
+ 30 s staleness cutoff) before integration. Gated to underway+sailing
106
+ (no wind-on-mast drift at the dock, no meaningless polar under
107
+ power); no matrix corrections while on polar (bins were trained on
108
+ real STW — a model estimate is circular input); uncertainty grows at
109
+ the fallback rate; Training Mode and maneuver detection suspended;
110
+ the divergence advisory keeps watching. `navigation.speedThroughWater`
111
+ stays silent while on polar (a model estimate is not a measurement).
112
+ The DR state value gains `speedSource: "paddlewheel"|"polar"`, and a
113
+ §3.1 sensor-health alert names the switch (paddlewheel
114
+ unavailable/fouled — DR on polar-derived speed).
115
+ - Scalar sibling paths `navigation.deadReckoning.uncertainty.radius`
116
+ and `navigation.deadReckoning.divergence.distance` (metres, with
117
+ nautical-mile display-unit meta) for rule/display engines
118
+ (signalk-status-tiles threshold checks) that cannot read subfields of
119
+ object-valued paths — subscribing to a subfield path never sees a
120
+ delta.
121
+
122
+ ### Changed
123
+ - **All published deltas now follow the Signal K SI unit conventions**
124
+ (breaking):
125
+ - `navigation.deadReckoning.log` / `trip.log` publish metres (was
126
+ nautical miles), with `value/1852` NM display-unit meta.
127
+ - The `environment.current` object is replaced by the standard
128
+ `environment.current.setTrue` (radians) and
129
+ `environment.current.drift` (m/s) paths; the DR-specific
130
+ tier/source enrichment rides REST `/status` instead of the bus.
131
+ - The uncertainty object field `radius_nm` becomes `radius_m`; the
132
+ divergence object fields `distance_nm`/`bearing_true` (deg) become
133
+ `distance_m`/`bearing_true` (rad).
134
+ - `navigation.deadReckoning.elapsedSinceFix` gains duration
135
+ display-unit meta so glance consumers render "3h 05m", not
136
+ seconds.
137
+ - All display-unit meta declares `category: "custom"` — the
138
+ server's unit-preference system rewrites category-less
139
+ `displayUnits` to the user's global preference (a seconds path
140
+ rendered as "0.0 hour"); custom keeps the nautical styling.
141
+ - **Inbound sensor deltas are now interpreted per the Signal K unit
142
+ conventions** (breaking for feeds that were publishing non-SI):
143
+ `speedThroughWater`/`speedApparent` are read as m/s and heading paths
144
+ as radians, converting to the engine's internal knots/degrees at the
145
+ boundary. Previously m/s and radian values were treated as knots and
146
+ degrees — DR under-travelled ~5× and mis-steered on standard feeds.
147
+ The standard-path passthroughs (`navigation.speedThroughWater`,
148
+ `navigation.headingTrue`) publish what they received, unchanged.
149
+ - Logbook fix/tack entries convert SOG/COG/heading at the boundary
150
+ (`_kn`/`_deg` REST fields previously received raw m/s/radians).
151
+ - The webapp converts SI bus values (m, m/s, rad) to nautical displays
152
+ centrally in the view model (`metresToNm`/`msToKn`/`radToDeg`).
153
+ - REST `/status` and `/current/manual` keep the plugin's internal
154
+ nautical units (`logNm`, `current.setTrue` deg, `drift` kn) — they
155
+ are the plugin's own API, not the Signal K bus.
156
+ - `navigation.deadReckoning.method` now reflects the actual speed
157
+ source every tick, completing the SPEC §3.1 enum: the idle branch
158
+ (no usable speed at all) publishes `fallback-zero` instead of
159
+ inheriting the constructor's `inertial-paddlewheel` — the `Polar`/
160
+ `Zero` headline labels added earlier are now driven by real values.
161
+ SPEC §3.1/§3.2/§6.1 aligned with the implemented fallback hierarchy
162
+ (polar first, fault-based selection, speed output silent on polar).
163
+ - The "Active method" headline shows a short watchkeeper-sized label
164
+ (`STW` for `inertial-paddlewheel`, `Polar`/`Zero` for the spec's
165
+ reserved methods) with the full token on hover — one of five figures,
166
+ it previously gave a full spec token permanent large-type real estate
167
+ despite having exactly one possible value today.
168
+ - The DR status no longer claims the vessel is `underway` while
169
+ moored: `navigation.deadReckoning.state.status` gains a `"warm"`
170
+ value (engine integrating on a tied-up boat, SPEC §5 runs it warm for
171
+ instant OVERRIDE handoff — that is not being under way) alongside
172
+ `"underway"` and `"idle"`. The webapp subscribes to the vessel's own
173
+ `navigation.state` (already in the system — nothing is republished
174
+ inside our deltas) and words the line accordingly:
175
+ `DR warm — moored/anchored, integrating sensors` vs
176
+ `Dead reckoning active`. Text logic moved to the pure `drStatusText()`
177
+ view-model helper.
178
+
8
179
  ## [0.2.0] - 2026-08-27
9
180
 
10
181
  ### Fixed
182
+ - **Map no longer opens blank — defaults to the first chart provider.**
183
+ The webapp previously kept its tile-less offline-first default unless
184
+ the server had *configured* charts, so on servers without any (the
185
+ common case: the layers control showed only "OpenStreetMap (online)",
186
+ unselected) the plot opened on an empty dark canvas. The first entry
187
+ of the chart list — first configured chart when the server has them,
188
+ otherwise the OSM online fallback — is now auto-selected on load, and
189
+ duplicate chart names no longer clobber each other's entry in the
190
+ layers control.
11
191
  - **Dockside false positives while moored/anchored** — three alerts
12
192
  that misfire on a tied-up boat are now suppressed in the moored/
13
193
  anchored regime (consistent with the divergence monitor's existing
package/README.md CHANGED
@@ -1,5 +1,41 @@
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.
6
+
7
+ ## Requirements
8
+
9
+ * This plugin installed and configured
10
+ * Some chart source available to Signal K
11
+ * Hand bearing compass and/or sextant
12
+
13
+ ## Getting started
14
+
15
+ 1. Install the plugin from the Signal K Appstore (or `npm install @meri-imperiumi/signalk-dead-reckoning`) and enable it. The defaults are sane — you only need to revisit the configuration if you want to change intervals or integrations.
16
+ 2. Make sure the required data feeds are available on the vessel network:
17
+ * `navigation.position` (GPS) — the training baseline and anomaly reference
18
+ * `navigation.speedThroughWater` (paddlewheel log) and `navigation.headingMagnetic` (compass + variation) — the water-track inputs
19
+ * Wind data — used by the calibration model and the polar speed fallback
20
+ 3. Just sail. While GPS is reliable and you're under sail (propulsion stopped), the engine continuously learns vessel-specific leeway and speed-loss corrections against GPS ground truth. There is nothing to activate: the "shadow boat" runs at all times, and the uncertainty polygon tightens over a season as the calibration bins fill up. Early on, expect conservative uncertainty estimates.
21
+ 4. Optional integrations, configured in the plugin settings:
22
+ * `signalk-logbook` — write confirmed fixes and maneuvers to the vessel's logbook
23
+ * `signalk-polar-performance-plugin` — polar-derived speed when the paddlewheel is fouled or silent
24
+ * `@meri-imperiumi/signalk-autostate` — keeps the navigation state (anchored, sailing, motoring) accurate, which gates training
25
+ * Signal K Weather API — point-forecast current vectors for the DR solution
26
+
27
+ ## Daily use
28
+
29
+ Open the webapp from your Signal K server's *Web apps* menu. It shows the vessel and the shadow boat on the chart, GPS↔DR divergence, the uncertainty polygon, and the water-track log. A status tile is also available for Freeboard-SK ≥ 3.0.
30
+
31
+ **Entering fixes.** Open *⊕ Sight / LOP* and pick the method:
32
+
33
+ * **Bearing** — compass bearing to a known object (lighthouse, tower) gives a line of position
34
+ * **Vert. Angle** — vertical angle to a known object of known height gives a circular position line
35
+ * **Celestial** — sextant sight of the Sun, Moon, or an almanac star
36
+
37
+ You can right-click an object on the map to pre-fill its coordinates. Sights can be entered with a stopwatch delay ("minutes ago") instead of clock time. Observations accumulate in the pending list; select two or more, *Preview selected* to resolve the candidate fix, then *Confirm fix* to snap dead reckoning to it. A single line of position can also be advanced to a later sight (running fix).
38
+
39
+ **When GPS looks wrong.** The plugin raises notifications for GPS anomalies, growing DR divergence, and paddlewheel fouling — but it never switches navigational authority by itself. If you decide to trust DR over GPS, press *Engage OVERRIDE*; the shadow boat takes over `navigation.position` instantly (it has been running all along). Release it when position is re-established, e.g. after confirming a fix.
40
+
41
+ **Other controls.** *≋ Current* sets a manual set-and-drift override with a TTL, which outranks the automatic current sources while it lasts.
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.4.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",