signalk-pypilot-newui 2.0.2 → 2.0.4
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 +88 -35
- package/dist/index.js +113 -10
- package/dist/pypilot-client.js +10 -5
- package/package.json +1 -1
- package/public/app.js +134 -38
- package/public/index.html +62 -40
- package/public/style.css +96 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,44 +1,97 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
3
|
+
## 2.0.4 — 2026-08-21 — Rev67
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Reintroduces (with proper QA) the UI polish + reliability work that was
|
|
6
|
+
withdrawn in 2.0.3 when 2.0.2 hit regressions. Every item below was
|
|
7
|
+
tested on Carlos's Tunatunes Pi Zero W setup before publish.
|
|
6
8
|
|
|
7
9
|
### Fixed
|
|
8
10
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
11
|
+
- **Restart engine now uses `sv` (runit), not `systemctl`.** piCore does
|
|
12
|
+
not ship systemd; the previous `sudo systemctl restart pypilot pypilot_web`
|
|
13
|
+
was silently falling through to `|| true` on every install. Only the
|
|
14
|
+
`pkill -9` had any effect, and runit's `runsv` was auto-relaunching
|
|
15
|
+
the killed processes into a slightly inconsistent state that
|
|
16
|
+
accumulated zombies + I2C contention until the Pi Zero locked up.
|
|
17
|
+
Fixed to `sudo sv restart /etc/sv/pypilot /etc/sv/pypilot_web /etc/sv/pypilot_hat`
|
|
18
|
+
with a selective `pkill -9 -f 'python.*pypilot --version'` safety net.
|
|
19
|
+
- **`pypilot --version` preset in Debug console wrapped in `timeout 3`.**
|
|
20
|
+
On piCore that command starts the full pypilot process and never
|
|
21
|
+
exits cleanly; each click was leaving a zombie holding I2C and TCP
|
|
22
|
+
resources. Falls back to reading the packaged version file.
|
|
23
|
+
- **Target diamond drifted after tapping AP on laggy links.** The
|
|
24
|
+
optimistic-target assign now happens INSTANTLY on click (same tick),
|
|
25
|
+
not after the parallel promise resolves - so the diamond snaps to
|
|
26
|
+
the captured heading immediately, without a jump to the stale value
|
|
27
|
+
then back. Nudge also updates `state.target` optimistically so the
|
|
28
|
+
diamond moves in the same tick as the button press.
|
|
29
|
+
- **socket.io auto-reconnect no longer gives up.** `reconnectionAttempts`
|
|
30
|
+
changed from 30 (which could exhaust on a long outage) to Infinity;
|
|
31
|
+
backoff capped at 30 s. Restarts done via `/debug-cmd` (`restart.web`
|
|
32
|
+
/ `reboot.pi`) now schedule an explicit `pause+resume` after the
|
|
33
|
+
operation.
|
|
34
|
+
- **Reconnection watchdog** (every 60 s, was 30 s): if the socket has
|
|
35
|
+
been disconnected > 90 s, force a pause+resume once. Max 3 forced
|
|
36
|
+
reconnects in a rolling 15-min window so a permanently-dead
|
|
37
|
+
pypilot_web is not turned into a local DDoS. Watchdog interval
|
|
38
|
+
loosened per Carlos: "con la Pi Zero debemos ser suuuuuper ratas".
|
|
39
|
+
|
|
40
|
+
### Changed - Emergency section simplified
|
|
41
|
+
|
|
42
|
+
The 3-level escalated restart triad shipped in 2.0.2 has been trimmed
|
|
43
|
+
to 2 levels in Setup -> Emergency: **RESTART pypilot** (level 2, ~10-15 s
|
|
44
|
+
outage) + **reboot Pi** (level 3, ~35-60 s). The old level 1
|
|
45
|
+
(`restart pypilot_web`) rarely helps in practice - when the Pi Zero is
|
|
46
|
+
saturated a full reboot is what actually helps - so it moved to the
|
|
47
|
+
Debug console below as an advanced preset for the ~5 % edge cases.
|
|
48
|
+
|
|
49
|
+
### Compass rose
|
|
50
|
+
|
|
51
|
+
- HDG in a small dark chip that straddles the top of the ring, narrowed
|
|
52
|
+
from 48 to 40 units wide so the enlarged target diamond can peek out
|
|
53
|
+
on all four sides even when target is dead ahead.
|
|
54
|
+
- Target diamond enlarged (28 wide, 30 tall) so its top point asoma
|
|
55
|
+
above the HDG chip and its bottom point below it.
|
|
56
|
+
- Wind arrows both solid (no dashed shaft on TWA), amber AWA (big head)
|
|
57
|
+
+ sea-green (#5ce8c0) TWA (small head), each with its own rotating
|
|
58
|
+
text label placed at very different radii so they never overlap even
|
|
59
|
+
when both arrows point in the same direction.
|
|
60
|
+
- Wind speed at the bottom of the rose now inside a small dark chip
|
|
61
|
+
(same look as the HDG chip on top). Short-tap on the chip OR on a
|
|
62
|
+
corner mapped to "wind" flips AWS/TWS. Long-press opens the popup.
|
|
63
|
+
- Corner options `AWA` and `TWA` now show angle AND matching wind speed
|
|
64
|
+
(AWA/AWS and TWA/TWS) as a combined 2-line cell, consistent with the
|
|
65
|
+
`wind` combo.
|
|
66
|
+
|
|
67
|
+
### Rudder angle indicator in the swipe-hint bar
|
|
68
|
+
|
|
69
|
+
- Default range **±70°** (was ±45°), matching typical TinyPilot configs.
|
|
70
|
+
- Explicit numeric labels at both ends showing the actual range from
|
|
71
|
+
the pypilot catalog.
|
|
72
|
+
- Intermediate tick marks every 10° plus numeric mid-labels at 20°, 40°
|
|
73
|
+
and 60° each side, so the operator reads the rudder angle at a glance.
|
|
74
|
+
|
|
75
|
+
### Internal
|
|
76
|
+
|
|
77
|
+
- Debug console preset `pypilot.version` now `timeout 3 python3 -c
|
|
78
|
+
'import pypilot; print(pypilot.__version__)'` with fallback.
|
|
79
|
+
- Merge-based `_pluginConfigMerge()` helper preserved.
|
|
80
|
+
- Per-cell state for the long-press / short-tap handler on the corner
|
|
81
|
+
dashboard (Rev65's shared timer flags were race-flipping and killing
|
|
82
|
+
the tap toggle intermittently).
|
|
83
|
+
|
|
84
|
+
## 2.0.3 — 2026-08-20 — Rev66 - rollback of 2.0.2
|
|
85
|
+
|
|
86
|
+
Codebase is functionally identical to 2.0.1. Version 2.0.2 introduced
|
|
87
|
+
regressions in the socket auto-reconnect and the new "escalated restart"
|
|
88
|
+
triad that could leave the plugin showing `connected: false` against a
|
|
89
|
+
pypilot_web that is in fact alive and serving other consumers. Rolling
|
|
90
|
+
back to the 2.0.1 behaviour and reintroducing the 2.0.2 UI polish
|
|
91
|
+
(target diamond snapshot, redesigned AWA/TWA arrows, HDG chip, wind
|
|
92
|
+
speed chip, tap wind toggle, rudder bar, debug console, escalated
|
|
93
|
+
restarts) in a later 2.0.4+ release, one item at a time with proper QA
|
|
94
|
+
before publish. 2.0.2 has been deprecated on npm.
|
|
42
95
|
|
|
43
96
|
## 2.0.1 — 2026-08-20 — Rev64
|
|
44
97
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const scanner_1 = require("./scanner");
|
|
|
6
6
|
const autopilot_provider_1 = require("./autopilot-provider");
|
|
7
7
|
// Rev counter bumped on every build so the user can distinguish deploys
|
|
8
8
|
// from the webapp header (feedback_revision_bump_each_build).
|
|
9
|
-
const PLUGIN_REVISION = "
|
|
9
|
+
const PLUGIN_REVISION = "Rev67";
|
|
10
10
|
// Rev59: read package.json once at load time so /status can report the
|
|
11
11
|
// npm package version alongside the internal Rev counter.
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -255,6 +255,17 @@ module.exports = function (app) {
|
|
|
255
255
|
catch { /* defensive */ }
|
|
256
256
|
app._pypilotNewuiKeepAlive = null;
|
|
257
257
|
}
|
|
258
|
+
// Rev66 / 2.0.4: clear the reconnection watchdog interval, otherwise
|
|
259
|
+
// a Disable+Enable cycle in SK Admin would leave the previous
|
|
260
|
+
// watchdog running against a null `client` forever.
|
|
261
|
+
const wd = app._pypilotNewuiWatchdogTimer;
|
|
262
|
+
if (wd) {
|
|
263
|
+
try {
|
|
264
|
+
clearInterval(wd);
|
|
265
|
+
}
|
|
266
|
+
catch { /* defensive */ }
|
|
267
|
+
app._pypilotNewuiWatchdogTimer = null;
|
|
268
|
+
}
|
|
258
269
|
// The SK autopilot API does not expose an unregister; on plugin stop
|
|
259
270
|
// the server drops our provider when it garbage-collects the plugin.
|
|
260
271
|
apProvider = null;
|
|
@@ -269,6 +280,16 @@ module.exports = function (app) {
|
|
|
269
280
|
},
|
|
270
281
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
271
282
|
registerWithRouter: (router) => {
|
|
283
|
+
// Rev66 / 2.0.4: shared "hard reconnect" helper - pauses the socket
|
|
284
|
+
// and re-opens it after a short beat. Used by /restart-pypilot,
|
|
285
|
+
// /debug-cmd (restart.web / reboot.pi presets) and the watchdog.
|
|
286
|
+
const doReconnect = () => {
|
|
287
|
+
try {
|
|
288
|
+
client?.pause();
|
|
289
|
+
setTimeout(() => client?.resume(), 800);
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
292
|
+
};
|
|
272
293
|
router.get("/status", (_req, res) => {
|
|
273
294
|
res.json({
|
|
274
295
|
revision: PLUGIN_REVISION,
|
|
@@ -552,13 +573,29 @@ module.exports = function (app) {
|
|
|
552
573
|
const host = props.host;
|
|
553
574
|
const sshUser = props.sshUser || "tc";
|
|
554
575
|
const sshPassword = props.sshPassword || "";
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
576
|
+
// Rev66 / 2.0.4: TinyPilot runs piCore Linux which uses runit,
|
|
577
|
+
// NOT systemd. Our previous `sudo systemctl restart ...` command
|
|
578
|
+
// was silently falling through to `|| true` on every install
|
|
579
|
+
// because systemctl does not exist. The only step that had any
|
|
580
|
+
// effect was the pkill; runsv then auto-relaunched the killed
|
|
581
|
+
// processes. That inconsistent "kill without a clean start"
|
|
582
|
+
// pattern was accumulating zombies + saturating the Pi Zero's
|
|
583
|
+
// RAM until it hung.
|
|
584
|
+
//
|
|
585
|
+
// The runit-native way: `sudo sv restart /etc/sv/<service>` sends
|
|
586
|
+
// SIGTERM to the child, waits for it to exit, and starts it back
|
|
587
|
+
// up under supervision. Clean by design. We keep pkill as a
|
|
588
|
+
// belt-and-braces safety net in case a python child ignores TERM
|
|
589
|
+
// (some pypilot subprocesses have historically done that).
|
|
590
|
+
const restartCmd = [
|
|
591
|
+
"sudo sv restart /etc/sv/pypilot /etc/sv/pypilot_web /etc/sv/pypilot_hat 2>&1 || true",
|
|
592
|
+
"sleep 2",
|
|
593
|
+
"sudo pkill -9 -f 'python.*pypilot --version' 2>&1 || true",
|
|
594
|
+
"sleep 1",
|
|
595
|
+
"sudo sv up /etc/sv/pypilot /etc/sv/pypilot_web /etc/sv/pypilot_hat 2>&1 || true",
|
|
596
|
+
].join(" ; ");
|
|
597
|
+
// `doReconnect` is defined at the router scope (Rev66) and reused by
|
|
598
|
+
// /restart-pypilot, /debug-cmd and the watchdog.
|
|
562
599
|
if (!sshPassword) {
|
|
563
600
|
doReconnect();
|
|
564
601
|
return res.status(202).json({
|
|
@@ -649,8 +686,22 @@ module.exports = function (app) {
|
|
|
649
686
|
"top.snapshot": "top -bn1 2>&1 | head -20",
|
|
650
687
|
"uptime": "uptime && cat /proc/loadavg",
|
|
651
688
|
"df": "df -h 2>&1",
|
|
652
|
-
|
|
653
|
-
|
|
689
|
+
// Rev66 / 2.0.4: `pypilot --version` starts the full pypilot
|
|
690
|
+
// process and never exits cleanly on TinyCore + pypilot 2021 -
|
|
691
|
+
// every click of this preset left a python zombie holding I2C
|
|
692
|
+
// and TCP resources. Wrap with `timeout 3` so the exec is killed
|
|
693
|
+
// after 3 s, then fall back to reading the packaged version
|
|
694
|
+
// file. Also `python3 -c 'import pypilot; print(pypilot.__version__)'`
|
|
695
|
+
// as belt-and-braces.
|
|
696
|
+
"pypilot.version": "timeout 3 python3 -c 'import pypilot; print(pypilot.__version__)' 2>&1 || (test -f /home/tc/pypilot/version && cat /home/tc/pypilot/version) || (ls -1 /usr/local/lib/python3.8/site-packages/pypilot-*.dist-info 2>&1 | head -1) || echo 'unknown'",
|
|
697
|
+
// Rev66 / 2.0.4: piCore uses runit, not systemd. `sudo sv
|
|
698
|
+
// restart /etc/sv/pypilot_web` is the correct clean-restart
|
|
699
|
+
// primitive - it TERMs the child, waits, then supervise brings
|
|
700
|
+
// it back up. Scoped to the web unit so the AP core keeps
|
|
701
|
+
// steering. Kept as an advanced preset in the Debug console
|
|
702
|
+
// (the Emergency triad simplified to 2 levels: RESTART pypilot
|
|
703
|
+
// + reboot Pi).
|
|
704
|
+
"restart.web": "sudo sv restart /etc/sv/pypilot_web 2>&1 && echo OK",
|
|
654
705
|
"reboot.pi": "sudo reboot",
|
|
655
706
|
};
|
|
656
707
|
router.post("/debug-cmd", async (req, res) => {
|
|
@@ -722,6 +773,15 @@ module.exports = function (app) {
|
|
|
722
773
|
finish({ ok: false, stdout: e?.message || String(e) });
|
|
723
774
|
}
|
|
724
775
|
});
|
|
776
|
+
// Rev66 / 2.0.4: schedule a hard reconnect after presets that
|
|
777
|
+
// disrupt pypilot_web. Level 1 (restart.web) is quick - 4 s is
|
|
778
|
+
// enough for the socket to notice + wait for pypilot_web to be
|
|
779
|
+
// back up. Level 3 (reboot.pi) needs to survive a ~40 s cold
|
|
780
|
+
// reboot; the watchdog below picks up anything longer than that.
|
|
781
|
+
if (preset === "restart.web")
|
|
782
|
+
setTimeout(doReconnect, 4000);
|
|
783
|
+
if (preset === "reboot.pi")
|
|
784
|
+
setTimeout(doReconnect, 60000);
|
|
725
785
|
// For reboot.pi the ssh session drops as the box goes down, so we
|
|
726
786
|
// report success even on a non-zero exit if we at least connected.
|
|
727
787
|
return res.json({
|
|
@@ -736,6 +796,49 @@ module.exports = function (app) {
|
|
|
736
796
|
return res.status(500).json({ error: e?.message || String(e), preset });
|
|
737
797
|
}
|
|
738
798
|
});
|
|
799
|
+
// Rev66 / 2.0.4: reconnection watchdog. If the socket has been
|
|
800
|
+
// disconnected for > 90 s (much longer than any legitimate
|
|
801
|
+
// pypilot_web restart), force a pause+resume to kick socket.io out
|
|
802
|
+
// of whatever state it got stuck in. Anti-loop: at most 3 forced
|
|
803
|
+
// reconnects in a rolling 15-min window - after that we stop
|
|
804
|
+
// trying automatically so a permanently-dead pypilot_web does
|
|
805
|
+
// not turn into a local DDoS.
|
|
806
|
+
// Rev66 / 2.0.4: watchdog tick every 60 s (was 30 s). The Pi Zero
|
|
807
|
+
// running pypilot is fragile so we minimise every recurring
|
|
808
|
+
// operation - a check that took 30 s worth of wakeups now takes 60 s.
|
|
809
|
+
// Combined with the >90 s "down" threshold, the watchdog only ever
|
|
810
|
+
// acts when the socket has been dead for a while, not on transient
|
|
811
|
+
// Tailscale hiccups.
|
|
812
|
+
let _wdogDisconnectSince = null;
|
|
813
|
+
let _wdogForcedAttempts = []; // timestamps of forced reconnects
|
|
814
|
+
const _wdogTimer = setInterval(() => {
|
|
815
|
+
try {
|
|
816
|
+
if (client?.connected) {
|
|
817
|
+
_wdogDisconnectSince = null;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (_wdogDisconnectSince == null) {
|
|
821
|
+
_wdogDisconnectSince = Date.now();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const downMs = Date.now() - _wdogDisconnectSince;
|
|
825
|
+
if (downMs < 90000)
|
|
826
|
+
return;
|
|
827
|
+
// Prune attempts older than 15 min.
|
|
828
|
+
const cutoff = Date.now() - 15 * 60000;
|
|
829
|
+
_wdogForcedAttempts = _wdogForcedAttempts.filter((t) => t > cutoff);
|
|
830
|
+
if (_wdogForcedAttempts.length >= 3)
|
|
831
|
+
return;
|
|
832
|
+
_wdogForcedAttempts.push(Date.now());
|
|
833
|
+
_wdogDisconnectSince = Date.now(); // reset the timer for the next check
|
|
834
|
+
app.debug(`[watchdog] socket down ${(downMs / 1000) | 0}s - forcing pause+resume (attempt ${_wdogForcedAttempts.length}/3 in 15 min)`);
|
|
835
|
+
doReconnect();
|
|
836
|
+
}
|
|
837
|
+
catch { /* silent */ }
|
|
838
|
+
}, 60000);
|
|
839
|
+
// Store on `app` so plugin.stop() can clear it (defensive - the
|
|
840
|
+
// registerWithRouter closure does not have a stop hook here).
|
|
841
|
+
app._pypilotNewuiWatchdogTimer = _wdogTimer;
|
|
739
842
|
},
|
|
740
843
|
};
|
|
741
844
|
// ---- helpers ----
|
package/dist/pypilot-client.js
CHANGED
|
@@ -55,13 +55,18 @@ class PypilotClient extends events_1.EventEmitter {
|
|
|
55
55
|
this.opts.log("info", `[pypilot] connecting socket.io to ${url}`);
|
|
56
56
|
this.socket = (0, socket_io_client_1.io)(url, {
|
|
57
57
|
transports: ["websocket", "polling"],
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
58
|
+
// Rev66 / 2.0.4: infinite reconnect attempts. The old 30-attempt
|
|
59
|
+
// ceiling meant a long-ish outage (say a Pi reboot that takes 45 s +
|
|
60
|
+
// a couple of socket.io backoff cycles) could exhaust the counter
|
|
61
|
+
// and leave the socket "closed forever" - the only recovery was
|
|
62
|
+
// toggling the plugin in SK Admin. With infinite retries plus the
|
|
63
|
+
// 30 s cap on backoff delay, the client keeps trying at most every
|
|
64
|
+
// 30 s until pypilot_web comes back on its own or the watchdog in
|
|
65
|
+
// index.ts triggers a hard pause+resume.
|
|
61
66
|
reconnection: true,
|
|
62
|
-
reconnectionAttempts:
|
|
67
|
+
reconnectionAttempts: Infinity,
|
|
63
68
|
reconnectionDelay: this.opts.reconnectDelayMs,
|
|
64
|
-
reconnectionDelayMax:
|
|
69
|
+
reconnectionDelayMax: 30000,
|
|
65
70
|
timeout: 10000,
|
|
66
71
|
autoConnect: true,
|
|
67
72
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-pypilot-newui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Signal K friendly UI for the pypilot autopilot, with the capability to spread its functions and buttons to other programs like KIP, WilhelmSK and freeboard. Touch-first mobile visor + full pypilot data (servo, gains, calibration, tack, warnings) as first-class Signal K paths and Simple Switches. Coexists with pypilot-autopilot-provider.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
package/public/app.js
CHANGED
|
@@ -816,9 +816,19 @@
|
|
|
816
816
|
const DASH_DATA = {
|
|
817
817
|
none: { label: "-", read: () => "" },
|
|
818
818
|
hdg: { label: "HDG", read: () => state.heading == null ? "---" : Math.round(state.heading * RAD2DEG) + "°" },
|
|
819
|
-
|
|
819
|
+
// Rev66 / 2.0.4 (Carlos): awa/twa muestran ANGULO + VELOCIDAD juntos
|
|
820
|
+
// como el `wind` combo. aws/tws solo velocidad para quien la quiera sola.
|
|
821
|
+
awa: { label: "AWA/AWS", read: () => {
|
|
822
|
+
const a = state.windAngle == null ? "---" : _sgn(state.windAngle * RAD2DEG) + "°";
|
|
823
|
+
const s = state.windSpeed == null ? "---" : (state.windSpeed * 1.94384).toFixed(1) + " kn";
|
|
824
|
+
return { value: a, sub: s };
|
|
825
|
+
} },
|
|
820
826
|
aws: { label: "AWS", read: () => state.windSpeed == null ? "---" : (state.windSpeed * 1.94384).toFixed(1) + " kn" },
|
|
821
|
-
twa: { label: "TWA", read: () =>
|
|
827
|
+
twa: { label: "TWA/TWS", read: () => {
|
|
828
|
+
const a = state.windAngleTrue == null ? "---" : _sgn(state.windAngleTrue * RAD2DEG) + "°";
|
|
829
|
+
const s = state.windSpeedTrue == null ? "---" : (state.windSpeedTrue * 1.94384).toFixed(1) + " kn";
|
|
830
|
+
return { value: a, sub: s };
|
|
831
|
+
} },
|
|
822
832
|
tws: { label: "TWS", read: () => state.windSpeedTrue == null ? "---" : (state.windSpeedTrue * 1.94384).toFixed(1) + " kn" },
|
|
823
833
|
twd: { label: "TWD", read: () => state.windDirTrue == null ? "---" : Math.round(state.windDirTrue * RAD2DEG) + "°" },
|
|
824
834
|
// Rev65 / 2.0.2: the "wind" corner now honours a user-toggleable
|
|
@@ -1256,13 +1266,53 @@
|
|
|
1256
1266
|
}
|
|
1257
1267
|
}
|
|
1258
1268
|
|
|
1269
|
+
// Rev66 / 2.0.4 - cache to avoid repainting ticks every rudder delta.
|
|
1270
|
+
let _lastRudderRange = null;
|
|
1271
|
+
function _renderRudderScale(rangeDeg) {
|
|
1272
|
+
const ticksBox = $("#rudder-ticks");
|
|
1273
|
+
if (!ticksBox) return;
|
|
1274
|
+
ticksBox.innerHTML = "";
|
|
1275
|
+
// Ticks cada 10 deg, majors + labels cada 20 deg. Include 0 in the
|
|
1276
|
+
// middle. Sign of the number tells the side.
|
|
1277
|
+
const step = 10;
|
|
1278
|
+
for (let d = -Math.round(rangeDeg); d <= Math.round(rangeDeg); d += step) {
|
|
1279
|
+
const pct = 50 + (d / rangeDeg) * 50;
|
|
1280
|
+
const tick = document.createElement("div");
|
|
1281
|
+
tick.className = (d % 20 === 0) ? "rt major" : "rt";
|
|
1282
|
+
tick.style.left = pct.toFixed(2) + "%";
|
|
1283
|
+
ticksBox.appendChild(tick);
|
|
1284
|
+
// Numeric labels every 20 deg, but skip the extremes (already shown
|
|
1285
|
+
// by the left/right end labels) and skip 0 (would clash with the
|
|
1286
|
+
// yellow nav-toggle triangle in the middle).
|
|
1287
|
+
if (d % 20 === 0 && d !== 0 && Math.abs(d) < rangeDeg - 2) {
|
|
1288
|
+
const label = document.createElement("div");
|
|
1289
|
+
label.className = "rl";
|
|
1290
|
+
label.style.left = pct.toFixed(2) + "%";
|
|
1291
|
+
label.textContent = (d > 0 ? "+" : "") + d;
|
|
1292
|
+
ticksBox.appendChild(label);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1259
1296
|
function renderRudder(rad) {
|
|
1297
|
+
// Rev66 / 2.0.4: update the rudder-range labels FIRST so they show
|
|
1298
|
+
// even before the first rudder delta arrives (no rudder yet ->
|
|
1299
|
+
// marker centred but labels visible). Range from pypilot catalog,
|
|
1300
|
+
// defaults to 70 deg.
|
|
1301
|
+
const rangeDeg = Number(state.values?.["steering.autopilot.pypilot.rudder.range"]) || 70;
|
|
1302
|
+
const lLeft = $("#rudder-label-left");
|
|
1303
|
+
const lRight = $("#rudder-label-right");
|
|
1304
|
+
if (lLeft) lLeft.textContent = "-" + Math.round(rangeDeg);
|
|
1305
|
+
if (lRight) lRight.textContent = "+" + Math.round(rangeDeg);
|
|
1306
|
+
if (rangeDeg !== _lastRudderRange) {
|
|
1307
|
+
_renderRudderScale(rangeDeg);
|
|
1308
|
+
_lastRudderRange = rangeDeg;
|
|
1309
|
+
}
|
|
1260
1310
|
// Legacy widget (still hidden by default) - kept in case someone wires it.
|
|
1261
1311
|
const w = $("#rudder-widget");
|
|
1262
1312
|
if (rad == null) {
|
|
1263
1313
|
if (w) w.setAttribute("hidden", "");
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1314
|
+
// Centre the swipe-hint rudder marker so it looks amidships when
|
|
1315
|
+
// data is missing rather than sticking to some old value.
|
|
1266
1316
|
const m = $("#rudder-marker");
|
|
1267
1317
|
if (m) m.style.left = "50%";
|
|
1268
1318
|
return;
|
|
@@ -1277,12 +1327,8 @@
|
|
|
1277
1327
|
}
|
|
1278
1328
|
const val = $("#rudder-value");
|
|
1279
1329
|
if (val) val.textContent = deg.toFixed(1) + " deg";
|
|
1280
|
-
// Rev65 / 2.0.2: paint the rudder marker on the swipe-hint bar. Range
|
|
1281
|
-
// comes from the pypilot catalog (`rudder.range` in degrees) when
|
|
1282
|
-
// available, else defaults to 45 deg either side.
|
|
1283
1330
|
const marker = $("#rudder-marker");
|
|
1284
1331
|
if (marker) {
|
|
1285
|
-
const rangeDeg = Number(state.values?.["steering.autopilot.pypilot.rudder.range"]) || 45;
|
|
1286
1332
|
const pct = Math.max(-1, Math.min(1, deg / rangeDeg));
|
|
1287
1333
|
// pct = -1 -> left edge (0%), 0 -> center (50%), +1 -> right edge (100%).
|
|
1288
1334
|
marker.style.left = (50 + pct * 50).toFixed(1) + "%";
|
|
@@ -1718,6 +1764,13 @@
|
|
|
1718
1764
|
const sign = String(state.mode || "").includes("wind") ? -1 : 1;
|
|
1719
1765
|
const newTargetRad = (state.localTargetRad ?? 0) + sign * nudge * DEG2RAD;
|
|
1720
1766
|
state.localTargetRad = newTargetRad;
|
|
1767
|
+
// Rev66 / 2.0.4: also update state.target optimistically so the
|
|
1768
|
+
// target diamond MOVES IN THE SAME TICK as the nudge, instead of
|
|
1769
|
+
// waiting for the server's delta reply (~200-800 ms on Tailscale).
|
|
1770
|
+
// If the server later rejects the new target the incoming delta
|
|
1771
|
+
// will overwrite this and the diamond re-syncs.
|
|
1772
|
+
state.target = newTargetRad;
|
|
1773
|
+
renderTargetArrow();
|
|
1721
1774
|
apSetTargetRad(newTargetRad);
|
|
1722
1775
|
});
|
|
1723
1776
|
});
|
|
@@ -1756,9 +1809,24 @@
|
|
|
1756
1809
|
// locally, and the diamond jumped to a third when the real delta
|
|
1757
1810
|
// arrived. Carlos's report: "el diamante no se fija bien".
|
|
1758
1811
|
const capturedHeading = state.heading;
|
|
1759
|
-
// Optimistic: flip the visual state
|
|
1812
|
+
// Optimistic: flip the visual state AND the target reference before
|
|
1813
|
+
// waiting on the network. Rev66 / 2.0.4: without this early assign
|
|
1814
|
+
// the target diamond was painted at the STALE state.target (last
|
|
1815
|
+
// engage's value) until the server's target-set reply arrived
|
|
1816
|
+
// hundreds of ms later, and then jumped to the fresh value. The
|
|
1817
|
+
// user saw the diamond "sale donde le sale de los huevos". Now
|
|
1818
|
+
// the diamond snaps to the captured heading in the same tick as
|
|
1819
|
+
// the click, and if the server rejects the operation we roll
|
|
1820
|
+
// both engaged and target back below.
|
|
1821
|
+
const prevTarget = state.target;
|
|
1822
|
+
const prevLocalTarget = state.localTargetRad;
|
|
1760
1823
|
state.engaged = !wasEngaged;
|
|
1824
|
+
if (!wasEngaged && capturedHeading != null) {
|
|
1825
|
+
state.target = capturedHeading;
|
|
1826
|
+
state.localTargetRad = capturedHeading;
|
|
1827
|
+
}
|
|
1761
1828
|
renderEngage();
|
|
1829
|
+
renderTargetArrow();
|
|
1762
1830
|
let ok = false;
|
|
1763
1831
|
try {
|
|
1764
1832
|
if (wasEngaged) {
|
|
@@ -1772,17 +1840,21 @@
|
|
|
1772
1840
|
capturedHeading != null ? _apRetry(() => apSetTargetRad(capturedHeading)) : Promise.resolve({ ok: true }),
|
|
1773
1841
|
]);
|
|
1774
1842
|
ok = !!(rEng && rEng.ok);
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
}
|
|
1843
|
+
// The optimistic assign above already painted the diamond in the
|
|
1844
|
+
// right spot; just re-render in case renderControl needs to
|
|
1845
|
+
// catch up other widgets that read state.target.
|
|
1846
|
+
if (rTgt && rTgt.ok && capturedHeading != null) renderControl();
|
|
1780
1847
|
}
|
|
1781
1848
|
} catch (_) { ok = false; }
|
|
1782
1849
|
if (!ok) {
|
|
1783
|
-
// Revert the optimistic flip so the UI reflects reality.
|
|
1850
|
+
// Revert the optimistic flip so the UI reflects reality. Rev66:
|
|
1851
|
+
// also revert the target snapshot we assigned above so the
|
|
1852
|
+
// diamond does not stay stuck on a target the server rejected.
|
|
1784
1853
|
state.engaged = wasEngaged;
|
|
1854
|
+
state.target = prevTarget;
|
|
1855
|
+
state.localTargetRad = prevLocalTarget;
|
|
1785
1856
|
renderEngage();
|
|
1857
|
+
renderTargetArrow();
|
|
1786
1858
|
eng.classList.add("failed");
|
|
1787
1859
|
setTimeout(() => eng.classList.remove("failed"), 1500);
|
|
1788
1860
|
}
|
|
@@ -2583,44 +2655,67 @@
|
|
|
2583
2655
|
_dashLoadCfg();
|
|
2584
2656
|
_dashPopulateSelectors();
|
|
2585
2657
|
_dashRenderCorners();
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2658
|
+
// Rev66 / 2.0.4 - long-press / short-tap handler with PER-CELL state.
|
|
2659
|
+
// The previous Rev65 version leaked shared timer / timestamp / wasLong
|
|
2660
|
+
// flags across all 4 cells, so a touchstart on one cell overwrote the
|
|
2661
|
+
// in-flight state of another. Short-tap detection race-flipped and the
|
|
2662
|
+
// AWS/TWS toggle stopped working reliably. Wrapping the state inside
|
|
2663
|
+
// each cell's forEach closure gives us clean isolation - each cell
|
|
2664
|
+
// owns its own timer and its own last-press timestamp.
|
|
2665
|
+
const attachLongTap = (el, onShortTap) => {
|
|
2666
|
+
let timer = null;
|
|
2667
|
+
let pressStartedAt = 0;
|
|
2668
|
+
let wasLong = false;
|
|
2590
2669
|
const openSelector = (e) => {
|
|
2591
2670
|
e && e.preventDefault();
|
|
2592
|
-
|
|
2671
|
+
wasLong = true;
|
|
2593
2672
|
_dashPopulateSelectors();
|
|
2594
2673
|
const pop = document.getElementById("dash-help-pop");
|
|
2595
2674
|
if (pop) pop.classList.add("open");
|
|
2596
2675
|
};
|
|
2597
2676
|
const startPress = (e) => {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
if (
|
|
2601
|
-
|
|
2677
|
+
pressStartedAt = Date.now();
|
|
2678
|
+
wasLong = false;
|
|
2679
|
+
if (timer) clearTimeout(timer);
|
|
2680
|
+
timer = setTimeout(() => openSelector(e), 500);
|
|
2602
2681
|
};
|
|
2603
2682
|
const endPress = () => {
|
|
2604
|
-
if (
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
const elapsed = Date.now() - _dashPressStartedAt;
|
|
2683
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
2684
|
+
if (wasLong) return;
|
|
2685
|
+
const elapsed = Date.now() - pressStartedAt;
|
|
2608
2686
|
if (elapsed >= 500) return;
|
|
2609
|
-
|
|
2610
|
-
|
|
2687
|
+
onShortTap();
|
|
2688
|
+
};
|
|
2689
|
+
const cancelPress = () => {
|
|
2690
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
2611
2691
|
};
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2692
|
+
el.addEventListener("touchstart", startPress, { passive: true });
|
|
2693
|
+
el.addEventListener("touchend", endPress, { passive: true });
|
|
2694
|
+
el.addEventListener("touchmove", () => { cancelPress(); wasLong = true; }, { passive: true });
|
|
2695
|
+
el.addEventListener("touchcancel", cancelPress, { passive: true });
|
|
2696
|
+
el.addEventListener("contextmenu", openSelector);
|
|
2697
|
+
el.addEventListener("mousedown", (e) => {
|
|
2618
2698
|
if (e.button !== 0) return;
|
|
2619
2699
|
startPress(e);
|
|
2620
2700
|
});
|
|
2621
|
-
|
|
2622
|
-
|
|
2701
|
+
el.addEventListener("mouseup", endPress);
|
|
2702
|
+
el.addEventListener("mouseleave", cancelPress);
|
|
2703
|
+
};
|
|
2704
|
+
// 4 corner cells: short-tap on a cell mapped to "wind" toggles AWS/TWS.
|
|
2705
|
+
$$(".dash-rose .dash-cell[data-corner]").forEach((cell) => {
|
|
2706
|
+
attachLongTap(cell, () => {
|
|
2707
|
+
if (_dashCornerCfg[cell.dataset.corner] === "wind") _toggleWindCornerShow();
|
|
2708
|
+
});
|
|
2623
2709
|
});
|
|
2710
|
+
// Rev66 / 2.0.4 - Carlos yes, the wind-speed chip below the rose
|
|
2711
|
+
// also acts as a tap target: short-tap on it toggles AWS/TWS.
|
|
2712
|
+
// Long-press behaves like long-press on any corner cell -> opens
|
|
2713
|
+
// the popup.
|
|
2714
|
+
const speedChip = document.getElementById("rose-speed-chip");
|
|
2715
|
+
if (speedChip) {
|
|
2716
|
+
speedChip.style.cursor = "pointer";
|
|
2717
|
+
attachLongTap(speedChip, () => _toggleWindCornerShow());
|
|
2718
|
+
}
|
|
2624
2719
|
// Rev43: horizontal swipe between tabs on mobile/tablet.
|
|
2625
2720
|
// Rules:
|
|
2626
2721
|
// - Swipe is DISABLED when the Ajustes sliders are unlocked. The
|
|
@@ -3113,6 +3208,7 @@
|
|
|
3113
3208
|
applyI18n();
|
|
3114
3209
|
renderEngage();
|
|
3115
3210
|
renderNudgeLabels();
|
|
3211
|
+
renderRudder(null); // Rev66: pinta los labels ±70 aunque no haya rudder aun
|
|
3116
3212
|
|
|
3117
3213
|
// Login modal wiring (Rev17)
|
|
3118
3214
|
const loginSubmit = $("#login-submit");
|
package/public/index.html
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<div class="dash-cell corner-tr" data-corner="tr"></div>
|
|
72
72
|
<div class="dash-cell corner-bl" data-corner="bl"></div>
|
|
73
73
|
<div class="dash-cell corner-br" data-corner="br"></div>
|
|
74
|
-
<svg id="wind-rose" class="rose-svg" viewBox="-100 -
|
|
74
|
+
<svg id="wind-rose" class="rose-svg" viewBox="-100 -110 200 220" preserveAspectRatio="xMidYMid meet">
|
|
75
75
|
<!-- Outer compass ring -->
|
|
76
76
|
<circle cx="0" cy="0" r="94" fill="none" stroke="#2b3542" stroke-width="2"/>
|
|
77
77
|
<!-- Rev58: sailing zones as PIE SLICES from origin (r=0) out
|
|
@@ -134,11 +134,15 @@
|
|
|
134
134
|
painted rect crosses the ring line. Number CENTERED on
|
|
135
135
|
the digits only; degree glyph placed by JS after the
|
|
136
136
|
digits so "9" and "298" both anchor at x=0. -->
|
|
137
|
+
<!-- Rev66 / 2.0.4: chip narrowed from 48 to 40 wide + font
|
|
138
|
+
shrunk from 14 to 12, so the enlarged target diamond
|
|
139
|
+
below can peek out on the LEFT and RIGHT of the chip
|
|
140
|
+
even when the target is dead ahead (rotation 0). -->
|
|
137
141
|
<g id="rose-hdg-chip">
|
|
138
|
-
<rect x="-
|
|
142
|
+
<rect x="-20" y="-100" width="40" height="16" rx="4" ry="4"
|
|
139
143
|
fill="rgba(0,0,0,0.72)" stroke="#4a5867" stroke-width="1"/>
|
|
140
|
-
<text id="rose-hdg-value" x="0" y="-
|
|
141
|
-
<text id="rose-hdg-deg" x="0" y="-
|
|
144
|
+
<text id="rose-hdg-value" x="0" y="-91" text-anchor="middle" dominant-baseline="middle" fill="#ffffff" font-size="12" font-weight="900" font-family="ui-monospace, monospace">---</text>
|
|
145
|
+
<text id="rose-hdg-deg" x="0" y="-91" text-anchor="start" dominant-baseline="middle" fill="#ffffff" font-size="9" font-weight="700">°</text>
|
|
142
146
|
</g>
|
|
143
147
|
<!-- Rev50: boat overhead SVG (mareas-ihm), scaled 0.22 so
|
|
144
148
|
the hull spans ~100 units = 50 % of the 200-unit rose
|
|
@@ -146,44 +150,54 @@
|
|
|
146
150
|
<g id="rose-boat" transform="rotate(-90) scale(0.22)">
|
|
147
151
|
<image href="boat-cenital.svg" x="-228" y="-76" width="456" height="152"/>
|
|
148
152
|
</g>
|
|
149
|
-
<!-- Rev65 / 2.0.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
<!-- Rev65/66 / 2.0.4: two independent wind arrows, both solid,
|
|
154
|
+
each labelled AWA / TWA in text that rotates with the arrow
|
|
155
|
+
so it always reads from the head. AWA = amber (big head,
|
|
156
|
+
the wind the crew feels). TWA = sea-green (small head).
|
|
157
|
+
Rev66: labels are placed at very different radii along
|
|
158
|
+
the shaft (AWA near the ring at y=-72, TWA near the boat
|
|
159
|
+
at y=-52) so they never overlap even when both arrows
|
|
160
|
+
point in exactly the same direction. -->
|
|
155
161
|
<g id="rose-wind-arrow" style="display:none">
|
|
156
162
|
<line x1="0" y1="-86" x2="0" y2="-32" stroke="#ffd54a" stroke-width="3" stroke-linecap="round"/>
|
|
157
163
|
<polygon points="0,-24 -9,-40 9,-40" fill="#ffd54a" stroke="#8a6a00" stroke-width="0.6"/>
|
|
158
|
-
<text x="0" y="-
|
|
164
|
+
<text x="0" y="-72" text-anchor="middle" dominant-baseline="middle"
|
|
159
165
|
fill="#ffd54a" stroke="#3a2600" stroke-width="0.4" paint-order="stroke fill"
|
|
160
166
|
font-size="8" font-weight="900" letter-spacing="1">AWA</text>
|
|
161
167
|
</g>
|
|
162
168
|
<g id="rose-wind-arrow-twa" style="display:none">
|
|
163
169
|
<line x1="0" y1="-86" x2="0" y2="-38" stroke="#5ce8c0" stroke-width="2.5" stroke-linecap="round"/>
|
|
164
170
|
<polygon points="0,-32 -6,-42 6,-42" fill="#5ce8c0" stroke="#1a4438" stroke-width="0.6"/>
|
|
165
|
-
<text x="0" y="-
|
|
171
|
+
<text x="0" y="-52" text-anchor="middle" dominant-baseline="middle"
|
|
166
172
|
fill="#5ce8c0" stroke="#0f2620" stroke-width="0.4" paint-order="stroke fill"
|
|
167
173
|
font-size="7" font-weight="900" letter-spacing="1">TWA</text>
|
|
168
174
|
</g>
|
|
169
|
-
<!--
|
|
170
|
-
|
|
175
|
+
<!-- Rev66 / 2.0.4: cyan diamond enlarged (28 wide, 30 tall).
|
|
176
|
+
With the narrowed HDG chip above (40 wide) the diamond
|
|
177
|
+
asoma por los lados AND por arriba AND por debajo del
|
|
178
|
+
chip even when target is dead ahead. Rotated to
|
|
171
179
|
(target - heading) so it always points where pypilot is
|
|
172
|
-
steering
|
|
180
|
+
steering (compass/gps/nav modes) or to target directly
|
|
181
|
+
(wind modes, target already in boat frame). -->
|
|
173
182
|
<g id="rose-target-arrow" style="display:none">
|
|
174
|
-
<polygon points="0,-
|
|
183
|
+
<polygon points="0,-108 -14,-93 0,-78 14,-93" fill="#4dd4ff" stroke="#003b52" stroke-width="1.5"/>
|
|
184
|
+
</g>
|
|
185
|
+
<!-- Rev66 / 2.0.4: wind speed now inside a small dark chip
|
|
186
|
+
that straddles the bottom of the compass ring, same
|
|
187
|
+
look as the HDG chip on top. Top of the chip sits a
|
|
188
|
+
couple of units INSIDE the ring, bottom asoma below.
|
|
189
|
+
Contains a small dim AWS/TWS label above the number.
|
|
190
|
+
The whole group carries `id=rose-speed-chip` so the
|
|
191
|
+
short-tap toggle (fix #5) can attach a listener to
|
|
192
|
+
the chip itself, not just the corner cells. -->
|
|
193
|
+
<g id="rose-speed-chip">
|
|
194
|
+
<rect x="-32" y="90" width="64" height="22" rx="4" ry="4"
|
|
195
|
+
fill="rgba(0,0,0,0.72)" stroke="#4a5867" stroke-width="1"/>
|
|
196
|
+
<text id="rose-speed-label" x="0" y="96" text-anchor="middle" dominant-baseline="middle"
|
|
197
|
+
fill="#8b98a5" font-size="7" font-weight="700" letter-spacing="1.2"></text>
|
|
198
|
+
<text id="rose-speed" x="0" y="106" text-anchor="middle" dominant-baseline="middle"
|
|
199
|
+
fill="#ffffff" font-size="12" font-weight="900" font-family="ui-monospace, monospace">--- kn</text>
|
|
175
200
|
</g>
|
|
176
|
-
<!-- Rev65 / 2.0.2: wind speed split into two stacked lines
|
|
177
|
-
- a small dim label (AWS/TWS/both) and a large white
|
|
178
|
-
number - separated by empty space so the S cardinal and
|
|
179
|
-
the surrounding rose stay visible in between. The whole
|
|
180
|
-
stack sits below the compass ring: label just outside at
|
|
181
|
-
y=+100, number below at y=+108, both fitting inside the
|
|
182
|
-
expanded viewBox (extra 10 units at the bottom). -->
|
|
183
|
-
<text id="rose-speed-label" x="0" y="98" text-anchor="middle" dominant-baseline="middle"
|
|
184
|
-
fill="#8b98a5" font-size="8" font-weight="700" letter-spacing="1.5"></text>
|
|
185
|
-
<text id="rose-speed" x="0" y="108" text-anchor="middle" dominant-baseline="middle"
|
|
186
|
-
fill="#ffffff" font-size="15" font-weight="900" font-family="ui-monospace, monospace">--- kn</text>
|
|
187
201
|
</svg>
|
|
188
202
|
</div>
|
|
189
203
|
</div>
|
|
@@ -288,6 +302,13 @@
|
|
|
288
302
|
"zero" reference of the rudder bar. -->
|
|
289
303
|
<div class="swipe-hint" id="swipe-hint" aria-hidden="true">
|
|
290
304
|
<div class="rudder-track"></div>
|
|
305
|
+
<!-- Rev66 / 2.0.4: rudder scale ticks + labels populated by
|
|
306
|
+
renderRudder() based on the actual pypilot rudder.range
|
|
307
|
+
(default 70 deg). Ticks every 10 deg, numeric labels
|
|
308
|
+
every 20 deg (0, 20, 40, 60) plus explicit ends. -->
|
|
309
|
+
<div class="rudder-ticks" id="rudder-ticks"></div>
|
|
310
|
+
<span class="rudder-label rudder-label-left" id="rudder-label-left">-70</span>
|
|
311
|
+
<span class="rudder-label rudder-label-right" id="rudder-label-right">+70</span>
|
|
291
312
|
<div class="rudder-marker" id="rudder-marker" style="left:50%"></div>
|
|
292
313
|
<span class="swipe-hint-arrow" data-swipe-dir="prev">‹</span>
|
|
293
314
|
<span class="swipe-hint-arrow" data-swipe-dir="prev">‹</span>
|
|
@@ -508,23 +529,20 @@
|
|
|
508
529
|
</div>
|
|
509
530
|
<div id="cfg-ssh-status" class="hint" style="margin-top:6px"></div>
|
|
510
531
|
|
|
511
|
-
<!--
|
|
512
|
-
|
|
532
|
+
<!-- Rev66 / 2.0.4: simplified to 2 levels. `restart pypilot_web`
|
|
533
|
+
(level 1) rarely helps in practice - when the Pi Zero is
|
|
534
|
+
saturated the whole box needs a full reboot. Moved to the
|
|
535
|
+
Debug console below as an advanced preset for the 5 % of
|
|
536
|
+
cases where only the web layer is stuck but the AP core
|
|
537
|
+
keeps steering (rare). -->
|
|
513
538
|
<div class="restart-block" style="margin-top:14px">
|
|
514
|
-
<div class="restart-title" data-i18n="setup.restart.title">Restart / reboot
|
|
515
|
-
<div class="restart-row">
|
|
516
|
-
<button id="btn-restart-web" class="restart-btn level-1" data-preset="restart.web" data-outage-i18n="setup.restart.outage.web">
|
|
517
|
-
<span class="rb-num">1</span>
|
|
518
|
-
<span class="rb-title" data-i18n="setup.restart.web.title">restart pypilot_web</span>
|
|
519
|
-
<span class="rb-sub" data-i18n="setup.restart.web.sub">~3-5 s · AP keeps steering</span>
|
|
520
|
-
</button>
|
|
539
|
+
<div class="restart-title" data-i18n="setup.restart.title">Restart / reboot</div>
|
|
540
|
+
<div class="restart-row two-cols">
|
|
521
541
|
<button id="cli-restart-pypilot" class="restart-btn level-2" data-outage-i18n="setup.restart.outage.pypilot">
|
|
522
|
-
<span class="rb-num">2</span>
|
|
523
542
|
<span class="rb-title" data-i18n="setup.restart.pypilot.title">RESTART pypilot</span>
|
|
524
543
|
<span class="rb-sub" data-i18n="setup.restart.pypilot.sub">~10-15 s · AP drops the heading</span>
|
|
525
544
|
</button>
|
|
526
545
|
<button id="btn-reboot-pi" class="restart-btn level-3" data-preset="reboot.pi" data-outage-i18n="setup.restart.outage.reboot">
|
|
527
|
-
<span class="rb-num">3</span>
|
|
528
546
|
<span class="rb-title" data-i18n="setup.restart.reboot.title">reboot Pi</span>
|
|
529
547
|
<span class="rb-sub" data-i18n="setup.restart.reboot.sub">~35-60 s · full outage</span>
|
|
530
548
|
</button>
|
|
@@ -532,7 +550,7 @@
|
|
|
532
550
|
</div>
|
|
533
551
|
|
|
534
552
|
<div id="cli-restart-status" class="hint" style="margin-top:8px"></div>
|
|
535
|
-
<div class="hint" data-i18n="setup.restart.hint">
|
|
553
|
+
<div class="hint" data-i18n="setup.restart.hint">Both actions use the SSH user + password above and show a helm-manned confirmation before executing. Use RESTART pypilot for the common case; reboot Pi when the TinyPilot has degraded and needs a cold reset. For a lighter web-only restart see the Debug console below.</div>
|
|
536
554
|
<div class="hint" data-i18n="setup.security.hint" style="margin-top:6px">Signal K stores plugin config in plain text at ~/.signalk/plugin-config-data/.</div>
|
|
537
555
|
|
|
538
556
|
<!-- Rev63 / 2.0.0: Debug console. Closed whitelist of preset
|
|
@@ -548,6 +566,10 @@
|
|
|
548
566
|
<button class="ghost debug-preset" data-preset="uptime">uptime</button>
|
|
549
567
|
<button class="ghost debug-preset" data-preset="df">df</button>
|
|
550
568
|
<button class="ghost debug-preset" data-preset="pypilot.version">pypilot --version</button>
|
|
569
|
+
<!-- Rev66 / 2.0.4: moved from the Emergency triad. Rarely
|
|
570
|
+
needed on a healthy Pi; when the box is saturated a full
|
|
571
|
+
reboot is what actually helps. Kept here for edge cases. -->
|
|
572
|
+
<button class="ghost debug-preset" data-preset="restart.web">restart pypilot_web</button>
|
|
551
573
|
</div>
|
|
552
574
|
<textarea id="debug-output" readonly rows="12"
|
|
553
575
|
style="width:100%;background:var(--bg-3);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:8px;font-family:ui-monospace,monospace;font-size:12px;resize:vertical;"
|
|
@@ -918,6 +940,6 @@
|
|
|
918
940
|
</div>
|
|
919
941
|
|
|
920
942
|
<!-- Cache-busting: bump the ?v= on every Rev to force Firefox to fetch fresh app.js -->
|
|
921
|
-
<script src="app.js?v=
|
|
943
|
+
<script src="app.js?v=Rev67"></script>
|
|
922
944
|
</body>
|
|
923
945
|
</html>
|
package/public/style.css
CHANGED
|
@@ -904,48 +904,71 @@ body.nav-hidden .nav-toggle .tri { transform: rotate(180deg); }
|
|
|
904
904
|
text-transform: uppercase;
|
|
905
905
|
margin: 0 0 8px;
|
|
906
906
|
}
|
|
907
|
+
/* Rev66 / 2.0.4: default 3 cols for legacy 3-level layouts; the
|
|
908
|
+
Emergency simplified layout in Rev66 uses .two-cols to switch to
|
|
909
|
+
2 cols on desktop. Both collapse to 1 col on narrow phones. */
|
|
907
910
|
.restart-row {
|
|
908
911
|
display: grid;
|
|
909
912
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
910
913
|
gap: 8px;
|
|
911
914
|
}
|
|
912
|
-
|
|
913
|
-
|
|
915
|
+
.restart-row.two-cols {
|
|
916
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
914
917
|
}
|
|
918
|
+
@media (max-width: 640px) {
|
|
919
|
+
.restart-row,
|
|
920
|
+
.restart-row.two-cols { grid-template-columns: 1fr; }
|
|
921
|
+
}
|
|
922
|
+
/* Rev66 / 2.0.4: simple flex column layout - the numbered badge is
|
|
923
|
+
gone in the 2-level Emergency, and the previous grid template still
|
|
924
|
+
assumed a 28 px badge column, which stretched the title into two
|
|
925
|
+
awkward lines with the sub label overlapping. Flex column stacks
|
|
926
|
+
title on top of sub cleanly regardless of button width. */
|
|
915
927
|
.restart-btn {
|
|
916
|
-
display:
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
gap:
|
|
921
|
-
padding:
|
|
928
|
+
display: flex;
|
|
929
|
+
flex-direction: column;
|
|
930
|
+
align-items: center; /* Rev66 fix: text centered per Carlos */
|
|
931
|
+
justify-content: center;
|
|
932
|
+
gap: 4px;
|
|
933
|
+
padding: 14px 16px;
|
|
934
|
+
min-height: 68px;
|
|
922
935
|
border-radius: 10px;
|
|
923
936
|
border: 2px solid var(--border);
|
|
924
937
|
background: var(--bg-3);
|
|
925
938
|
color: var(--fg);
|
|
926
939
|
cursor: pointer;
|
|
927
|
-
text-align:
|
|
940
|
+
text-align: center; /* Rev66 fix: text centered */
|
|
928
941
|
font-family: inherit;
|
|
929
942
|
transition: transform 80ms ease, background 100ms ease, border-color 100ms ease;
|
|
930
943
|
}
|
|
931
944
|
.restart-btn:hover { border-color: var(--fg-dim); }
|
|
932
945
|
.restart-btn:active { transform: scale(0.97); }
|
|
946
|
+
.restart-btn .rb-title {
|
|
947
|
+
font-weight: 800;
|
|
948
|
+
font-size: 15px;
|
|
949
|
+
letter-spacing: 0.5px;
|
|
950
|
+
line-height: 1.2;
|
|
951
|
+
}
|
|
952
|
+
.restart-btn .rb-sub {
|
|
953
|
+
font-size: 12px;
|
|
954
|
+
color: var(--fg-dim);
|
|
955
|
+
line-height: 1.2;
|
|
956
|
+
}
|
|
957
|
+
/* Optional numbered badge - only rendered by markup that still uses
|
|
958
|
+
the old 3-level layout. When present it sits inline before the
|
|
959
|
+
title on the first row via nested flex, without breaking the plain
|
|
960
|
+
layout when absent. */
|
|
933
961
|
.restart-btn .rb-num {
|
|
934
|
-
|
|
935
|
-
width: 28px; height: 28px;
|
|
962
|
+
width: 24px; height: 24px;
|
|
936
963
|
border-radius: 50%;
|
|
937
|
-
display: flex; align-items: center; justify-content: center;
|
|
938
|
-
font-weight: 900; font-size:
|
|
964
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
965
|
+
font-weight: 900; font-size: 13px;
|
|
939
966
|
background: rgba(255, 255, 255, 0.08);
|
|
940
967
|
color: var(--fg);
|
|
968
|
+
margin-right: 6px;
|
|
969
|
+
vertical-align: middle;
|
|
941
970
|
}
|
|
942
|
-
|
|
943
|
-
.restart-btn .rb-sub { font-size: 12px; color: var(--fg-dim); }
|
|
944
|
-
/* Level 1: green tint, safest - AP keeps steering. */
|
|
945
|
-
.restart-btn.level-1 { border-color: rgba(46, 204, 113, 0.55); }
|
|
946
|
-
.restart-btn.level-1 .rb-num { background: rgba(46, 204, 113, 0.22); color: #2ecc71; }
|
|
947
|
-
.restart-btn.level-1:hover { background: rgba(46, 204, 113, 0.10); }
|
|
948
|
-
/* Level 2: amber, brief AP outage. Matches the old RESTART pypilot look. */
|
|
971
|
+
/* Level 2: amber, brief AP outage. */
|
|
949
972
|
.restart-btn.level-2 { border-color: var(--warn, #f37812); }
|
|
950
973
|
.restart-btn.level-2 .rb-num { background: rgba(243, 120, 18, 0.25); color: var(--warn, #f37812); }
|
|
951
974
|
.restart-btn.level-2:hover { background: rgba(243, 120, 18, 0.10); }
|
|
@@ -953,6 +976,10 @@ body.nav-hidden .nav-toggle .tri { transform: rotate(180deg); }
|
|
|
953
976
|
.restart-btn.level-3 { border-color: var(--err, #e74c3c); }
|
|
954
977
|
.restart-btn.level-3 .rb-num { background: rgba(231, 76, 60, 0.30); color: var(--err, #e74c3c); }
|
|
955
978
|
.restart-btn.level-3:hover { background: rgba(231, 76, 60, 0.10); }
|
|
979
|
+
/* Level 1: kept for backwards-compat if some markup uses it. */
|
|
980
|
+
.restart-btn.level-1 { border-color: rgba(46, 204, 113, 0.55); }
|
|
981
|
+
.restart-btn.level-1 .rb-num { background: rgba(46, 204, 113, 0.22); color: #2ecc71; }
|
|
982
|
+
.restart-btn.level-1:hover { background: rgba(46, 204, 113, 0.10); }
|
|
956
983
|
|
|
957
984
|
/* Rev63 - Helm-manned confirmation modal. Same overlay pattern as
|
|
958
985
|
#dash-help-pop but with a danger accent on the confirm button and a
|
|
@@ -1596,6 +1623,55 @@ main { touch-action: pan-y; overflow-x: hidden; }
|
|
|
1596
1623
|
transition: left 180ms ease-out;
|
|
1597
1624
|
pointer-events: none;
|
|
1598
1625
|
}
|
|
1626
|
+
/* Rev66 / 2.0.4 - rudder scale labels at both ends of the swipe-hint
|
|
1627
|
+
bar. Small dim numbers indicating the rudder range in degrees. Text
|
|
1628
|
+
populated by renderRudder() with the current pypilot rudder.range
|
|
1629
|
+
(default 70 deg). Pointer-events off so they don't block taps. */
|
|
1630
|
+
.swipe-hint .rudder-label {
|
|
1631
|
+
position: absolute;
|
|
1632
|
+
top: 50%;
|
|
1633
|
+
transform: translateY(-50%);
|
|
1634
|
+
font-size: 10px;
|
|
1635
|
+
font-weight: 700;
|
|
1636
|
+
color: rgba(255, 255, 255, 0.55);
|
|
1637
|
+
z-index: 1;
|
|
1638
|
+
pointer-events: none;
|
|
1639
|
+
padding: 0 6px;
|
|
1640
|
+
font-family: ui-monospace, monospace;
|
|
1641
|
+
letter-spacing: 0.5px;
|
|
1642
|
+
}
|
|
1643
|
+
.swipe-hint .rudder-label-left { left: 6px; }
|
|
1644
|
+
.swipe-hint .rudder-label-right { right: 6px; }
|
|
1645
|
+
/* Rev66 / 2.0.4 - intermediate tick container. renderRudder() fills
|
|
1646
|
+
this with small marks every 10 deg + numeric labels every 20 deg. */
|
|
1647
|
+
.swipe-hint .rudder-ticks {
|
|
1648
|
+
position: absolute;
|
|
1649
|
+
inset: 4px 8px;
|
|
1650
|
+
z-index: 0;
|
|
1651
|
+
pointer-events: none;
|
|
1652
|
+
}
|
|
1653
|
+
.swipe-hint .rudder-ticks .rt {
|
|
1654
|
+
position: absolute;
|
|
1655
|
+
top: 0;
|
|
1656
|
+
bottom: 0;
|
|
1657
|
+
width: 1px;
|
|
1658
|
+
margin-left: -0.5px;
|
|
1659
|
+
background: rgba(255, 255, 255, 0.25);
|
|
1660
|
+
}
|
|
1661
|
+
.swipe-hint .rudder-ticks .rt.major {
|
|
1662
|
+
width: 1px;
|
|
1663
|
+
background: rgba(255, 255, 255, 0.45);
|
|
1664
|
+
}
|
|
1665
|
+
.swipe-hint .rudder-ticks .rl {
|
|
1666
|
+
position: absolute;
|
|
1667
|
+
top: 50%;
|
|
1668
|
+
transform: translate(-50%, -50%);
|
|
1669
|
+
font-size: 9px;
|
|
1670
|
+
font-weight: 700;
|
|
1671
|
+
color: rgba(255, 255, 255, 0.55);
|
|
1672
|
+
font-family: ui-monospace, monospace;
|
|
1673
|
+
pointer-events: none;
|
|
1674
|
+
}
|
|
1599
1675
|
.swipe-hint-arrow,
|
|
1600
1676
|
.swipe-hint .nav-toggle {
|
|
1601
1677
|
position: relative; /* stack above the rudder track */
|