signalk-pypilot-newui 3.1.3 → 3.2.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 +103 -0
- package/README.md +17 -3
- package/dist/index.js +54 -54
- package/package.json +1 -1
- package/public/app.js +431 -82
- package/public/index.html +90 -8
- package/public/style.css +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.2.0 — 2026-10-15 — AIS rose rebuilt on real positions, tide popup, chart kept warm
|
|
4
|
+
|
|
5
|
+
Rolls up Rev470 → Rev482 on top of 3.1.4. Minor bump: new UI (tide
|
|
6
|
+
button + popup, "Chart in the background" card, anchor-watch visor
|
|
7
|
+
embedded in the rose) plus a set of AIS rose fixes found in port QA in
|
|
8
|
+
Moaña. No breaking changes.
|
|
9
|
+
|
|
10
|
+
### Fixed — AIS targets on the rose (root cause)
|
|
11
|
+
|
|
12
|
+
- **The target position/SOG/COG cache never filled.** The REST calls
|
|
13
|
+
used `encodeURIComponent` on the vessel URN
|
|
14
|
+
(`urn%3Amrn%3Aimo%3Ammsi%3A…`) and Signal K answers 404 to that; the
|
|
15
|
+
raw URN answers 200. With an empty cache the hull fell back to
|
|
16
|
+
`cpaPositions.target` — the target's FUTURE position at CPA — so a
|
|
17
|
+
boat coming from 7 o'clock was drawn at 11 o'clock and "0 m" away.
|
|
18
|
+
The fallback is removed: no current position → no hull on the rose
|
|
19
|
+
(the infobox still shows the alarm). Cache refresh 30 s → 5 s.
|
|
20
|
+
(Rev474)
|
|
21
|
+
- **Hull and projected track were drawn 180° off.** The sprite has its
|
|
22
|
+
bow at +x, so a direction needs `rotate(angle − 90)`; Rev457 used
|
|
23
|
+
`+ 90`. (Rev470)
|
|
24
|
+
- **Moored boats.** A stopped target's COG is GPS noise: the hull is
|
|
25
|
+
oriented by `headingTrue` when available, by COG only when moving
|
|
26
|
+
(≥ 0.5 kn), otherwise it points at us; the dashed track is drawn only
|
|
27
|
+
for moving targets and always along COG. The "stationary" filter
|
|
28
|
+
(now 0.5 kn) is re-evaluated when the REST cache lands, not only on
|
|
29
|
+
the alarm delta. (Rev470, Rev471)
|
|
30
|
+
- **Targets drawn on top of our own hull.** Radial position is now a
|
|
31
|
+
log scale (50 m → 2 NM) over the free ring between our hull outline
|
|
32
|
+
and the compass ring, with a distance label ("40 m", "0.4 NM") under
|
|
33
|
+
each target. (Rev472)
|
|
34
|
+
- **Target name in the infobox** comes from the Signal K vessel `name`
|
|
35
|
+
first, then from the alarm text; "MMSI 123456789" is no longer shown
|
|
36
|
+
as a name. (Rev481)
|
|
37
|
+
- **AIS card:** the Harbour / At anchor / Sailing selector showed raw
|
|
38
|
+
i18n keys; all texts rewritten in plain language (EN/ES). (Rev473)
|
|
39
|
+
- **Unstyled flash on reload:** critical inline CSS in `<head>` and
|
|
40
|
+
`style.css?v=RevNNN` cache-busting from `deploy.ps1`. (Rev476)
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Tide button** above the bottom-right corner tile of the rose (shown
|
|
45
|
+
only when `signalk-mareas-ihm` is installed). Opens a popup with the
|
|
46
|
+
station, current height, rising/falling, coefficient, next HW/LW and
|
|
47
|
+
a −6 h → +12 h tide curve. "Open full Tides" opens the
|
|
48
|
+
signalk-mareas-ihm anchor-watch visor inside the rose, same chrome as
|
|
49
|
+
Freeboard (maximize / close). Data from `/signalk-mareas-ihm/api/extremes`
|
|
50
|
+
and `environment.tide.*`, fetched only when the popup opens.
|
|
51
|
+
(Rev478 → Rev480, Rev482)
|
|
52
|
+
- **Freeboard-SK kept loaded in the background** so a double-tap on the
|
|
53
|
+
rose opens it instantly with AIS names already in place. Own card
|
|
54
|
+
"Chart in the background" with a per-device switch and a "?" that
|
|
55
|
+
explains when to turn it off (lag / battery). (Rev475 → Rev477)
|
|
56
|
+
|
|
57
|
+
## 3.1.4 — 2026-10-14 — iOS "Add to Home Screen" PWA basics (issue #5)
|
|
58
|
+
|
|
59
|
+
Rev469. Patch bump: no code changes, just the HTML shell. Thanks to
|
|
60
|
+
[@eric-fitzsimon](https://github.com/eric-fitzsimon) for the detailed
|
|
61
|
+
report in [#5](https://github.com/Aitonos/signalk-pypilot-newui/issues/5):
|
|
62
|
+
on iOS, adding the webapp to the home screen worked the first launch,
|
|
63
|
+
then subsequent launches loaded the page but Tune gain sliders stayed
|
|
64
|
+
empty and Chart did not populate.
|
|
65
|
+
|
|
66
|
+
### Added — PWA meta tags in the shell
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
70
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
71
|
+
<meta name="apple-mobile-web-app-title" content="PyPilot" />
|
|
72
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
73
|
+
<meta name="theme-color" content="#0a1220" />
|
|
74
|
+
<link rel="apple-touch-icon" href="icon.svg" />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Previously the shell had none of these, so iOS treated the home-screen
|
|
78
|
+
bookmark as a half-baked WebView bookmark with a storage context
|
|
79
|
+
distinct from Safari and no explicit standalone contract.
|
|
80
|
+
|
|
81
|
+
### Fixed — stale `app.js` cached indefinitely by iOS WebView
|
|
82
|
+
|
|
83
|
+
Added `Cache-Control: no-cache, no-store, must-revalidate` + legacy
|
|
84
|
+
`Pragma` / `Expires` HTTP-equiv metas to the shell. iOS Safari (both
|
|
85
|
+
browser and standalone WebView) was caching the first successful load
|
|
86
|
+
of `app.js` and ignoring the `?v=RevNNN` query bump that `deploy.ps1`
|
|
87
|
+
writes on every deploy. Result: user's bookmark served a stale JS
|
|
88
|
+
bundle for days, mismatching the backend API and leaving features
|
|
89
|
+
unpopulated. These metas force the WebView to re-fetch the shell on
|
|
90
|
+
every launch; the actual `app.js?v=RevNNN` request then matches the
|
|
91
|
+
current backend.
|
|
92
|
+
|
|
93
|
+
See `docs/_audit_ios_pwa_issue_5.md` for the full forensics.
|
|
94
|
+
|
|
95
|
+
### Known limitations
|
|
96
|
+
|
|
97
|
+
- `apple-touch-icon` points to `icon.svg`. iOS prefers PNG for the
|
|
98
|
+
home-screen icon; with SVG only, iOS may use a page-screenshot
|
|
99
|
+
fallback. A proper PNG set is TODO for a future release.
|
|
100
|
+
- No Service Worker. Rev-A path only — if the Cache-Control metas do
|
|
101
|
+
not fully fix the issue on Eric's device, Rev-B/C/D will add a
|
|
102
|
+
`visibilitychange` reconnect handler, a standalone-mode banner with
|
|
103
|
+
"Reset app data" button, and eventually a proper SW. Not shipped
|
|
104
|
+
yet because they are not confirmed necessary.
|
|
105
|
+
|
|
3
106
|
## 3.1.3 — 2026-10-14 — Safety: offline-aware write gating + batched trace + AIS polish
|
|
4
107
|
|
|
5
108
|
Rolls up Rev464 → Rev468 (5 revs) on top of 3.1.2. Patch bump: no
|
package/README.md
CHANGED
|
@@ -277,7 +277,10 @@ on its own), but installing them unlocks the features listed.
|
|
|
277
277
|
— Double-tap the compass rose to open Freeboard-SK embedded inside
|
|
278
278
|
the rose box (side panels stay visible and usable). Lets you drop
|
|
279
279
|
from the autopilot view to the full chart with every AIS target in
|
|
280
|
-
one tap, without leaving the pypilot-newui session.
|
|
280
|
+
one tap, without leaving the pypilot-newui session. Freeboard is kept
|
|
281
|
+
loaded in the background so it opens instantly (per-device switch in
|
|
282
|
+
the "Chart in the background" card if a weak tablet prefers lazy
|
|
283
|
+
loading). **Pair with a
|
|
281
284
|
Signal K chart provider (e.g. `@signalk/charts-plugin`) or the chart
|
|
282
285
|
area will stay empty** — see
|
|
283
286
|
[Chart provider is required](#chart-provider-is-required-for-the-embedded-plotter).
|
|
@@ -286,13 +289,24 @@ on its own), but installing them unlocks the features listed.
|
|
|
286
289
|
- **[`signalk-collision-alerts`](https://www.npmjs.com/package/signalk-collision-alerts)**
|
|
287
290
|
by @dirkwa — Provides the CPA/TCPA engine behind our AIS collision
|
|
288
291
|
overlay. Pypilot-newui subscribes to
|
|
289
|
-
`notifications.navigation.closestApproach.*` and
|
|
290
|
-
|
|
292
|
+
`notifications.navigation.closestApproach.*` and draws each target
|
|
293
|
+
on the rose at its real bearing and distance (log scale, distance
|
|
294
|
+
label), oriented by its heading / course with a projected track line
|
|
295
|
+
when it is moving, plus an infobox with ACK that routes
|
|
291
296
|
through the Signal K v2 Notifications API, so an ACK from
|
|
292
297
|
pypilot-newui is also seen by Freeboard-SK and every other SK
|
|
293
298
|
client.
|
|
294
299
|
See `docs/_feature_ais_overlay.md`.
|
|
295
300
|
|
|
301
|
+
- **[`signalk-mareas-ihm`](https://www.npmjs.com/package/signalk-mareas-ihm)**
|
|
302
|
+
("AnchorWatch Pro: Smart Anchoring, AIS & Tides" in the App Store) —
|
|
303
|
+
**Recommended.** Unlocks the tide button on the rose (popup with the
|
|
304
|
+
tide curve, next HW/LW and coefficient) and opens its anchor-watch
|
|
305
|
+
visor inside the rose. It also fills AIS vessel names into the Signal
|
|
306
|
+
K tree from its own persistent AIS database, so pypilot-newui,
|
|
307
|
+
Freeboard-SK and collision-alerts show names instead of raw MMSIs
|
|
308
|
+
right after a server restart.
|
|
309
|
+
|
|
296
310
|
- **[`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)**
|
|
297
311
|
by Panaaj — Shares the single pypilot socket cleanly with our
|
|
298
312
|
autopilot provider (see next section) so KIP, Freeboard's Autopilot
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ const trip_recorder_1 = require("./trip-recorder");
|
|
|
28
28
|
const maneuver_trace_1 = require("./maneuver-trace");
|
|
29
29
|
// Rev counter bumped on every build so the user can distinguish deploys
|
|
30
30
|
// from the webapp header (feedback_revision_bump_each_build).
|
|
31
|
-
const PLUGIN_REVISION = "
|
|
31
|
+
const PLUGIN_REVISION = "Rev482";
|
|
32
32
|
// Rev59: read package.json once at load time so /status can report the
|
|
33
33
|
// npm package version alongside the internal Rev counter.
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -193,7 +193,7 @@ module.exports = function (app) {
|
|
|
193
193
|
let _tackStatePrev = null;
|
|
194
194
|
let _tackDirection = null;
|
|
195
195
|
// Rev283: persistent servo error log. Writes JSONL to dataDir on
|
|
196
|
-
// every off
|
|
196
|
+
// every off→on transition of a servo-* alarm rule and remembers the
|
|
197
197
|
// 30 s pre-fault window from the historian.
|
|
198
198
|
let servoErrorLog = null;
|
|
199
199
|
// Rev286 (B2): profile advisor + last-published state for the SK
|
|
@@ -223,9 +223,9 @@ module.exports = function (app) {
|
|
|
223
223
|
// Rev323 (Carlos, 2026-09-27): pseudo-modo visor-side. El visor lo
|
|
224
224
|
// posta a `/maneuver-state` al arrancar / cerrar Aproado o Empopado.
|
|
225
225
|
// Se usa para silenciar heading-deviation / cruise-drift / unable-
|
|
226
|
-
// to-steer durante la maniobra, INDEPENDIENTE de `ap.tack.state`
|
|
226
|
+
// to-steer durante la maniobra, INDEPENDIENTE de `ap.tack.state` —
|
|
227
227
|
// en modo wind pypilot NO obedece y ap.tack.state se queda en "none"
|
|
228
|
-
// aunque el visor
|
|
228
|
+
// aunque el visor esté pilotando la maniobra por otras vÃas.
|
|
229
229
|
let _maneuverPseudoActive = false;
|
|
230
230
|
let lastNavState = null;
|
|
231
231
|
let sessionSampleTimer = null;
|
|
@@ -382,24 +382,24 @@ module.exports = function (app) {
|
|
|
382
382
|
sensorsIgnored: {
|
|
383
383
|
type: "array",
|
|
384
384
|
title: "Ignored sensor SK paths",
|
|
385
|
-
description: "List of Signal K paths the sailor has marked as 'not on this boat' from Setup
|
|
385
|
+
description: "List of Signal K paths the sailor has marked as 'not on this boat' from Setup → Sensor Quality. Ignored sensors stop turning the Setup chip red without hiding the row. Managed from the visor; edit here only if you know the exact path names.",
|
|
386
386
|
items: { type: "string" },
|
|
387
387
|
default: [],
|
|
388
388
|
},
|
|
389
389
|
tripRecorderEnabled: {
|
|
390
390
|
type: "boolean",
|
|
391
|
-
title: "Record logbook (
|
|
392
|
-
description: "When ON, the plugin auto-opens a trip on every leg away from moored, records position + wind + speed samples, and closes on return with a summary + KPI. Files stay on the Pi 5. Managed from Setup
|
|
391
|
+
title: "Record logbook (bitácora) trips",
|
|
392
|
+
description: "When ON, the plugin auto-opens a trip on every leg away from moored, records position + wind + speed samples, and closes on return with a summary + KPI. Files stay on the Pi 5. Managed from Setup → Logbook.",
|
|
393
393
|
default: true,
|
|
394
394
|
},
|
|
395
395
|
maneuverTraceEnabled: {
|
|
396
396
|
type: "boolean",
|
|
397
397
|
title: "Trace user maneuvers vs. pypilot response (sea-trial forensics)",
|
|
398
|
-
description: "When ON, every maneuver-related action from the visor (aproado / empopado / tack / mode change / target PUT / engage) is logged along with pypilot state before and 300 ms after the command, so we can audit whether pypilot actually obeyed. Files land under <plugin data dir>/maneuver-trace/*.jsonl. Off by default
|
|
398
|
+
description: "When ON, every maneuver-related action from the visor (aproado / empopado / tack / mode change / target PUT / engage) is logged along with pypilot state before and 300 ms after the command, so we can audit whether pypilot actually obeyed. Files land under <plugin data dir>/maneuver-trace/*.jsonl. Off by default — enable only during a sea trial.",
|
|
399
399
|
default: false,
|
|
400
400
|
},
|
|
401
401
|
// Rev299 (2026-09-24): per-rule alarm enable list. Managed from
|
|
402
|
-
// the visor's Setup
|
|
402
|
+
// the visor's Setup → Alarms card. NO schema default here on
|
|
403
403
|
// purpose: an existing install that upgraded from a pre-Rev299
|
|
404
404
|
// plugin must not have its (implicit) alarms silenced by a
|
|
405
405
|
// schema default of []. The visor is responsible for prompting
|
|
@@ -407,7 +407,7 @@ module.exports = function (app) {
|
|
|
407
407
|
alarmsEnabled: {
|
|
408
408
|
type: "array",
|
|
409
409
|
title: "Alarms enabled (rule ids)",
|
|
410
|
-
description: "List of alarm rule ids that are active. When absent the plugin keeps its legacy behaviour (every rule on). Managed from the visor's Setup
|
|
410
|
+
description: "List of alarm rule ids that are active. When absent the plugin keeps its legacy behaviour (every rule on). Managed from the visor's Setup → Alarms card; edit here only if you know the rule ids (heading-deviation, servo-overcurrent, low-voltage, etc.).",
|
|
411
411
|
items: { type: "string" },
|
|
412
412
|
},
|
|
413
413
|
// Rev167 (Carlos): the Smart Pilot toggles (auto-profile, gust
|
|
@@ -438,8 +438,8 @@ module.exports = function (app) {
|
|
|
438
438
|
});
|
|
439
439
|
// Rev271 (audit R05): propagate the allowWrites gate to the
|
|
440
440
|
// single write path in PypilotClient.set(). Every route to a
|
|
441
|
-
// pypilot write
|
|
442
|
-
// paths, momentary switches
|
|
441
|
+
// pypilot write — provider interface, PUT handlers on action
|
|
442
|
+
// paths, momentary switches — goes through set(), so this
|
|
443
443
|
// check covers them all.
|
|
444
444
|
client.setAllowWrites(props.allowWrites !== false);
|
|
445
445
|
client.on("connect", () => {
|
|
@@ -511,7 +511,7 @@ module.exports = function (app) {
|
|
|
511
511
|
if (changedField)
|
|
512
512
|
pushAutopilotUpdate(changedField);
|
|
513
513
|
}
|
|
514
|
-
// Rev299 (I2): watch ap.tack.state for the tacking
|
|
514
|
+
// Rev299 (I2): watch ap.tack.state for the tacking → none
|
|
515
515
|
// transition, which is the moment the sailor needs the
|
|
516
516
|
// catch-up nudge. `ap.tack.direction` is watched separately
|
|
517
517
|
// so we already know which side we tacked onto by the time
|
|
@@ -1275,7 +1275,7 @@ module.exports = function (app) {
|
|
|
1275
1275
|
// reject embedded webapps served over Signal K. Routing tiles
|
|
1276
1276
|
// through this backend lets us send the User-Agent that OSM's
|
|
1277
1277
|
// policy requires (self-identifying app string) and centralises
|
|
1278
|
-
// caching. Zero external dependencies
|
|
1278
|
+
// caching. Zero external dependencies — Node 18+ has fetch built
|
|
1279
1279
|
// in, browser handles per-tile caching via Cache-Control.
|
|
1280
1280
|
router.get("/tiles/:z/:x/:y.png", async (req, res) => {
|
|
1281
1281
|
const z = parseInt(req.params.z, 10);
|
|
@@ -1300,7 +1300,7 @@ module.exports = function (app) {
|
|
|
1300
1300
|
}
|
|
1301
1301
|
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
1302
1302
|
res.setHeader("Content-Type", "image/png");
|
|
1303
|
-
// Cache 24 h in the browser
|
|
1303
|
+
// Cache 24 h in the browser — OSM tiles are effectively
|
|
1304
1304
|
// immutable within a day; a hard reload still fetches fresh.
|
|
1305
1305
|
res.setHeader("Cache-Control", "public, max-age=86400, immutable");
|
|
1306
1306
|
res.send(buf);
|
|
@@ -1627,7 +1627,7 @@ module.exports = function (app) {
|
|
|
1627
1627
|
// PUT /raw { name: "ap.tack.direction", value: "port|starboard" }
|
|
1628
1628
|
// PUT /raw { name: "ap.tack.state", value: "begin" }
|
|
1629
1629
|
// Rev378-380 put the virtual-tack FSM behind the POST /ap/tack/:dir
|
|
1630
|
-
// endpoint, but the frontend never calls it
|
|
1630
|
+
// endpoint, but the frontend never calls it — the raw path bypassed
|
|
1631
1631
|
// our provider.tack() entirely (seen in trace-20261001-161853 QA
|
|
1632
1632
|
// 2026-10-01 16:49: ap.tack.state=begin forwarded to pypilot
|
|
1633
1633
|
// unchanged, mode stayed "wind" throughout, no virtual-tack).
|
|
@@ -1635,14 +1635,14 @@ module.exports = function (app) {
|
|
|
1635
1635
|
// active, re-route it through the FSM and DO NOT forward the raw
|
|
1636
1636
|
// write to pypilot (otherwise pypilot runs its own buggy wind
|
|
1637
1637
|
// tack in parallel). The direction write that typically precedes
|
|
1638
|
-
// "begin" by a few ms is harmless
|
|
1638
|
+
// "begin" by a few ms is harmless — pypilot stores it but our
|
|
1639
1639
|
// FSM derives direction from its own argument.
|
|
1640
1640
|
// Rev382 (Carlos, 2026-10-01): memorise the direction the
|
|
1641
1641
|
// frontend sends in the ap.tack.direction raw write so the
|
|
1642
1642
|
// state=begin that follows ~100ms later uses the FRESH value.
|
|
1643
1643
|
// Rev381 QA on Tunatunes read ap.tack.direction from the
|
|
1644
1644
|
// pypilot values cache which still carried the previous tack's
|
|
1645
|
-
// "starboard" when the user had just pressed "port"
|
|
1645
|
+
// "starboard" when the user had just pressed "port" — the
|
|
1646
1646
|
// intercept launched the wrong direction (16:58:42 journal).
|
|
1647
1647
|
if (name === "ap.tack.direction" && (value === "port" || value === "starboard")) {
|
|
1648
1648
|
app._pypilotNewuiLastTackDirection = {
|
|
@@ -1663,7 +1663,7 @@ module.exports = function (app) {
|
|
|
1663
1663
|
? dirCache
|
|
1664
1664
|
: (dirRaw === "port" || dirRaw === "starboard" ? dirRaw : "starboard");
|
|
1665
1665
|
// eslint-disable-next-line no-console
|
|
1666
|
-
console.log(`[/raw tack intercept] mode=${modeStr} dir=${dir} (cache=${dirCache ?? "n/a"} cache=${dirRaw ?? "n/a"})
|
|
1666
|
+
console.log(`[/raw tack intercept] mode=${modeStr} dir=${dir} (cache=${dirCache ?? "n/a"} cache=${dirRaw ?? "n/a"}) → virtual-tack`);
|
|
1667
1667
|
apProvider
|
|
1668
1668
|
.toProviderInterface()
|
|
1669
1669
|
.tack(dir, apProvider.deviceId)
|
|
@@ -1897,7 +1897,7 @@ module.exports = function (app) {
|
|
|
1897
1897
|
// Rev416 (Carlos sea trial 2026-10-04, obs #12): accept an
|
|
1898
1898
|
// optional `finalAngleRad` so the visor can trim the destination
|
|
1899
1899
|
// AWA (closer-hauled or bear-away) at tack-time. Values outside
|
|
1900
|
-
// (
|
|
1900
|
+
// (-π, π] or non-finite are discarded by startVirtualTack.
|
|
1901
1901
|
const rawFinalAngle = req.body?.finalAngleRad;
|
|
1902
1902
|
const finalAngleRadOverride = typeof rawFinalAngle === "number" && Number.isFinite(rawFinalAngle)
|
|
1903
1903
|
? rawFinalAngle : null;
|
|
@@ -2567,7 +2567,7 @@ module.exports = function (app) {
|
|
|
2567
2567
|
});
|
|
2568
2568
|
// Rev291 (F1): APB target preview. Reads the current SK course
|
|
2569
2569
|
// data via app.getCourse and returns what the AP *would* steer to
|
|
2570
|
-
// if the plugin drove NAV directly. Diagnostic only
|
|
2570
|
+
// if the plugin drove NAV directly. Diagnostic only — pypilot's
|
|
2571
2571
|
// own nav mode is still the one commanding the pilot.
|
|
2572
2572
|
router.get("/nav/apb-preview", async (_req, res) => {
|
|
2573
2573
|
try {
|
|
@@ -2603,7 +2603,7 @@ module.exports = function (app) {
|
|
|
2603
2603
|
}
|
|
2604
2604
|
});
|
|
2605
2605
|
// Rev296 (Carlos, navigating - bug D): audit trail of profile
|
|
2606
|
-
// switches. Answers "who
|
|
2606
|
+
// switches. Answers "who cargó default?" — shows the last N
|
|
2607
2607
|
// changes with source attribution (user / auto-profile /
|
|
2608
2608
|
// gust-heavy / doctor / external / etc).
|
|
2609
2609
|
router.get("/profile-change-log", (_req, res) => {
|
|
@@ -2997,9 +2997,9 @@ module.exports = function (app) {
|
|
|
2997
2997
|
// flaky 4G link. This endpoint accepts {events: [{kind, payload,
|
|
2998
2998
|
// visorRev}, ...]} and records them in order. Malformed events
|
|
2999
2999
|
// are skipped silently (never block the batch). The visor side
|
|
3000
|
-
// (public/app.js _mtLog + _mtFlush) encola localmente y
|
|
3001
|
-
// cada 2 s en vez de uno a uno
|
|
3002
|
-
//
|
|
3000
|
+
// (public/app.js _mtLog + _mtFlush) encola localmente y envÃa
|
|
3001
|
+
// cada 2 s en vez de uno a uno — ~20x menos HTTP overhead en
|
|
3002
|
+
// navegación con muchos deltas.
|
|
3003
3003
|
router.post("/maneuver-trace/events", (req, res) => {
|
|
3004
3004
|
if (!maneuverTrace)
|
|
3005
3005
|
return res.status(503).json({ error: "not initialised" });
|
|
@@ -3650,12 +3650,12 @@ module.exports = function (app) {
|
|
|
3650
3650
|
rollFfTwaGateDeg: clampNumber(options.rollFfTwaGateDeg, 30, 179, 90),
|
|
3651
3651
|
// Rev298 (H4): leeway adjustment. Practical range 9..12 for a
|
|
3652
3652
|
// keelboat. 0 = disabled (nothing published). Upper cap 30 keeps
|
|
3653
|
-
// a stray fat-finger config from producing a 40
|
|
3653
|
+
// a stray fat-finger config from producing a 40° leeway.
|
|
3654
3654
|
leewayAdjustment: clampNumber(options.leewayAdjustment, 0, 30, 0),
|
|
3655
3655
|
// Rev299 (H2): failsafe boat speed in knots. Clamped 0..15;
|
|
3656
3656
|
// 0 = disabled. Practical range 3..8 kn for a cruising keelboat.
|
|
3657
3657
|
failSafeBspKn: clampNumber(options.failSafeBspKn, 0, 15, 0),
|
|
3658
|
-
// Rev299 (I2): tack catch-up. Peak 0..15
|
|
3658
|
+
// Rev299 (I2): tack catch-up. Peak 0..15° (0 disables); tau
|
|
3659
3659
|
// 1..15 s. Defaults keep the module inert; sailor opts in
|
|
3660
3660
|
// after sea trial validation.
|
|
3661
3661
|
tackCatchupDeg: clampNumber(options.tackCatchupDeg, 0, 15, 0),
|
|
@@ -3665,7 +3665,7 @@ module.exports = function (app) {
|
|
|
3665
3665
|
profileAdvisorRmsLowDeg: clampNumber(options.profileAdvisorRmsLowDeg, 0.1, 5, 1),
|
|
3666
3666
|
profileAdvisorSustainSec: clampNumber(options.profileAdvisorSustainSec, 15, 600, 60),
|
|
3667
3667
|
// Rev289 (B5): sanitise the persisted metadata against the current
|
|
3668
|
-
// schema
|
|
3668
|
+
// schema — a plugin downgrade could otherwise leave stray rows
|
|
3669
3669
|
// with unknown conditions.
|
|
3670
3670
|
profileMetadata: (0, profile_metadata_1.loadMetadata)(options.profileMetadata),
|
|
3671
3671
|
// Rev290 (E2/E3): threshold overrides, clamped to sane ranges.
|
|
@@ -3674,8 +3674,8 @@ module.exports = function (app) {
|
|
|
3674
3674
|
alarmServoMotorTempC: clampNumber(options.alarmServoMotorTempC, 40, 90, 70),
|
|
3675
3675
|
alarmPypilotDiscSec: clampNumber(options.alarmPypilotDiscSec, 3, 300, 15),
|
|
3676
3676
|
// Rev299 (I1): attitude envelope thresholds. Ranges kept wide
|
|
3677
|
-
// enough for coastal cruisers (25
|
|
3678
|
-
// put the rail under (60
|
|
3677
|
+
// enough for coastal cruisers (25°) and racers who purposely
|
|
3678
|
+
// put the rail under (60°). Defaults are conservative.
|
|
3679
3679
|
alarmAttitudeHeelDeg: clampNumber(options.alarmAttitudeHeelDeg, 15, 70, 45),
|
|
3680
3680
|
alarmAttitudePitchDeg: clampNumber(options.alarmAttitudePitchDeg, 10, 45, 25),
|
|
3681
3681
|
// Rev299: per-rule enable list. Preserve undefined so legacy
|
|
@@ -3707,14 +3707,14 @@ module.exports = function (app) {
|
|
|
3707
3707
|
if (!apProvider)
|
|
3708
3708
|
return;
|
|
3709
3709
|
// Rev387 (Carlos, 2026-10-01, GPT/Gemini consult): REVERTED Rev386
|
|
3710
|
-
// masking. Both LLMs flagged it as a bus pollution bug
|
|
3710
|
+
// masking. Both LLMs flagged it as a bus pollution bug — other SK
|
|
3711
3711
|
// clients (KIP, derived-data, etc.) subscribe to steering.autopilot.*
|
|
3712
3712
|
// and need the TRUTH about mode/target, not a convenient visor-side
|
|
3713
3713
|
// lie. Masking belongs in the VISOR's presentation layer, consuming
|
|
3714
3714
|
// the dedicated `steering.autopilot.virtualTack` path we now publish
|
|
3715
3715
|
// alongside the canonical deltas. Rev388 frontend will do the
|
|
3716
3716
|
// display-side override.
|
|
3717
|
-
// Rev394: 'virtualTack' is a VT-only notification
|
|
3717
|
+
// Rev394: 'virtualTack' is a VT-only notification — skip the
|
|
3718
3718
|
// autopilotUpdate call and the canonical delta block below, only
|
|
3719
3719
|
// emit the virtualTack path itself (which may now carry `null`
|
|
3720
3720
|
// telling the visor to drop its shield).
|
|
@@ -3745,8 +3745,8 @@ module.exports = function (app) {
|
|
|
3745
3745
|
// publishes ONLY state/engaged/availableActions. This kills the
|
|
3746
3746
|
// race where a parallel /engage + /target pair produced two deltas
|
|
3747
3747
|
// each carrying both fields; whichever landed second would clobber
|
|
3748
|
-
// the sibling with a stale copy (the "DIA jumps to 115
|
|
3749
|
-
// to 75
|
|
3748
|
+
// the sibling with a stale copy (the "DIA jumps to 115° then back
|
|
3749
|
+
// to 75°" Carlos reported on Rev82). "all" is still used by the
|
|
3750
3750
|
// 30-s keep-alive and startup push.
|
|
3751
3751
|
const values = [];
|
|
3752
3752
|
if (fields === "all" || fields === "engaged") {
|
|
@@ -3787,7 +3787,7 @@ module.exports = function (app) {
|
|
|
3787
3787
|
// Rev410 (fix T.1-A, Carlos 2026-10-04): prefer the authoritative
|
|
3788
3788
|
// `remainingDeg` written by _runVirtualTack (distance to the FINAL
|
|
3789
3789
|
// compass target, computed against the REAL heading after each
|
|
3790
|
-
// step reached
|
|
3790
|
+
// step reached — accounts for Rev407 recompute). Fallback to the
|
|
3791
3791
|
// old per-step stale calculation only when the FSM has not written
|
|
3792
3792
|
// one yet (very first snapshot, pre-publishState race).
|
|
3793
3793
|
const remainingDeg = (typeof vt.remainingDeg === "number")
|
|
@@ -3808,7 +3808,7 @@ module.exports = function (app) {
|
|
|
3808
3808
|
direction: vt.direction,
|
|
3809
3809
|
windMode: vt.windMode,
|
|
3810
3810
|
// Rev394: backend-computed kind so visor doesn't misdetect
|
|
3811
|
-
// from |AWA| vs 90
|
|
3811
|
+
// from |AWA| vs 90°. "tack" = rotation crosses bow; "jibe"
|
|
3812
3812
|
// = crosses stern.
|
|
3813
3813
|
maneuverKind: g?.maneuverKind ?? null,
|
|
3814
3814
|
finalWindTargetRad: g?.angleNewRad ?? null,
|
|
@@ -3892,7 +3892,7 @@ module.exports = function (app) {
|
|
|
3892
3892
|
return;
|
|
3893
3893
|
// Rev328 (Carlos, 2026-09-28): master switch. When the sailor
|
|
3894
3894
|
// disables the logbook from Setup we neither open new trips nor
|
|
3895
|
-
// record samples into an open one
|
|
3895
|
+
// record samples into an open one — but we DO close any trip that
|
|
3896
3896
|
// was already recording so its summary lands cleanly on disk.
|
|
3897
3897
|
if (props.tripRecorderEnabled === false) {
|
|
3898
3898
|
if (tripRecorder.isRecording()) {
|
|
@@ -3908,7 +3908,7 @@ module.exports = function (app) {
|
|
|
3908
3908
|
// Transitions.
|
|
3909
3909
|
// Rev280 (audit T10): also open a trip when the plugin boots
|
|
3910
3910
|
// with the vessel already off "moored". Previously the "start"
|
|
3911
|
-
// path only fired on a moored
|
|
3911
|
+
// path only fired on a moored → sailing transition, so restarting
|
|
3912
3912
|
// Signal K mid-outing missed the entire session and stored zero
|
|
3913
3913
|
// trips. Treat lastNavState==null (boot) the same as coming from
|
|
3914
3914
|
// "moored" for the purpose of opening a new trip.
|
|
@@ -4223,7 +4223,7 @@ module.exports = function (app) {
|
|
|
4223
4223
|
}
|
|
4224
4224
|
const currentProfile = client.getValues()["profile"];
|
|
4225
4225
|
if (currentProfile !== targetProfile) {
|
|
4226
|
-
_autoProfileLastReason = `TWS avg ${avgKn.toFixed(1)} kn
|
|
4226
|
+
_autoProfileLastReason = `TWS avg ${avgKn.toFixed(1)} kn → band ${targetBand} → profile ${targetProfile}`;
|
|
4227
4227
|
try {
|
|
4228
4228
|
profileChangeLog.markPlannedWrite(targetProfile, "auto-profile", _autoProfileLastReason);
|
|
4229
4229
|
client.set("profile", targetProfile);
|
|
@@ -4495,7 +4495,7 @@ module.exports = function (app) {
|
|
|
4495
4495
|
};
|
|
4496
4496
|
}
|
|
4497
4497
|
// Rev322 (Carlos, 2026-09-27): snapshot pypilot + SK state for the
|
|
4498
|
-
// maneuver trace log. Cheaper than collectSample()
|
|
4498
|
+
// maneuver trace log. Cheaper than collectSample() — no historian
|
|
4499
4499
|
// aggregations, no derivations. Read once, fill the flat object,
|
|
4500
4500
|
// return. Called on every trace event pre + 300 ms later.
|
|
4501
4501
|
function _captureManeuverContext() {
|
|
@@ -4547,9 +4547,9 @@ module.exports = function (app) {
|
|
|
4547
4547
|
// with state / method / message so any downstream SK client honours
|
|
4548
4548
|
// them out of the box.
|
|
4549
4549
|
// Rev299: apply the per-rule enable list to the AlarmEngine.
|
|
4550
|
-
// undefined = legacy
|
|
4550
|
+
// undefined = legacy → every rule keeps its defaultEnabled (=true
|
|
4551
4551
|
// for pre-Rev299 installs, so Tunatunes carries on unchanged).
|
|
4552
|
-
// Array = user-selected
|
|
4552
|
+
// Array = user-selected → only listed ids are enabled, everything
|
|
4553
4553
|
// else is silenced. Empty array = fresh install = ALL silent.
|
|
4554
4554
|
// Rev299 (I2): watch pypilot values that describe the tack state
|
|
4555
4555
|
// machine so we can trigger the catch-up module on completion.
|
|
@@ -4628,10 +4628,10 @@ module.exports = function (app) {
|
|
|
4628
4628
|
// Rev311 (Carlos sea trial 2026-09-25): flag para las reglas
|
|
4629
4629
|
// de heading-error / drift / unable-to-steer. Durante una
|
|
4630
4630
|
// virada nativa de pypilot (ap.tack.state != "none") el error
|
|
4631
|
-
//
|
|
4632
|
-
// `_tackStatePrev` lo mantiene observeTackTransition() al
|
|
4633
|
-
// el mismo callback de client.on("value"),
|
|
4634
|
-
// refleja el
|
|
4631
|
+
// instantáneo es enorme por diseño; silenciamos esas alarmas.
|
|
4632
|
+
// `_tackStatePrev` lo mantiene observeTackTransition() al dÃa en
|
|
4633
|
+
// el mismo callback de client.on("value"), asà que siempre
|
|
4634
|
+
// refleja el último valor conocido de pypilot.
|
|
4635
4635
|
maneuverInProgress: !!(_tackStatePrev && _tackStatePrev !== "none"),
|
|
4636
4636
|
// Rev323 (Carlos, 2026-09-27): flag pseudo-modo del visor.
|
|
4637
4637
|
maneuverPseudoActive: _maneuverPseudoActive,
|
|
@@ -4834,7 +4834,7 @@ module.exports = function (app) {
|
|
|
4834
4834
|
}
|
|
4835
4835
|
// Rev286 (B2): drive the profile advisor from the sampler tick.
|
|
4836
4836
|
// Publishes a SK notification only on state transitions to avoid
|
|
4837
|
-
// flooding
|
|
4837
|
+
// flooding — the same idempotency the AlarmEngine uses.
|
|
4838
4838
|
function tickProfileAdvisor(nowMs) {
|
|
4839
4839
|
if (!profileAdvisor || !kpis || !props.profileAdvisorEnabled)
|
|
4840
4840
|
return;
|
|
@@ -4848,7 +4848,7 @@ module.exports = function (app) {
|
|
|
4848
4848
|
publishProfileAdvisorNotification(ev);
|
|
4849
4849
|
}
|
|
4850
4850
|
else {
|
|
4851
|
-
// No new event
|
|
4851
|
+
// No new event — but if we previously published an advisory and
|
|
4852
4852
|
// the cooldown has expired, revert the notification to normal so
|
|
4853
4853
|
// downstream SK clients stop showing the banner.
|
|
4854
4854
|
const st = profileAdvisor.status(nowMs);
|
|
@@ -4931,7 +4931,7 @@ module.exports = function (app) {
|
|
|
4931
4931
|
const meta = [
|
|
4932
4932
|
{ path: "steering.autopilot.pypilot.tuning.rollFf.gain", value: { description: "Roll FF gain (unitless, output = -gain*rollHp). 0 = off." } },
|
|
4933
4933
|
{ path: "steering.autopilot.pypilot.tuning.rollFf.rollHpRad", value: { units: "rad", description: "High-pass filtered roll (dynamic component only)." } },
|
|
4934
|
-
{ path: "steering.autopilot.pypilot.tuning.rollFf.deltaRad", value: { units: "rad", description: "Feed-forward heading delta this tick. Computed even when disabled
|
|
4934
|
+
{ path: "steering.autopilot.pypilot.tuning.rollFf.deltaRad", value: { units: "rad", description: "Feed-forward heading delta this tick. Computed even when disabled — but only non-zero when gain>0, AP engaged, and |TWA| above the gate." } },
|
|
4935
4935
|
{ path: "steering.autopilot.pypilot.tuning.rollFf.appliedToAp", value: { description: "False until a later Rev flips the enable switch after sea trial." } },
|
|
4936
4936
|
];
|
|
4937
4937
|
app.handleMessage(PLUGIN_ID, {
|
|
@@ -4949,7 +4949,7 @@ module.exports = function (app) {
|
|
|
4949
4949
|
app.debug(`[roll-ff] publish failed: ${e?.message || e}`);
|
|
4950
4950
|
}
|
|
4951
4951
|
}
|
|
4952
|
-
// Rev298 (H4)
|
|
4952
|
+
// Rev298 (H4) → Rev300: publish the leeway estimate on our own
|
|
4953
4953
|
// namespace instead of the canonical `performance.leeway`. Two
|
|
4954
4954
|
// reasons: (1) signalk-derived-data already publishes on that
|
|
4955
4955
|
// canonical path with a different formula and the sailor was
|
|
@@ -4966,7 +4966,7 @@ module.exports = function (app) {
|
|
|
4966
4966
|
const meta = [
|
|
4967
4967
|
{ path: LEEWAY_PATH, value: {
|
|
4968
4968
|
units: "rad",
|
|
4969
|
-
description: "Estimated leeway (side-slip) angle from the plugin's own estimator. Formula: drift_deg = adj * heel_deg / bsp_kn^2 with adj set by the sailor (Setup
|
|
4969
|
+
description: "Estimated leeway (side-slip) angle from the plugin's own estimator. Formula: drift_deg = adj * heel_deg / bsp_kn^2 with adj set by the sailor (Setup → Smart Pilot → Sensor helpers). Positive = drifting to port. Published only when leewayAdjustment > 0. Kept off the canonical performance.leeway path so it does not overwrite signalk-derived-data.",
|
|
4970
4970
|
} },
|
|
4971
4971
|
];
|
|
4972
4972
|
app.handleMessage(PLUGIN_ID, {
|
|
@@ -4990,7 +4990,7 @@ module.exports = function (app) {
|
|
|
4990
4990
|
}
|
|
4991
4991
|
// Rev299 (I2): publish the current tack catch-up delta as a
|
|
4992
4992
|
// diagnostic path so downstream SK clients can watch the decay.
|
|
4993
|
-
// NOT applied to the pilot yet
|
|
4993
|
+
// NOT applied to the pilot yet — the sea trial gates that switch.
|
|
4994
4994
|
let tackCatchupMetaSent = false;
|
|
4995
4995
|
function publishTackCatchupPaths() {
|
|
4996
4996
|
if (!tackCatchup)
|
|
@@ -5344,7 +5344,7 @@ module.exports = function (app) {
|
|
|
5344
5344
|
// refusal. The pre-check at the top of this handler covers
|
|
5345
5345
|
// `!client.connected`, but `client.set` can still refuse on
|
|
5346
5346
|
// zombie socket (no pong > 5s), coreOffline flag or
|
|
5347
|
-
// writes-disabled config
|
|
5347
|
+
// writes-disabled config — all of which pre-Rev411 landed as
|
|
5348
5348
|
// "COMPLETED 200" to KIP/SK clients.
|
|
5349
5349
|
if (!client.set(name, value)) {
|
|
5350
5350
|
return { state: "COMPLETED", statusCode: 503, message: "pypilot write refused" };
|
|
@@ -6011,7 +6011,7 @@ module.exports = function (app) {
|
|
|
6011
6011
|
// resident for subscribers; re-emitting the CURRENT
|
|
6012
6012
|
// apProvider.data value periodically produced double-pushes
|
|
6013
6013
|
// that raced with the real change delta and got collapsed by
|
|
6014
|
-
// minPeriod:500 on the visor's subscription
|
|
6014
|
+
// minPeriod:500 on the visor's subscription — the sailor saw
|
|
6015
6015
|
// engaged=false held for ~10 s after an external engage.
|
|
6016
6016
|
// The KIP-specific momentary + mode radio + engage switch
|
|
6017
6017
|
// values still ship (KIP UI needs them refreshed to stay in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-pypilot-newui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Signal K friendly UI for the pypilot autopilot, with first-class Signal K paths and Simple Switches so any downstream Signal K client can drive or monitor the pilot. Touch-first mobile visor + full pypilot data (servo, gains, calibration, tack, warnings). Coexists with pypilot-autopilot-provider.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|