signalk-pypilot-newui 2.5.0 → 2.7.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 +546 -0
- package/README.md +18 -7
- package/dist/alarms.js +34 -5
- package/dist/autopilot-provider.js +197 -33
- package/dist/index.js +456 -47
- package/dist/pypilot-client.js +125 -4
- package/dist/session-recorder.js +100 -0
- package/dist/trip-recorder.js +462 -0
- package/package.json +2 -2
- package/public/app.js +2658 -199
- package/public/boat-cenital.svg +5 -5
- package/public/index.html +443 -62
- package/public/style.css +250 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,553 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.7.0 — 2026-09-14 — Rev246..Rev277: connection resilience + audit follow-up + full i18n audit
|
|
4
|
+
|
|
5
|
+
**Rev277 QA fix (latency of detection)**:
|
|
6
|
+
|
|
7
|
+
- **Faster offline detection**: ping cadence 5 s → 2 s, pong window
|
|
8
|
+
8 s → 5 s. Two consecutive missed pings now raise `healthy=false`
|
|
9
|
+
in roughly half the previous time — ~5-6 s worst case instead of
|
|
10
|
+
~10-14 s. 30 pings/min is a rounding error next to pypilot_web's
|
|
11
|
+
own ~20 Hz value stream, so no measurable extra load on the
|
|
12
|
+
Pi Zero W.
|
|
13
|
+
- **Active ping on reconnect**: instead of waiting for the next 2 s
|
|
14
|
+
tick, the fresh socket emits a `ping` the moment the `connect`
|
|
15
|
+
event fires. A live pypilot_web pongs in milliseconds, and the
|
|
16
|
+
first value stream that follows clears `coreOffline` immediately.
|
|
17
|
+
Trims another ~2-3 s off the "back online" transition Carlos
|
|
18
|
+
observed on Rev276.
|
|
19
|
+
|
|
20
|
+
**Rev276 QA fix**:
|
|
21
|
+
|
|
22
|
+
- **Snapshot survives a failed restore**. Previously the disconnect
|
|
23
|
+
snapshot was consumed the moment `_pypilotMaybeOfferRestore`
|
|
24
|
+
read it — so if the user pressed "Yes" and the restore failed
|
|
25
|
+
(e.g. `POST /engage` returned HTTP 500 because pypilot dropped
|
|
26
|
+
again mid-flow), the next reconnection had no snap to re-offer
|
|
27
|
+
and the sailor silently lost the chance to restore. Now: the
|
|
28
|
+
snap is purged only on **success**, on explicit **No**, or on
|
|
29
|
+
the 60 s timeout. On failure it is kept.
|
|
30
|
+
- **Snapshot capture no longer clobbers a valid pending snap**. If
|
|
31
|
+
pypilot disconnects again while we still hold an unACKed snap
|
|
32
|
+
from an earlier outage, the current call keeps the older
|
|
33
|
+
snapshot (with the original disconnect timestamp) rather than
|
|
34
|
+
overwriting it with a fresh one that may carry
|
|
35
|
+
`engaged=false` from the failed restore.
|
|
36
|
+
- **Micro-hiccups (<20 s) no longer purge the snap**. Previously
|
|
37
|
+
any reconnection swallowed the snapshot; now sub-20 s recoveries
|
|
38
|
+
leave it available so if a longer outage follows the sailor
|
|
39
|
+
still gets the banner.
|
|
40
|
+
|
|
41
|
+
**Rev275 QA fix**:
|
|
42
|
+
|
|
43
|
+
- **Restore banner no longer stuck on success**. `verdict("ok")`
|
|
44
|
+
only spoke "Target restaurado" and forgot to close the
|
|
45
|
+
"Restaurando..." banner — sticky until the next reload. Now the
|
|
46
|
+
banner briefly turns green with the success message and
|
|
47
|
+
auto-hides in 3 s.
|
|
48
|
+
|
|
49
|
+
**Rev274 QA fix (Carlos re-test)**:
|
|
50
|
+
|
|
51
|
+
- **Restore verdict window widened from 3 s to 10 s with polling**.
|
|
52
|
+
On Rev273 the 3 s deadline still cleared before `pypilotHealthy`
|
|
53
|
+
could flip false (pong window is ~8 s), so the visor still
|
|
54
|
+
spoke "target restaurado" a few seconds before spotting the
|
|
55
|
+
disconnect. Now: after the three HTTP writes succeed, the banner
|
|
56
|
+
shows "Restaurando... esperando confirmación pypilot" and the
|
|
57
|
+
code polls every 500 ms up to 10 s. Any negative evidence
|
|
58
|
+
(`pypilotHealthy=false` or `engaged=false`) aborts to failure
|
|
59
|
+
immediately with a specific reason; success voice only fires if
|
|
60
|
+
both survive the whole window.
|
|
61
|
+
|
|
62
|
+
**Rev273 QA fix**:
|
|
63
|
+
|
|
64
|
+
- **Restore banner no longer lies "restaurado" on 200 without
|
|
65
|
+
confirmation** (Carlos QA on Rev272). SK Autopilot API v2 returns
|
|
66
|
+
200 as soon as the request reaches the provider, before pypilot
|
|
67
|
+
has had a chance to confirm. The success verdict is deferred and
|
|
68
|
+
gated on `state.pypilotHealthy !== false` +
|
|
69
|
+
`state.engaged === true` — otherwise the failure banner + voice
|
|
70
|
+
fire with a specific reason.
|
|
71
|
+
|
|
72
|
+
**Rev270..Rev271 delta on top of Rev269**:
|
|
73
|
+
|
|
74
|
+
- **i18n audit follow-up** (Rev270): every dynamic string that leaked
|
|
75
|
+
through the previous pass moved to the dictionary — the tack log
|
|
76
|
+
modal (empty state, chip labels, badge tags, recovery line,
|
|
77
|
+
confirm), the maneuver HUD verdicts (VIRADA / TRASLUCHADA), the
|
|
78
|
+
Trips card in Setup + its list rows + status line + WhatsApp
|
|
79
|
+
caption + KPI grid + highlights + map legend + all detail modal
|
|
80
|
+
cards + the PNG report card header/footer + share picker labels +
|
|
81
|
+
restore banner. Renamed variable clash in the trip loop where
|
|
82
|
+
`t` shadowed the i18n helper. Fixed accidental "AnchorWatch Pro"
|
|
83
|
+
reference in the trip caption (memory `feedback_no_commercial_refs`).
|
|
84
|
+
- **Second external audit closed** (Rev271): the follow-up report
|
|
85
|
+
raised 7 findings against Rev270. Five are landed here as small
|
|
86
|
+
targeted fixes; two (concurrent adjustTarget race, angular
|
|
87
|
+
normalisation defence for out-of-range inputs) are deferred to
|
|
88
|
+
Rev272 with rationale in the commit log.
|
|
89
|
+
|
|
90
|
+
**Rev271 fixes**:
|
|
91
|
+
|
|
92
|
+
- **Core-offline sticky flag** (audit R03). `PypilotClient` now
|
|
93
|
+
distinguishes between "the socket to pypilot_web is up" and "the
|
|
94
|
+
pypilot core is alive". A `pypilot_disconnect` event from the web
|
|
95
|
+
server sets a sticky `coreOffline` that only clears when the core
|
|
96
|
+
actually starts talking again — a fresh catalog delivery or a live
|
|
97
|
+
`pypilot` value. `healthy` and `set()` both consume this flag, so
|
|
98
|
+
a socket that keeps ponging while the core is dead can no longer
|
|
99
|
+
report success on writes. New events `pypilot_offline` /
|
|
100
|
+
`pypilot_online` are wired into `index.ts` to update plugin status
|
|
101
|
+
and mark the AP provider offline.
|
|
102
|
+
- **`allowWrites` gate is now global** (audit R05). Previously only
|
|
103
|
+
the plugin's own HTTP routes honoured it — the Autopilot API v2
|
|
104
|
+
provider interface, the KIP action PUT handlers and the momentary
|
|
105
|
+
switches all bypassed it. The check now lives in
|
|
106
|
+
`PypilotClient.set()` (single point every writer routes through)
|
|
107
|
+
plus a `writesOff()` 403 response on every PUT handler so
|
|
108
|
+
KIP/WilhelmSK/freeboard see a legible reason instead of a silent
|
|
109
|
+
no-op.
|
|
110
|
+
- **Generation counter cancels stale engage retries** (audit
|
|
111
|
+
R01+R04). `AutopilotProvider` bumps `engageGen` on every
|
|
112
|
+
`setState` call and cancels any pending NAV-mode 500 ms timer.
|
|
113
|
+
`_setWithRetry` accepts an `isStale()` callback that aborts the
|
|
114
|
+
backoff between attempts. A disengage landing mid-retry now
|
|
115
|
+
invalidates the still-pending engage, and vice versa; the delayed
|
|
116
|
+
NAV activation can no longer wake up after a disengage.
|
|
117
|
+
- **Restore banner sequenced with hard-fail** (audit R02). The "yes,
|
|
118
|
+
restore" path now runs mode → target → engage in that order, each
|
|
119
|
+
step guarded by `throwIfNotOk(res, op)` that treats both an
|
|
120
|
+
absent response and any HTTP >=400 as a stop condition. Target
|
|
121
|
+
is prepared before engage so the AP does not latch on the current
|
|
122
|
+
heading first and then jerk. Failure is spoken ("Restore failed"
|
|
123
|
+
/ "Restauración fallida") and shown as a red line in the banner
|
|
124
|
+
instead of the previous silent `catch`.
|
|
125
|
+
- **Access-request precedence bug** (audit T09). Fixed `j.href ||
|
|
126
|
+
j.requestId ? ... : null` operator precedence that was building
|
|
127
|
+
`/requests/undefined` when the server returned an explicit
|
|
128
|
+
`href`. Now `j.href || (j.requestId ? ... : null)`.
|
|
129
|
+
|
|
130
|
+
**Rev272 fixes** (close the remaining two audit points):
|
|
131
|
+
|
|
132
|
+
- **`adjustTarget` serialisation** (audit R06). Two concurrent
|
|
133
|
+
`+Δ` calls used to read the same base from `data.target` before
|
|
134
|
+
awaiting the write and both wrote `base+Δ`, losing one increment.
|
|
135
|
+
A single `_adjustChain` promise now serialises adjusts through
|
|
136
|
+
the provider so the second call reads the base written by the
|
|
137
|
+
first. Errors on one call do not poison the next caller's turn.
|
|
138
|
+
- **`shortestArcRad` bounded + finite guards** (audit R07). The
|
|
139
|
+
angular normalisation loop is now a single modulo (constant cost)
|
|
140
|
+
and returns 0 on a non-finite delta. `setTarget` and
|
|
141
|
+
`adjustTarget` also reject `NaN` / `Infinity` / astronomically
|
|
142
|
+
large radians at the boundary with a clear error, so a
|
|
143
|
+
misbehaving integration cannot poison provider state.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 2.7.0 headline — Rev246..Rev269: connection resilience + reliability + full i18n audit
|
|
148
|
+
|
|
149
|
+
Sea-trial-driven release. After several sessions of **intensive
|
|
150
|
+
sailing and beta testing** on Tunatunes we surfaced (and closed) a
|
|
151
|
+
long list of connection-resilience holes, TTS gaps, i18n drifts and
|
|
152
|
+
UX rough edges that only show up when the boat is under way, the
|
|
153
|
+
TinyPilot resets, or the link flakes. The whole batch was audited
|
|
154
|
+
twice by an external LLM auditor between iterations before being
|
|
155
|
+
folded back in.
|
|
156
|
+
|
|
157
|
+
### English
|
|
158
|
+
|
|
159
|
+
**Added**
|
|
160
|
+
|
|
161
|
+
- **Full "Pypilot down" UX cycle**: passive UI reflection tied to a
|
|
162
|
+
pong-based heartbeat. When the socket goes silent for > 8 s the
|
|
163
|
+
target diamond and TGT chip hide immediately, AP-affecting
|
|
164
|
+
controls grey out with a `.pypilot-offline` class, and the
|
|
165
|
+
alarm rule `pypilot-disconnected` fires with a spoken message
|
|
166
|
+
("Pypilot desconectado desde hace N segundos") whose h / min / s
|
|
167
|
+
units are read as full words instead of abbreviations. On
|
|
168
|
+
reconnect: green "Pypilot operativo" toast + voice announcement
|
|
169
|
+
+ controls reactivate.
|
|
170
|
+
- **Three-tier outage handler**: on reconnect the visor compares
|
|
171
|
+
the outage length against the last known target. < 20 s: no
|
|
172
|
+
action (probably a hiccup). 20 s – 5 min: banner offers to
|
|
173
|
+
restore the previous target with Sí / No buttons. > 5 min:
|
|
174
|
+
nothing (too long, sailor most likely wants a fresh engage).
|
|
175
|
+
- **Optimistic write-then-throw pattern** with `client.set()`
|
|
176
|
+
returning `boolean`. `apEngage / apDisengage / apSetMode /
|
|
177
|
+
apSetTargetRad / apTack` reject BEFORE mutating optimistic state,
|
|
178
|
+
so a phantom command against an offline TinyPilot cannot leave
|
|
179
|
+
the UI stuck in "engaged" waiting for a delta that will never
|
|
180
|
+
arrive.
|
|
181
|
+
- **`_setWithRetry` helper** (250 ms + 750 ms backoff) around the
|
|
182
|
+
autopilot-provider adapter for transient socket blips.
|
|
183
|
+
- **Watchdog + pong heartbeat** in `pypilot-client.ts`: `healthy`
|
|
184
|
+
getter combines `socket.connected` with `lastPongMs <= 8 s`.
|
|
185
|
+
Alarm engine consumes `client.healthy` (not `.connected`) so
|
|
186
|
+
the network layer and the alarm engine agree on liveness. Alarm
|
|
187
|
+
ordering also reworked so `sensor-lost` no longer fires _before_
|
|
188
|
+
`pypilot-disconnected` when the whole socket dies.
|
|
189
|
+
- **Multi-tab session isolation**: owner keys now live in
|
|
190
|
+
`sessionStorage` (per-tab) not `localStorage`, so two visor tabs
|
|
191
|
+
do not race to release focus watches.
|
|
192
|
+
- **Signed-delta rotation counter** (Rev247) for the HUD progress
|
|
193
|
+
during tack maneuvers: accumulates the SIGNED angular delta so
|
|
194
|
+
IMU / compass jitter cancels around zero and the counter no
|
|
195
|
+
longer drifts to "0° remaining" with the boat still 100°
|
|
196
|
+
short. The 0.5° threshold that produced the false report
|
|
197
|
+
was removed entirely.
|
|
198
|
+
- **Full i18n audit** across EN / ES / DE / FR: purged mixed
|
|
199
|
+
Spanish text from EN-only modals (Boat menu, Tack log, Trip
|
|
200
|
+
detail, Trip share, Gust marker overlay, Bottom-bar picker,
|
|
201
|
+
nav-toggle aria-label), added `data-lang` variants, moved the
|
|
202
|
+
bottom-bar strings to i18n keys, added `data-i18n-title` on the
|
|
203
|
+
tack buttons. Rebrand "Gota Chain" (which "nobody understood",
|
|
204
|
+
per Carlos) as **"Target & Wind arrows"** / "Flechas
|
|
205
|
+
Target y Viento" everywhere it is public-facing (README, Info
|
|
206
|
+
modal, NPM screenshot title).
|
|
207
|
+
- **Spanish orthography pass**: `"COMPAS"` → `"COMPÁS"`
|
|
208
|
+
in the ES mode label, `<code>compas / gps / viento / real</code>`
|
|
209
|
+
→ the literal `<code>compass / gps / wind / true</code>`
|
|
210
|
+
string (they are pypilot enum values, not translatable words).
|
|
211
|
+
- **Full instructions section** in the Info modal (EN + ES)
|
|
212
|
+
covering every tab now that Chart / Race / Doctor / Smart Pilot
|
|
213
|
+
/ Alarms / Sensor Quality / Session recorder / Log capture /
|
|
214
|
+
Debug console / Remote Control Console / Watchdog have all
|
|
215
|
+
landed.
|
|
216
|
+
- **Health status endpoint** (`/status`) now returns
|
|
217
|
+
`healthy: boolean` alongside `connected`.
|
|
218
|
+
- **Focus-watch owner map**: `_focusWatches` moved to
|
|
219
|
+
`Map<key, Map<owner, entry>>` so a stale owner from a crashed
|
|
220
|
+
tab does not hold a watch alive.
|
|
221
|
+
|
|
222
|
+
**Fixed**
|
|
223
|
+
|
|
224
|
+
- **Phantom writes on offline socket**: `client.set()` no longer
|
|
225
|
+
returns a silent success when the socket has no live pong. Every
|
|
226
|
+
API-level writer either resolves after the retry succeeds or
|
|
227
|
+
throws with `"pypilot offline: <path>"`. Fixes a class of "the
|
|
228
|
+
UI says engaged but the boat is going straight" reports.
|
|
229
|
+
- **Slow reconnect after power-cycling pypilot**: watchdog interval
|
|
230
|
+
reduced to 15 s with a 20 s liveness threshold, and the ap
|
|
231
|
+
provider now reconciles state on the reconnect delta so the
|
|
232
|
+
visor picks up the fresh mode / target immediately.
|
|
233
|
+
- **`refreshPypilotValues` merge bug**: pilot / profile switch was
|
|
234
|
+
masked by the "don't overwrite existing keys" heuristic. Delta
|
|
235
|
+
handlers for `ap.pilot` and `profile` now pass `{replace: true}`.
|
|
236
|
+
- **`gainReady` exit too early**: the readiness gate was leaving
|
|
237
|
+
as soon as any gain from any pilot arrived. Now scoped to
|
|
238
|
+
`state.pilot` so a stale pilot's gains cannot short-circuit the
|
|
239
|
+
boot pull for the active one.
|
|
240
|
+
- **Tack pill "executing" missing target**: the pill fell back to
|
|
241
|
+
`null` after the arming phase closed. Now falls back through
|
|
242
|
+
`_tackPendingFinalTargetRad ?? _countdownPlannedTargetRad ??
|
|
243
|
+
state.target`.
|
|
244
|
+
- **TGT stays visible with AP engaged + connection lost**: no
|
|
245
|
+
disengage delta arrives when the pypilot vanishes, so the
|
|
246
|
+
target was staying pinned. Now the formatter and
|
|
247
|
+
`renderTargetArrow` bail on `state.pypilotHealthy === false`;
|
|
248
|
+
`scheduleRenderControl()` and `renderTargetArrow()` are also
|
|
249
|
+
kicked on every healthy transition.
|
|
250
|
+
- **Voice not heard on reconnect** (Rev264): added
|
|
251
|
+
`_alSpeak("Pypilot operativo")` to
|
|
252
|
+
`_pypilotShowReconnectedToast` so the sailor gets the audible
|
|
253
|
+
confirmation and does not have to look at the screen.
|
|
254
|
+
|
|
255
|
+
**Architecture**
|
|
256
|
+
|
|
257
|
+
- Rejected an active-ping-before-write pattern proposed mid-sprint:
|
|
258
|
+
it is a TOCTOU (time-of-check-to-time-of-use) anti-pattern that
|
|
259
|
+
adds latency to every write without giving a real safety
|
|
260
|
+
guarantee. Kept the passive-reflection approach instead. Design
|
|
261
|
+
discussion preserved in the commit log.
|
|
262
|
+
|
|
263
|
+
### Español
|
|
264
|
+
|
|
265
|
+
**Añadido**
|
|
266
|
+
|
|
267
|
+
- **Ciclo UX completo "Pypilot caído"**: la UI refleja el
|
|
268
|
+
estado real de la conexión de forma pasiva a partir de un
|
|
269
|
+
heartbeat pong. Cuando el socket lleva > 8 s en silencio, el
|
|
270
|
+
diamante del target y el chip TGT desaparecen inmediatamente,
|
|
271
|
+
los controles que afectan al AP se atenúan con la clase
|
|
272
|
+
`.pypilot-offline`, y la regla `pypilot-disconnected` dispara
|
|
273
|
+
con voz en palabras completas ("Pypilot desconectado desde hace
|
|
274
|
+
N segundos") en lugar de abreviaturas. Al reconectar: toast
|
|
275
|
+
verde "Pypilot operativo" + voz + controles activos otra vez.
|
|
276
|
+
- **Gestor de outage en 3 tramos**: al reconectar el visor compara
|
|
277
|
+
la duración de la caída contra el último
|
|
278
|
+
target conocido. < 20 s: no hace nada (probable microcorte).
|
|
279
|
+
20 s – 5 min: banner con Sí / No para restaurar el
|
|
280
|
+
target anterior. > 5 min: nada (demasiado tiempo, el
|
|
281
|
+
navegante querrá enganchar de cero).
|
|
282
|
+
- **Patrón optimistic write-then-throw** con `client.set()`
|
|
283
|
+
devolviendo `boolean`. `apEngage / apDisengage / apSetMode /
|
|
284
|
+
apSetTargetRad / apTack` rechazan ANTES de tocar el estado
|
|
285
|
+
optimista, para que un comando fantasma contra una TinyPilot
|
|
286
|
+
caída no deje la UI "enganchada" esperando un delta que
|
|
287
|
+
no va a llegar.
|
|
288
|
+
- **`_setWithRetry`** (250 ms + 750 ms backoff) alrededor del
|
|
289
|
+
autopilot-provider adapter para blips transitorios del socket.
|
|
290
|
+
- **Watchdog + heartbeat pong** en `pypilot-client.ts`: el getter
|
|
291
|
+
`healthy` combina `socket.connected` con `lastPongMs <= 8 s`.
|
|
292
|
+
El motor de alarmas usa `client.healthy` (no `.connected`) para
|
|
293
|
+
que la red y las alarmas estén de acuerdo sobre la
|
|
294
|
+
vitalidad. El orden de alarmas también se rehizo para
|
|
295
|
+
que `sensor-lost` no salte _antes_ que `pypilot-disconnected`
|
|
296
|
+
cuando cae el socket entero.
|
|
297
|
+
- **Aislamiento multi-pestaña**: las claves de owner van
|
|
298
|
+
ahora en `sessionStorage` (por pestaña) en lugar de
|
|
299
|
+
`localStorage`, para que dos visores abiertos no compitan por
|
|
300
|
+
soltar los focus watches.
|
|
301
|
+
- **Contador con delta con signo** (Rev247) para el HUD del
|
|
302
|
+
countdown de virada: acumula el delta angular CON SIGNO para
|
|
303
|
+
que el jitter del IMU / compass se cancele en torno a cero y el
|
|
304
|
+
contador no acabe en "0° restantes" con el barco todavía
|
|
305
|
+
a 100° del rumbo final. El threshold de 0.5° que
|
|
306
|
+
producía el falso reporte se eliminó por completo.
|
|
307
|
+
- **Auditoría i18n completa** en EN / ES / DE / FR: se
|
|
308
|
+
purgó el español en modales que se ven en cualquier
|
|
309
|
+
idioma (menú Barco, Historial de bordos, Detalle de viaje,
|
|
310
|
+
Compartir viaje, capa Marca de ráfagas, selector barra
|
|
311
|
+
inferior, aria del nav-toggle), se añadieron variantes
|
|
312
|
+
`data-lang`, las strings de la barra inferior pasaron a claves
|
|
313
|
+
i18n y se añadió `data-i18n-title` en los botones
|
|
314
|
+
de virada. Se renombró "Gota Chain" (que "no lo entiende
|
|
315
|
+
ni Claude", palabras de Carlos) a **"Flechas Target y Viento"**
|
|
316
|
+
/ "Target & Wind arrows" en todo lo que se ve fuera del
|
|
317
|
+
código (README, modal Info, título de screenshot
|
|
318
|
+
NPM).
|
|
319
|
+
- **Pase de ortografía española**: `"COMPAS"` →
|
|
320
|
+
`"COMPÁS"` en el label de modo ES, y el bloque
|
|
321
|
+
`<code>compas / gps / viento / real</code>` pasó a la
|
|
322
|
+
string literal `<code>compass / gps / wind / true</code>`
|
|
323
|
+
(son valores del enum de pypilot, no palabras traducibles).
|
|
324
|
+
- **Sección de Instrucciones completas** en el modal Info
|
|
325
|
+
(EN + ES) cubriendo todas las pestañas ahora que Chart /
|
|
326
|
+
Race / Doctor / Piloto Inteligente / Alarmas / Calidad de
|
|
327
|
+
sensores / Grabador de sesiones / Captura de logs / Consola de
|
|
328
|
+
debug / Consola de control remoto / Watchdog están todas
|
|
329
|
+
en su sitio.
|
|
330
|
+
- **Endpoint `/status`** ahora devuelve `healthy: boolean` junto
|
|
331
|
+
con `connected`.
|
|
332
|
+
- **Focus-watch owner map**: `_focusWatches` pasó a ser
|
|
333
|
+
`Map<key, Map<owner, entry>>` para que un owner colgado de una
|
|
334
|
+
pestaña muerta no mantenga vivo un watch.
|
|
335
|
+
|
|
336
|
+
**Corregido**
|
|
337
|
+
|
|
338
|
+
- **Escrituras fantasma con el socket caído**: `client.set()`
|
|
339
|
+
ya no devuelve un éxito silencioso cuando el socket no
|
|
340
|
+
tiene pong vivo. Cada writer de nivel API o bien resuelve tras
|
|
341
|
+
el retry o bien lanza `"pypilot offline: <path>"`. Cierra la
|
|
342
|
+
clase de reportes "la UI dice enganchado pero el barco va recto".
|
|
343
|
+
- **Reconexión lenta tras cortar y encender pypilot**: el
|
|
344
|
+
watchdog baja a 15 s de intervalo con umbral de vivacidad de
|
|
345
|
+
20 s, y el provider reconcilia estado en el delta de reconexión
|
|
346
|
+
para que el visor recoja el modo / target frescos al instante.
|
|
347
|
+
- **Bug de merge en `refreshPypilotValues`**: el cambio de piloto /
|
|
348
|
+
perfil quedaba enmascarado por la heurística de "no
|
|
349
|
+
sobreescribir claves existentes". Los delta handlers de
|
|
350
|
+
`ap.pilot` y `profile` ahora pasan `{replace: true}`.
|
|
351
|
+
- **`gainReady` salía antes de tiempo**: la puerta de
|
|
352
|
+
readiness se abría al llegar cualquier gain de cualquier
|
|
353
|
+
piloto. Ahora está limitada al `state.pilot` activo,
|
|
354
|
+
para que los gains de un piloto stale no cortocircuiten el
|
|
355
|
+
pull de boot del activo.
|
|
356
|
+
- **Pill de virada "ejecutando" sin target**: la pill acababa en
|
|
357
|
+
`null` al cerrar la fase de arming. Ahora hace fallback por
|
|
358
|
+
`_tackPendingFinalTargetRad ?? _countdownPlannedTargetRad ??
|
|
359
|
+
state.target`.
|
|
360
|
+
- **TGT visible con AP enganchado + conexión caída**:
|
|
361
|
+
no llega delta de desenganche cuando pypilot se va, así
|
|
362
|
+
que el target se quedaba clavado. El formateador y
|
|
363
|
+
`renderTargetArrow` ahora respetan `state.pypilotHealthy ===
|
|
364
|
+
false`; además se disparan `scheduleRenderControl()` y
|
|
365
|
+
`renderTargetArrow()` en cada cambio de healthy.
|
|
366
|
+
- **Voz muda al reconectar** (Rev264): se añadió
|
|
367
|
+
`_alSpeak("Pypilot operativo")` en
|
|
368
|
+
`_pypilotShowReconnectedToast` para que el navegante tenga
|
|
369
|
+
confirmación audible sin mirar la pantalla.
|
|
370
|
+
|
|
371
|
+
**Arquitectura**
|
|
372
|
+
|
|
373
|
+
- Se rechazó un patrón de ping-antes-de-escribir
|
|
374
|
+
propuesto a mitad de sprint: es un anti-patrón TOCTOU
|
|
375
|
+
(time-of-check-to-time-of-use) que añade latencia a cada
|
|
376
|
+
escritura sin dar una garantía real de seguridad. Se
|
|
377
|
+
mantuvo la reflexión pasiva. La discusión queda en
|
|
378
|
+
el log de commits para futura referencia.
|
|
379
|
+
|
|
380
|
+
## 2.6.0 — 2026-09-10 — Rev178..Rev180: Trip Report sprint (still experimental)
|
|
381
|
+
|
|
382
|
+
Big feature bundle. Every trip you take (from the moment
|
|
383
|
+
`navigation.state` leaves "moored" until you're moored again) is
|
|
384
|
+
recorded in the background and turned into a shareable report card.
|
|
385
|
+
|
|
386
|
+
### English
|
|
387
|
+
|
|
388
|
+
**Added**
|
|
389
|
+
|
|
390
|
+
- **Trip recorder** (new module `trip-recorder.ts`): auto-starts when
|
|
391
|
+
`signalk-autostate` (or any plugin publishing `navigation.state`)
|
|
392
|
+
moves the vessel off "moored". Closes and computes a summary when
|
|
393
|
+
the vessel is back to "moored". Trips < 60 s or with < 60 samples
|
|
394
|
+
are discarded automatically.
|
|
395
|
+
- **Live-accumulated KPIs** so summary computation is O(1) even on
|
|
396
|
+
Pi 4: distance (haversine), TWS/AWS avg + max, SOG avg + max, heel
|
|
397
|
+
avg + max, tack count, points-of-sail split (upwind / reach /
|
|
398
|
+
downwind), battery voltage min + sag count, mode share, engaged
|
|
399
|
+
percentage. Highlights (peak wind, peak SOG, min voltage) carry
|
|
400
|
+
their timestamps so the map can pin them.
|
|
401
|
+
- **"Viajes" card in Setup** (order 27) with live trip status + list
|
|
402
|
+
of past trips. Each row: date, distance, duration, avg SOG, avg
|
|
403
|
+
TWS, tack count.
|
|
404
|
+
- **Trip detail modal** with a 17-KPI grid + emoji-tagged highlight
|
|
405
|
+
strip (⛵ ráfaga, 🌀 vel max, 🔋 volt bajo).
|
|
406
|
+
- **OpenSeaMap trace on the map**: Leaflet lazy-loaded from CDN on
|
|
407
|
+
first modal open, OSM base + OpenSeaMap seamark overlay, polyline
|
|
408
|
+
coloured by SOG bucket (< 2 / 2-4 / 4-6 / > 6 kn), markers at
|
|
409
|
+
start (green), end (red), peak wind, peak SOG, min voltage, and
|
|
410
|
+
each detected tack. Auto-fit to the trace bounding box.
|
|
411
|
+
- **"Compartir por WhatsApp" button**: renders a portrait 900×1400
|
|
412
|
+
PNG report card (title, 10 KPI cards, mini trace with the same
|
|
413
|
+
colour buckets, endpoints and highlight emojis, footer). Uses the
|
|
414
|
+
Web Share API when available so the sailor picks any target app;
|
|
415
|
+
falls back to a download + prefilled `wa.me?text=` link on
|
|
416
|
+
platforms without file share.
|
|
417
|
+
- **HTTP endpoints**: `/trip-recorder/status`, `/list`,
|
|
418
|
+
`/summary/:id`, `/download/:id`, `DELETE /trip/:id`.
|
|
419
|
+
|
|
420
|
+
**Fixed (Rev177 carry-over)**
|
|
421
|
+
|
|
422
|
+
- Tack countdown never closes: added AWA-sign / heading-rotation
|
|
423
|
+
detection so the maneuver auto-completes in visor even when
|
|
424
|
+
pypilot never publishes `ap.tack.state="none"`.
|
|
425
|
+
- Gust ghost: fade 15 s (was 10), cooldown 15 s (was 20), thresholds
|
|
426
|
+
lowered (jump ≥ 3 kn, ratio ≥ 1.25, shift ≥ 18°). In true-wind
|
|
427
|
+
mode the ghost now keys off TWS/TWA and is drawn in the teal T
|
|
428
|
+
palette instead of the amber A. Fixes the "aparece poco / no
|
|
429
|
+
atiende a rafagas / en modo real deberia mirar real" feedback.
|
|
430
|
+
- `gustStrategy` default flipped from "warn" to "off" so the
|
|
431
|
+
supervisor cannot freeze the target / boost D / temp-heavy the
|
|
432
|
+
pilot without the sailor explicitly opting in. On the last sea
|
|
433
|
+
trial one of these strategies latched and the AP "se quedo
|
|
434
|
+
pillado, yendose orzada sin buscar rumbo".
|
|
435
|
+
- Reload race: after the plugin catalog lands we re-run
|
|
436
|
+
`refreshPypilotValues()` so gain / calibration sliders that
|
|
437
|
+
rendered before the catalog was known reposition themselves at
|
|
438
|
+
the real pypilot values. Fixes "al recargar la pagina muchas
|
|
439
|
+
veces no carga los valores de sliders o de modo".
|
|
440
|
+
|
|
441
|
+
**Known limits (still experimental)**
|
|
442
|
+
|
|
443
|
+
- Trasluchar (jibe): pypilot's tack primitive only crosses the wind
|
|
444
|
+
by proa. A jibe needs a different primitive (aproado-style). Not
|
|
445
|
+
implemented yet.
|
|
446
|
+
- Trip report card renders as a static PNG. The interactive map is
|
|
447
|
+
only visible inside the visor.
|
|
448
|
+
- CDN load of Leaflet needs internet. On a boat with no data link
|
|
449
|
+
the trace panel just says "sin conexion a los tiles del mapa" and
|
|
450
|
+
the modal still shows all the KPIs.
|
|
451
|
+
|
|
452
|
+
## 2.5.2 — 2026-09-10 — Rev176: session-recorder containment
|
|
453
|
+
|
|
454
|
+
Follow-up patch on 2.5.0. After analysing the on-water sessions
|
|
455
|
+
Carlos captured with Rev175 we found a pile of 1-sample JSONL
|
|
456
|
+
files caused by an external HTTP client (likely a KIP dashboard
|
|
457
|
+
tab, an idle visor tab, or a competing pypilot_web instance)
|
|
458
|
+
cycling `POST /engage` -> `POST /disengage` every 5-6 s. Every
|
|
459
|
+
flicker opened and closed a session file. This tranche contains
|
|
460
|
+
the damage and fixes several analysis blind spots.
|
|
461
|
+
|
|
462
|
+
### English
|
|
463
|
+
|
|
464
|
+
**Fixed**
|
|
465
|
+
|
|
466
|
+
- **Engaged debounce (8 s)** in the historian tick. A new
|
|
467
|
+
engage/disengage transition is only propagated to the session
|
|
468
|
+
recorder after the raw value has held for 8 s. This filters
|
|
469
|
+
the rebounds that were opening one JSONL every ~5 s during the
|
|
470
|
+
test window on 2026-09-08.
|
|
471
|
+
- **Bounce diagnostics**: filtered flips are counted and exposed
|
|
472
|
+
through `/session-recorder/status` (`bouncesFiltered`,
|
|
473
|
+
`lastBounceMs`, `lastBounceDetails`), plus a `[bounce]` line
|
|
474
|
+
in `app.debug` so the next culprit can be traced without
|
|
475
|
+
waiting for a repeat.
|
|
476
|
+
- **Discard short sessions on close**: if a session has less
|
|
477
|
+
than 30 samples OR less than 30 s of duration when the AP
|
|
478
|
+
disengages, the JSONL is unlinked instead of being flushed
|
|
479
|
+
and kept. Belt-and-suspenders on top of the debounce.
|
|
480
|
+
- **Retroactive purge**: on plugin start the recorder sweeps
|
|
481
|
+
`nav-sessions/` and deletes every closed file that meets the
|
|
482
|
+
same criterion. The tester's 54-file directory came down to 7
|
|
483
|
+
useful sessions after Rev176 booted.
|
|
484
|
+
- **Manual purge endpoint**: `POST /plugins/signalk-pypilot-newui/session-recorder/purge-short`
|
|
485
|
+
triggers the sweep on demand.
|
|
486
|
+
- **hdgErr per AP mode**: the recorder stored
|
|
487
|
+
`headingCmd - headingActual` regardless of mode, which was
|
|
488
|
+
garbage in `wind` / `true wind` where `headingCmd` is a wind
|
|
489
|
+
angle, not a heading. Now compass-family modes still store the
|
|
490
|
+
heading delta, `wind` mode stores `headingCmd - AWA`, and
|
|
491
|
+
`true wind` mode stores `headingCmd - TWA`. Offline analysers
|
|
492
|
+
finally get a real number for wind modes.
|
|
493
|
+
- **Pi Zero log-capture no longer re-injects the tail on
|
|
494
|
+
restart**: the per-source `lastLineHash` map is now persisted
|
|
495
|
+
to `pizero-log-state.json` and reloaded on plugin start. The
|
|
496
|
+
file used to grow 32x on the tester's setup because every
|
|
497
|
+
`-Restart` deploy reset the in-memory cursor and re-appended
|
|
498
|
+
the entire tail.
|
|
499
|
+
|
|
500
|
+
**Changed**
|
|
501
|
+
|
|
502
|
+
- **Gust ghost arrow recoloured to orange** so it stands out
|
|
503
|
+
from the live amber A arrow when the two land near the same
|
|
504
|
+
angle. The overlay-card preview matches.
|
|
505
|
+
- **Gust ghost detection thresholds lowered** (Rev175 carry-
|
|
506
|
+
over): AWS jump 4 kn / ratio 1.35, AWA shift 20 deg, baseline
|
|
507
|
+
min 2 kn, cooldown 20 s. Tuned against 2 h of real navigation
|
|
508
|
+
data (~18 potential fires for a 14 kn-avg sail).
|
|
509
|
+
|
|
510
|
+
**Investigation notes (for the curious)**
|
|
511
|
+
|
|
512
|
+
The 71-second racimo on 2026-09-08 19:01-02 (Rev166 that day)
|
|
513
|
+
shows the pattern:
|
|
514
|
+
|
|
515
|
+
engage -> PUT target -> PUT mode -> disengage -> engage -> ...
|
|
516
|
+
|
|
517
|
+
repeating every 5-6 s at the SK Autopilot API v2 endpoint. Not
|
|
518
|
+
manual: too regular. Culprit unidentified with the logs at
|
|
519
|
+
hand - the SK journal does not record client IP/UA. Suspects
|
|
520
|
+
still alive:
|
|
521
|
+
|
|
522
|
+
- A custom KIP dashboard widget that toggles engage on a tick.
|
|
523
|
+
- Two open visor tabs where one has a stale state machine and
|
|
524
|
+
re-issues engage on each SSE reconnect.
|
|
525
|
+
- A feedback loop through `signalk-pypilot` mirroring an incoming
|
|
526
|
+
bus delta as an outgoing SK write.
|
|
527
|
+
|
|
528
|
+
pypilot_web on the KIP tablet was cleared as a suspect - the
|
|
529
|
+
captured pypilot log shows its socket.io connections spaced
|
|
530
|
+
minutes apart, not seconds.
|
|
531
|
+
|
|
532
|
+
## 2.5.1 — 2026-09-09 — README bilingual
|
|
533
|
+
|
|
534
|
+
Docs-only patch. The EXPERIMENTAL banner in README and CHANGELOG
|
|
535
|
+
now appears in English *and* Spanish so the SignalK App Store
|
|
536
|
+
description communicates the sea-trial-pending status to English
|
|
537
|
+
speakers too. No code changes.
|
|
538
|
+
|
|
3
539
|
## 2.5.0 — 2026-09-09 — Rev154..Rev174
|
|
4
540
|
|
|
541
|
+
> ## ⚠️ Ships with features that have not been tested at sea yet
|
|
542
|
+
>
|
|
543
|
+
> This release brings NEW features that we **could not validate under
|
|
544
|
+
> sail** yet (there was not enough wind the day we cut it). Install
|
|
545
|
+
> it if you fancy helping us test them — and if anything looks off,
|
|
546
|
+
> **tell us**: https://github.com/Aitonos/signalk-pypilot-newui/issues
|
|
547
|
+
>
|
|
548
|
+
> Your feedback is GOLD for tuning the thresholds before we sign
|
|
549
|
+
> these features off as stable for everyone.
|
|
550
|
+
>
|
|
5
551
|
> ## ⚠️ Version con funciones aun sin probar en el mar
|
|
6
552
|
>
|
|
7
553
|
> Esta version trae funciones NUEVAS que **no hemos podido validar
|
package/README.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# signalk-pypilot-newui
|
|
2
2
|
|
|
3
|
+
> ### ⚠️ 2.5.0 ships new features that have not been tested at sea yet
|
|
4
|
+
>
|
|
5
|
+
> The day we cut this release there was not enough wind to validate
|
|
6
|
+
> the tack countdown, the post-tack stats, the tack log, the gust
|
|
7
|
+
> marker, or the "auto brain" that switches profiles and reacts to
|
|
8
|
+
> gusts for you. **Your feedback is GOLD:** install it, try it, and if
|
|
9
|
+
> anything looks off, tell us at
|
|
10
|
+
> https://github.com/Aitonos/signalk-pypilot-newui/issues
|
|
11
|
+
> That is how we tune the thresholds before we sign these features
|
|
12
|
+
> off as stable.
|
|
13
|
+
>
|
|
3
14
|
> ### ⚠️ 2.5.0 trae funciones que aun no hemos probado en el mar
|
|
4
15
|
>
|
|
5
16
|
> El dia de sacarla no habia viento suficiente para validar el
|
|
@@ -207,11 +218,11 @@ in-session Revs on top of the 2.1.0 visual foundation. Highlights:
|
|
|
207
218
|
other) with the same look as the Tune tab, a lock checkbox with
|
|
208
219
|
the padlock emoji, a frozen "before:" baseline that never moves
|
|
209
220
|
as you drag, and a per-slider ↺ restore button.
|
|
210
|
-
- **
|
|
211
|
-
the plugin: the AMBER "A" arrow (apparent wind) is now the
|
|
212
|
-
piece and the TEAL "T" arrow (true wind) the SMALL outer
|
|
213
|
-
Colors and letters kept their canonical meaning; only the
|
|
214
|
-
paths moved so the amber piece is visually the dominant one.
|
|
221
|
+
- **Target/wind arrows shape swap** — after iterations with skippers
|
|
222
|
+
using the plugin: the AMBER "A" arrow (apparent wind) is now the
|
|
223
|
+
LARGE piece and the TEAL "T" arrow (true wind) the SMALL outer
|
|
224
|
+
one. Colors and letters kept their canonical meaning; only the
|
|
225
|
+
SVG paths moved so the amber piece is visually the dominant one.
|
|
215
226
|
- **Full i18n audit** across EN / ES / FR / DE — every hard-coded
|
|
216
227
|
English string in the non-EN bundles purged, alarm messages and
|
|
217
228
|
Doctor suggestion reason / effect exposed as i18n keys + args so
|
|
@@ -247,9 +258,9 @@ pypilot version, restart pypilot_web).
|
|
|
247
258
|
- Compass rose with HDG readout, colour-coded cardinals (N orange, E green,
|
|
248
259
|
S blue, W red) + degree ticks, hull-fixed green/red sailing wedges, and 4
|
|
249
260
|
configurable corner tiles (long-press to reconfigure).
|
|
250
|
-
- **
|
|
261
|
+
- **Concentric target & wind arrows**: three arrow pieces on the rose — cyan
|
|
251
262
|
target diamond, amber `A` (AWA), sea-green `T` (TWA). Interlock into one
|
|
252
|
-
clean droplet when all three angles coincide; the V-
|
|
263
|
+
clean droplet when all three angles coincide; the V-notch of any piece
|
|
253
264
|
that drifts opens up as a visible hollow, and a colour ring-tick marks
|
|
254
265
|
the exact angle on the compass. Optional **COG line** from the bow
|
|
255
266
|
(yellow chevrons) and **current vector** (SOG − SOW, blue wavy arrow)
|
package/dist/alarms.js
CHANGED
|
@@ -30,7 +30,12 @@ exports.RULE_UNABLE_DUTY_MIN = 0.7; // 70% duty
|
|
|
30
30
|
exports.RULE_SERVO_OVERCURR_A = 5.0;
|
|
31
31
|
exports.RULE_SERVO_TEMP_C = 60;
|
|
32
32
|
exports.RULE_LOW_VOLTAGE_V = 11.0;
|
|
33
|
-
|
|
33
|
+
// Rev256 lowered this from 10 s to 3 s. Rev258 (Carlos): with the new
|
|
34
|
+
// pong-aware `healthy` check the socket is flagged offline after ~8 s
|
|
35
|
+
// of missed pings (much earlier than TCP heartbeat would), so we can
|
|
36
|
+
// afford a very short sustain here. Total from real pypilot power-off
|
|
37
|
+
// to alarm firing: ~8 s + 1 s = 9 s.
|
|
38
|
+
exports.RULE_PYPILOT_DISC_SEC = 1;
|
|
34
39
|
exports.DEFAULT_RULES = [
|
|
35
40
|
{
|
|
36
41
|
id: "heading-deviation",
|
|
@@ -188,6 +193,13 @@ exports.DEFAULT_RULES = [
|
|
|
188
193
|
evaluate: (c) => {
|
|
189
194
|
if (!c.quality)
|
|
190
195
|
return false;
|
|
196
|
+
// Rev256 (Carlos): if pypilot itself is disconnected, EVERY
|
|
197
|
+
// pypilot-fed sensor will read as "lost" too. Suppress this
|
|
198
|
+
// alarm while pypilot is down - the dedicated pypilot-disconnected
|
|
199
|
+
// alarm already tells the sailor the real cause and cascade
|
|
200
|
+
// noise only hides it.
|
|
201
|
+
if (!c.connected)
|
|
202
|
+
return false;
|
|
191
203
|
return c.quality.items.some((it) => it.level === "lost");
|
|
192
204
|
},
|
|
193
205
|
message: (c) => {
|
|
@@ -197,7 +209,7 @@ exports.DEFAULT_RULES = [
|
|
|
197
209
|
},
|
|
198
210
|
{
|
|
199
211
|
id: "pypilot-disconnected",
|
|
200
|
-
label: "
|
|
212
|
+
label: "Pypilot desconectado",
|
|
201
213
|
severity: "alarm",
|
|
202
214
|
defaultEnabled: true,
|
|
203
215
|
sustainSec: exports.RULE_PYPILOT_DISC_SEC,
|
|
@@ -205,9 +217,26 @@ exports.DEFAULT_RULES = [
|
|
|
205
217
|
evaluate: (c) => !c.connected,
|
|
206
218
|
message: (c) => {
|
|
207
219
|
if (c.disconnectedSinceMs == null)
|
|
208
|
-
return "
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
return "Pypilot desconectado";
|
|
221
|
+
// Rev265/266 (Carlos): palabras completas ("segundos", "minutos",
|
|
222
|
+
// "horas") - las abreviaturas "s" / "min" el TTS las lee mal ("dos
|
|
223
|
+
// ese" en vez de "dos segundos"). Uso plural/singular correcto.
|
|
224
|
+
const totalSec = Math.floor((c.nowMs - c.disconnectedSinceMs) / 1000);
|
|
225
|
+
const h = Math.floor(totalSec / 3600);
|
|
226
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
227
|
+
const s = totalSec % 60;
|
|
228
|
+
const w = (n, sing, plur) => `${n} ${n === 1 ? sing : plur}`;
|
|
229
|
+
let tail;
|
|
230
|
+
if (h > 0) {
|
|
231
|
+
tail = `${w(h, "hora", "horas")}, ${w(m, "minuto", "minutos")} y ${w(s, "segundo", "segundos")}`;
|
|
232
|
+
}
|
|
233
|
+
else if (m > 0) {
|
|
234
|
+
tail = `${w(m, "minuto", "minutos")} y ${w(s, "segundo", "segundos")}`;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
tail = w(s, "segundo", "segundos");
|
|
238
|
+
}
|
|
239
|
+
return `Pypilot desconectado desde hace ${tail}`;
|
|
211
240
|
},
|
|
212
241
|
},
|
|
213
242
|
];
|