signalk-pypilot-newui 2.9.0 → 2.10.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
@@ -1,5 +1,103 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.10.0 — 2026-09-27 — Sea-trial wind-mode fixes + maneuver forensics
4
+
5
+ Rolls up Rev309 through Rev323. Between 2.9.0 (Rev280) and 2.10.0 the
6
+ plugin absorbed a sea trial (2026-09-25) that surfaced several critical
7
+ gaps in the wind-mode workflow, plus a new forensics module so future
8
+ sea trials can be audited offline.
9
+
10
+ ### Added
11
+
12
+ - **Maneuver Trace Log** (Rev322, opt-in). Every user-driven action from
13
+ the visor — aproado / empopado pick, tack tap, mode change, target PUT,
14
+ engage / disengage — is recorded together with pypilot state right
15
+ before the command AND 300 ms after. Pair pre/post makes it trivial
16
+ to answer "did the pilot actually obey?". Files land as JSONL under
17
+ `<plugin data dir>/maneuver-trace/`. New endpoints:
18
+ `GET|POST /maneuver-trace/{status,start,stop,event}` +
19
+ `GET /maneuver-trace/tail?n=N`. Setup card "Maneuver trace
20
+ (sea-trial forensics)" toggles it on the fly, off by default.
21
+ - **Empopado pseudo-mode** (Rev313 → Rev320) — mirror of Aproado for the
22
+ other extreme: bring the boat to TWA ≈ 180° so the mainsail flogs the
23
+ least while striking sail. Full HUD (choosing → transit → active),
24
+ hot gains at latch (P × 1.5, D × 1.7, servo slew × 1.3), audible
25
+ warning when `|TWA| < 160°` sustained 1 s ("empopado inestable"),
26
+ auto-cancel when `|TWA| < 140°` sustained 2.5 s. Restore in order
27
+ hot gains → profile → mode → target → engaged on SALIR / cancel.
28
+ - **Attitude safety alarms** (Rev299 / I1) — `attitude-heel-extreme` and
29
+ `attitude-pitch-extreme` rules with configurable thresholds
30
+ (`alarmAttitudeHeelDeg`, `alarmAttitudePitchDeg`), off by default so
31
+ a fresh install never disarms on the dock.
32
+ - **Roll feed-forward** (Rev282+) and **Post-tack catch-up** (Rev299)
33
+ publish diagnostic paths under
34
+ `steering.autopilot.pypilot.tuning.rollFf.*` and `.tackCatchup.*`.
35
+ Both are currently `appliedToAp: false` — computed and observable
36
+ but not yet fed to the pilot; a later release flips the switch after
37
+ sea trial validation.
38
+ - **Leeway estimator** (Rev298 / H4) publishing
39
+ `steering.autopilot.pypilot.derived.leewayRad` from the classical
40
+ `drift_deg = adj * heel_deg / bsp_kn²` formula. Opt-in via
41
+ `leewayAdjustment > 0`, kept off the canonical `performance.leeway`
42
+ path so it never overwrites `signalk-derived-data`.
43
+ - **Failsafe boat speed** (Rev299 / H2) — when both BSP and SOG go
44
+ missing, the plugin injects a configurable knots value so downstream
45
+ wind / leeway calculations keep producing plausible numbers.
46
+ - **Profile default persistence fix** (Rev310) — opening the visor from
47
+ a second device no longer silently overwrites the pypilot profile with
48
+ `default`; the select is now primed from the HTTP snapshot before the
49
+ WebSocket delta arrives.
50
+
51
+ ### Fixed (critical — from the 2026-09-25 sea trial)
52
+
53
+ - **Tack in wind mode** (Rev309) — pressing VIRAR to a specific side now
54
+ reliably rotates the bow to that side, even when the "short arc" would
55
+ go the other way. Root cause: `planned = -refRad` in the countdown
56
+ path was ignoring the sign; now `_tackCountdownToExecuting` delegates
57
+ to `ap.tack.direction` + `ap.tack.state=begin` so pypilot honours the
58
+ band the sailor chose.
59
+ - **Aproado without countdown** (Rev311) — pressing POR PROA / POR POPA
60
+ starts the maneuver on the same tap (previously 5 s delay). Sustain
61
+ 3 s for the "achieved" latch avoids false positives on brief AWA
62
+ oscillations.
63
+ - **Alarm silence during maneuvers** (Rev311 → Rev312 → Rev323) — the
64
+ `heading-deviation`, `cruise-drift` and `unable-to-steer` rules now
65
+ respect both pypilot's own `ap.tack.state != "none"` AND a visor-side
66
+ pseudo-mode flag posted to `POST /maneuver-state` when Aproado /
67
+ Empopado are running. Independent flag needed because pypilot does
68
+ not always update `ap.tack.state` when the sailor uses one of the
69
+ pseudo-modes.
70
+ - **Heading-deviation false positives in wind mode** (Rev323) —
71
+ threshold raised to 35° and sustain to 30 s when the pilot mode is
72
+ `wind` / `true wind`. In waves and gusts the AWA target legitimately
73
+ oscillates ±20-30° while the AP is still correcting; the 20°/15 s
74
+ default of compass mode was disarming the alarm several times per
75
+ hour on Tunatunes.
76
+ - **Tack countdown stability close-out** (Rev312) — if the tack overshoots
77
+ the plan but the boat is clearly settled (rate-of-turn < 3°/s for 2 s),
78
+ the overlay closes on its own instead of counting up to the 120 s
79
+ timeout.
80
+
81
+ ### Changed
82
+
83
+ - **Alarms OFF by default** (Rev317) — a fresh install no longer arms
84
+ every rule; the sailor opts in via Setup → Alarms. Pre-Rev299 installs
85
+ keep legacy behaviour (undefined = all-on) so nothing changes silently
86
+ under an existing boat.
87
+ - **Terminology cleanup** (Rev321) — all references to third-party
88
+ autopilot brands / dashboards in code, comments, docs, UI hints and
89
+ package metadata have been rewritten as neutral descriptions of the
90
+ mechanism ("downstream Signal K clients", "wind-response damping
91
+ bands", "classical heel-over-speed² formula"). No behavioural change.
92
+ - **Tablet vertical crop hardening** (Rev314) — `100vh` → `100dvh` in
93
+ every fullscreen modal, `-webkit-overflow-scrolling: touch`, safe-area
94
+ padding. Fixes bottom-of-card clipping on Android tablets in portrait.
95
+
96
+ ### Removed
97
+
98
+ - `docs/PROMPT_FOR_LLM_maneuver_catalog.md` — obsolete Rev209 audit
99
+ prompt, superseded by the current tack design.
100
+
3
101
  ## 2.9.0 — 2026-09-23 — Relicensed to AGPL-3.0-or-later
4
102
 
5
103
  ### License change (see NOTICE for details)
package/README.md CHANGED
@@ -3,12 +3,12 @@
3
3
  **This plugin enhances and extends the magic of [pypilot](https://github.com/pypilot/pypilot)**,
4
4
  Sean D'Epagnier's open-source autopilot, with more functions, a modern
5
5
  touch-first user experience on tablets and phones, first-class
6
- **Signal K integration** so tools like [KIP](https://kip.signalk.org),
7
- WilhelmSK and freeboard can drive and monitor the autopilot out of the
8
- box, and a strong focus on **reliability, ease of use and ease of
9
- tuning**: the plugin itself watches how your autopilot is behaving and
10
- quietly suggests how to sharpen its settings, so you spend more time
11
- sailing and less time guessing which slider to move.
6
+ **Signal K integration** so any downstream Signal K client can drive
7
+ and monitor the autopilot out of the box, and a strong focus on
8
+ **reliability, ease of use and ease of tuning**: the plugin itself
9
+ watches how your autopilot is behaving and quietly suggests how to
10
+ sharpen its settings, so you spend more time sailing and less time
11
+ guessing which slider to move.
12
12
 
13
13
  Runs alongside [`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)
14
14
  by Panaaj — see [Working alongside pypilot-autopilot-provider](#working-alongside-pypilot-autopilot-provider).
@@ -78,8 +78,8 @@ by Panaaj — see [Working alongside pypilot-autopilot-provider](#working-alongs
78
78
  18. **Alarm engine — 9 rules** — heading deviation, cruise drift,
79
79
  unable-to-steer, servo overcurrent, servo temp, motor temp, high
80
80
  AP load, low voltage, sensor lost, pypilot disconnected — all
81
- published as canonical `notifications.autopilot.*` for KIP /
82
- WilhelmSK.
81
+ published as canonical `notifications.autopilot.*` for any
82
+ downstream Signal K client.
83
83
  19. **Head-to-wind pseudo-mode** — a "raise the sails" mode not
84
84
  present in pypilot, with countdown and bow / stern swing
85
85
  selection.
@@ -112,16 +112,16 @@ by Panaaj — see [Working alongside pypilot-autopilot-provider](#working-alongs
112
112
  stored as JSONL with tags (wind, sea state, propulsion, point of
113
113
  sail, crew), shareable for advice.
114
114
  28. **Full Signal K path catalog viewer** in the visor with
115
- copy-to-clipboard for KIP.
115
+ copy-to-clipboard for downstream widgets.
116
116
 
117
117
  ### Signal K integration — 8 new added functions
118
118
 
119
119
  29. **~110 Signal K paths** under `steering.autopilot.pypilot.*`,
120
120
  opt-in per path so you never flood the bus.
121
- 30. **Autopilot API v2 provider** (absorbable) — WilhelmSK,
122
- freeboard and KIP talk to this plugin as the canonical
121
+ 30. **Autopilot API v2 provider** (absorbable) — any downstream
122
+ Signal K autopilot client talks to this plugin as the canonical
123
123
  autopilot.
124
- 31. **KIP action paths** — Simple Switch for engage, nudge, tack,
124
+ 31. **Simple Switch action paths** — engage, nudge, tack,
125
125
  mode selector, profile selector.
126
126
  32. **9 momentary switches** — nudge ±10 / ±1, tack port / starboard
127
127
  / cancel — as KIP boolean paths.
@@ -152,8 +152,8 @@ history.
152
152
  2.0.0 is a **breaking release** addressing feedback from Sean D'Epagnier
153
153
  (pypilot author). Every pypilot key now maps 1:1 to
154
154
  `steering.autopilot.pypilot.<key>` verbatim - no more hand-picked renames
155
- or C -> K / deg -> rad conversions. If you have KIP widgets or WilhelmSK
156
- dashboards wired to 1.0.0 paths, see the full migration table in
155
+ or C -> K / deg -> rad conversions. If you have Signal K dashboards
156
+ wired to 1.0.0 paths, see the full migration table in
157
157
  [`CHANGELOG.md`](./CHANGELOG.md) - most notable examples:
158
158
 
159
159
  - `.servo.controllerTemperature` (K) -> `.servo.controller_temp` (C)
@@ -241,7 +241,7 @@ We build on top of the excellent
241
241
  [`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)
242
242
  by Panaaj — the plugin that gives Signal K the standard
243
243
  **Autopilot API v2** surface (`/signalk/v2/api/vessels/self/autopilots/*`) so
244
- WilhelmSK, freeboard and other clients can drive pypilot through the canonical
244
+ any downstream Signal K autopilot client can drive pypilot through the canonical
245
245
  interface. Panaaj focuses tightly on that API and opens its own socket.io
246
246
  connection to `pypilot_web`.
247
247
 
@@ -260,7 +260,7 @@ register the same ID and behaviour is undefined). Valid setups:
260
260
  | Enabled | Off (default) | Both run happily side by side. Panaaj owns the AP v2 API; this plugin adds the extra paths + UI. **2 sockets** to `pypilot_web`. |
261
261
  | Disabled | **On** | Only this plugin. Same AP v2 API + all the extras. **1 socket**. Handy on Pi Zero W. |
262
262
  | Enabled | On | Both try to register the same AP ID — leave one off. |
263
- | Disabled | Off | WilhelmSK / freeboard / KIP won't see the AP. Only useful if you exclusively use this plugin's own webapp. |
263
+ | Disabled | Off | No downstream Signal K client will see the AP. Only useful if you exclusively use this plugin's own webapp. |
264
264
 
265
265
  Absorb mode lives in **Setup → Autopilot Provider (one-socket mode)**.
266
266
 
package/dist/alarms.js CHANGED
@@ -12,10 +12,10 @@
12
12
  // - Rules never share state - a rule that says "true" this tick can
13
13
  // later say "false" without any cleanup dance.
14
14
  // - Every fire increments a per-rule counter used to add jitter to
15
- // the emitted notification timestamp, so KIP/WilhelmSK never
16
- // collapse two consecutive alerts into one.
15
+ // the emitted notification timestamp, so downstream SK clients
16
+ // never collapse two consecutive alerts into one.
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.AlarmEngine = exports.DEFAULT_RULES = exports.RULE_PYPILOT_DISC_SEC = exports.RULE_RUDDER_TRIM_RAD = exports.RULE_SERVO_TRIM_FRAC = exports.RULE_LOW_VOLTAGE_V = exports.RULE_SERVO_TEMP_C = exports.RULE_SERVO_OVERCURR_A = exports.RULE_UNABLE_DUTY_MIN = exports.RULE_UNABLE_ERR_RAD = exports.RULE_HEADING_ERR_RAD = exports.DEFAULT_MOTOR_TEMP_C = void 0;
18
+ exports.AlarmEngine = exports.RULE_ATTITUDE_PITCH_DEG = exports.RULE_ATTITUDE_HEEL_DEG = exports.DEFAULT_RULES = exports.RULE_PYPILOT_DISC_SEC = exports.RULE_RUDDER_TRIM_RAD = exports.RULE_SERVO_TRIM_FRAC = exports.RULE_LOW_VOLTAGE_V = exports.RULE_SERVO_TEMP_C = exports.RULE_SERVO_OVERCURR_A = exports.RULE_UNABLE_DUTY_MIN = exports.RULE_UNABLE_ERR_RAD = exports.RULE_HEADING_ERR_RAD = exports.DEFAULT_MOTOR_TEMP_C = void 0;
19
19
  const SEV_TO_SK = {
20
20
  info: "nominal",
21
21
  warn: "warn",
@@ -53,13 +53,35 @@ exports.DEFAULT_RULES = [
53
53
  label: "Heading deviation",
54
54
  severity: "warn",
55
55
  defaultEnabled: true,
56
- sustainSec: 15,
57
- description: `RMS heading error exceeds ${(exports.RULE_HEADING_ERR_RAD * 180 / Math.PI) | 0}° with the AP engaged.`,
56
+ // Rev323 (Carlos, 2026-09-27): sustain subido de 15 s a 30 s. Con
57
+ // 15 s cualquier ola grande / racha corta metía la RMS 20 s por
58
+ // encima del umbral y disparaba la alarma; 30 s filtra el ruido
59
+ // legítimo de un mar entrecortado sin perder la señal de un AP
60
+ // realmente descolgado.
61
+ sustainSec: 30,
62
+ description: `RMS heading error exceeds ${(exports.RULE_HEADING_ERR_RAD * 180 / Math.PI) | 0}° (compass) / 35° (wind) with the AP engaged.`,
58
63
  evaluate: (c) => {
59
64
  if (!c.kpis || !c.sample || !c.sample.engaged)
60
65
  return false;
66
+ // Rev311: durante una maniobra nativa del pypilot silenciamos.
67
+ if (c.maneuverInProgress)
68
+ return false;
69
+ // Rev323 (Carlos, 2026-09-27): pseudo-modo visor-side (Aproado /
70
+ // Empopado) también silencia — el visor está pilotando la
71
+ // maniobra por su cuenta y el "error" que reporta pypilot es
72
+ // el gap intencionado durante el swing.
73
+ if (c.maneuverPseudoActive)
74
+ return false;
75
+ // Rev323 mode-aware threshold. En wind / true-wind el heading
76
+ // command es un AWA/TWA target, que oscila naturalmente ±20-30°
77
+ // en olas y ráfagas mientras el AP sí está corrigiendo. Subir
78
+ // el umbral en esos modos elimina el disparo por doquier que
79
+ // Carlos reportó 2026-09-27.
80
+ const mode = (c.sample.mode || "").toLowerCase();
81
+ const isWind = mode.includes("wind");
82
+ const threshold = isWind ? (35 * Math.PI / 180) : exports.RULE_HEADING_ERR_RAD;
61
83
  const r = c.kpis.window1m.rmsErrorRad;
62
- return typeof r === "number" && r > exports.RULE_HEADING_ERR_RAD;
84
+ return typeof r === "number" && r > threshold;
63
85
  },
64
86
  message: (c) => {
65
87
  const r = c.kpis?.window1m.rmsErrorRad ?? 0;
@@ -81,6 +103,10 @@ exports.DEFAULT_RULES = [
81
103
  evaluate: (c) => {
82
104
  if (!c.kpis || !c.sample || !c.sample.engaged)
83
105
  return false;
106
+ if (c.maneuverInProgress)
107
+ return false; // Rev311: silencio en maniobra
108
+ if (c.maneuverPseudoActive)
109
+ return false; // Rev323: idem pseudo-modo
84
110
  const mean = c.kpis.window1m.meanErrorRad;
85
111
  return typeof mean === "number" && Math.abs(mean) > (5 * Math.PI / 180);
86
112
  },
@@ -100,6 +126,10 @@ exports.DEFAULT_RULES = [
100
126
  evaluate: (c) => {
101
127
  if (!c.kpis || !c.sample || !c.sample.engaged)
102
128
  return false;
129
+ if (c.maneuverInProgress)
130
+ return false; // Rev311: silencio en maniobra
131
+ if (c.maneuverPseudoActive)
132
+ return false; // Rev323: idem pseudo-modo
103
133
  const r = c.kpis.window1m.rmsErrorRad;
104
134
  const d = c.kpis.window1m.servoDutyPct;
105
135
  return typeof r === "number" && r > exports.RULE_UNABLE_ERR_RAD
@@ -213,9 +243,10 @@ exports.DEFAULT_RULES = [
213
243
  {
214
244
  // Rev156 (Carlos): ServoHealth already grades the drive as
215
245
  // learning/good/elevated/high on the deviation ratio (recent avg
216
- // vs learned baseline). Surface "high" as a notification so KIP /
217
- // WilhelmSK show the same red banner the visor card does. Only
218
- // fires once the baseline is actually learned (deviation != null).
246
+ // vs learned baseline). Surface "high" as a notification so
247
+ // downstream SK clients show the same red banner the visor card
248
+ // does. Only fires once the baseline is actually learned
249
+ // (deviation != null).
219
250
  id: "servo-load-high",
220
251
  label: "Autopilot load high",
221
252
  severity: "warn",
@@ -318,7 +349,55 @@ exports.DEFAULT_RULES = [
318
349
  return `Pypilot desconectado desde hace ${tail}`;
319
350
  },
320
351
  },
352
+ // Rev299 (I1): attitude safety envelope. These rules watch what the
353
+ // sailor cares about — is the boat pinned on its side, is the bow
354
+ // buried — not just what the servo is doing. Both are opt-in
355
+ // (defaultEnabled=false) because a heavy-weather passage will trip
356
+ // them and we do not want a fresh install screaming across a
357
+ // deliberate rail-in-the-water broach. Thresholds live in
358
+ // AlarmThresholds so the sailor can tune per-boat.
359
+ {
360
+ id: "attitude-heel-extreme",
361
+ label: "Escora extrema",
362
+ severity: "warn",
363
+ defaultEnabled: false,
364
+ sustainSec: 5,
365
+ description: "Heel angle sostenido por encima del umbral configurado (por defecto 45°). Sirve como envoltura de seguridad: no reemplaza el criterio del navegante ni desengancha el piloto.",
366
+ evaluate: (c) => {
367
+ if (!c.sample || typeof c.sample.heel !== "number")
368
+ return false;
369
+ const thrDeg = c.thresholds?.attitudeHeelDeg ?? exports.RULE_ATTITUDE_HEEL_DEG;
370
+ return Math.abs(c.sample.heel) * 180 / Math.PI > thrDeg;
371
+ },
372
+ message: (c) => {
373
+ const heelDeg = Math.abs((c.sample?.heel ?? 0) * 180 / Math.PI);
374
+ const thrDeg = c.thresholds?.attitudeHeelDeg ?? exports.RULE_ATTITUDE_HEEL_DEG;
375
+ return `Escora ${heelDeg.toFixed(0)}° (umbral ${thrDeg}°)`;
376
+ },
377
+ },
378
+ {
379
+ id: "attitude-pitch-extreme",
380
+ label: "Cabeceo extremo",
381
+ severity: "warn",
382
+ defaultEnabled: false,
383
+ sustainSec: 3,
384
+ description: "Pitch (cabeceo) sostenido por encima del umbral configurado. Suele indicar embarque de proa o clavado en ola.",
385
+ evaluate: (c) => {
386
+ if (!c.sample || typeof c.sample.pitch !== "number")
387
+ return false;
388
+ const thrDeg = c.thresholds?.attitudePitchDeg ?? exports.RULE_ATTITUDE_PITCH_DEG;
389
+ return Math.abs(c.sample.pitch) * 180 / Math.PI > thrDeg;
390
+ },
391
+ message: (c) => {
392
+ const pitchDeg = Math.abs((c.sample?.pitch ?? 0) * 180 / Math.PI);
393
+ const thrDeg = c.thresholds?.attitudePitchDeg ?? exports.RULE_ATTITUDE_PITCH_DEG;
394
+ return `Cabeceo ${pitchDeg.toFixed(0)}° (umbral ${thrDeg}°)`;
395
+ },
396
+ },
321
397
  ];
398
+ // Rev299 (I1): defaults for the attitude envelope.
399
+ exports.RULE_ATTITUDE_HEEL_DEG = 45;
400
+ exports.RULE_ATTITUDE_PITCH_DEG = 25;
322
401
  class AlarmEngine {
323
402
  constructor(rules = exports.DEFAULT_RULES, opts = {}) {
324
403
  this.rules = new Map();
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ // Fail-safe boat speed resolver.
3
+ //
4
+ // When both `navigation.speedThroughWater` (BSP) and
5
+ // `navigation.speedOverGround` (SOG) disappear at the same time —
6
+ // speedo cable snagged, GPS antenna covered by a sail, both plugin
7
+ // providers dropped — several downstream calculations collapse:
8
+ //
9
+ // - `environment.wind.speedTrue` needs BSP to subtract vessel motion
10
+ // from the masthead reading. Without BSP, `signalk-derived-data`
11
+ // stops emitting TWS and everything that depends on it (Doctor
12
+ // rules, wind KPIs, leeway estimator) goes null.
13
+ // - Our `performance.leeway` estimator (leeway.ts) divides by
14
+ // BSP squared and refuses to publish when BSP is null.
15
+ //
16
+ // The classical racing racks answer this with a configured "failsafe
17
+ // boat speed" — a plausible value (typically 5–7 kn for a cruising
18
+ // keelboat) that keeps the wind stack producing plausible numbers
19
+ // while the sailor investigates. We adopt the same pattern.
20
+ //
21
+ // Ranking (highest priority first):
22
+ // 1. Direct BSP.
23
+ // 2. SOG as proxy (acceptable in calm current; a source of error
24
+ // when there is significant tide, but the alternative is a hole
25
+ // in the wind chain).
26
+ // 3. Configured `failSafeBspKn` converted to m/s.
27
+ // 4. null.
28
+ //
29
+ // The caller receives both the resolved value and a `source` label so
30
+ // KPIs, Doctor, and the visor can tag any measurement that leans on
31
+ // the failsafe. This is important: a TWS computed off a failsafe BSP
32
+ // is a plausibility, not a measurement, and should not be graded like
33
+ // a real one.
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.FailsafeBspResolver = void 0;
36
+ const KN_PER_MS = 1.9438444924406046;
37
+ const DEFAULTS = {
38
+ failSafeBspKn: 0,
39
+ };
40
+ class FailsafeBspResolver {
41
+ constructor(opts = {}) {
42
+ this.opts = { ...DEFAULTS, ...opts };
43
+ }
44
+ update(patch) {
45
+ this.opts = { ...this.opts, ...patch };
46
+ }
47
+ getOptions() { return this.opts; }
48
+ /** Pick a boat-speed value in m/s using the priority ladder.
49
+ * Callers pass whatever they read from SK; null and NaN are both
50
+ * treated as "missing". */
51
+ resolve(bspMs, sogMs) {
52
+ if (typeof bspMs === "number" && Number.isFinite(bspMs) && bspMs >= 0) {
53
+ return { valueMs: bspMs, source: "bsp" };
54
+ }
55
+ if (typeof sogMs === "number" && Number.isFinite(sogMs) && sogMs >= 0) {
56
+ return { valueMs: sogMs, source: "sog" };
57
+ }
58
+ if (this.opts.failSafeBspKn > 0) {
59
+ return { valueMs: this.opts.failSafeBspKn / KN_PER_MS, source: "failsafe" };
60
+ }
61
+ return { valueMs: null, source: "none" };
62
+ }
63
+ }
64
+ exports.FailsafeBspResolver = FailsafeBspResolver;