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 +98 -0
- package/README.md +16 -16
- package/dist/alarms.js +88 -9
- package/dist/failsafe-bsp.js +64 -0
- package/dist/index.js +523 -17
- package/dist/leeway.js +74 -0
- package/dist/maneuver-trace.js +175 -0
- package/dist/publisher.js +2 -2
- package/dist/tack-catchup.js +90 -0
- package/dist/test/alarms-attitude.test.js +110 -0
- package/dist/test/alarms-trim.test.js +1 -1
- package/dist/test/episodes.test.js +1 -0
- package/dist/test/failsafe-bsp.test.js +56 -0
- package/dist/test/leeway.test.js +91 -0
- package/dist/test/servo-error-log.test.js +1 -1
- package/dist/test/tack-catchup.test.js +90 -0
- package/package.json +2 -2
- package/public/app.js +1057 -35
- package/public/index.html +295 -178
- package/public/style.css +55 -6
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
82
|
-
|
|
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
|
|
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) —
|
|
122
|
-
|
|
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. **
|
|
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
|
|
156
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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 >
|
|
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
|
|
217
|
-
//
|
|
218
|
-
// fires once the baseline is actually learned
|
|
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;
|