@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 +93 -0
- package/README.md +1 -1
- package/SPEC.md +11 -3
- package/package.json +3 -2
- package/plugin/index.js +364 -64
- package/plugin/polar.js +85 -0
- package/public/dr-app.js +51 -29
- package/public/dr-map-view.js +18 -17
- package/public/dr-viewmodel.js +99 -15
- package/tests/dr-current.test.js +2 -1
- package/tests/dr-viewmodel.test.js +78 -4
- package/tests/plugin.test.js +426 -34
- package/tests/polar.test.js +106 -0
- package/tests/tile-paths.test.js +258 -0
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`.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|