signalk-pypilot-newui 2.4.0 → 2.4.1
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 +42 -0
- package/dist/index.js +35 -5
- package/dist/sensor-quality.js +73 -4
- package/package.json +1 -1
- package/public/app.js +98 -20
- package/public/index.html +12 -3
- package/public/style.css +87 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.1 — 2026-09-08 — Rev146..Rev153
|
|
4
|
+
|
|
5
|
+
Patch release. Fixes + UI polish on top of 2.4.0.
|
|
6
|
+
|
|
7
|
+
### English
|
|
8
|
+
|
|
9
|
+
**Fixed**
|
|
10
|
+
|
|
11
|
+
- **TWS row in Sensor Quality no longer stuck as MISSING** on plain SK
|
|
12
|
+
setups. `environment.wind.speedTrue` (canonical) is rarely populated;
|
|
13
|
+
the canonical row now accepts `.speedOverGround` (what
|
|
14
|
+
`signalk-derived-data` emits) or `.speedOverWater` as alternatives.
|
|
15
|
+
Same treatment for TWA (`.angleTrueWater` / `.angleTrueGround` /
|
|
16
|
+
`.angleTrue`). Rev146-148.
|
|
17
|
+
- **Regression: AP mode selector went blank** after the Rev140 watch
|
|
18
|
+
refactor - `RESERVED_PYPILOT_KEYS` was blocking subscribe too, when
|
|
19
|
+
it should only block re-publish. Restored so the visor keeps
|
|
20
|
+
`ap.enabled/mode/heading_command` in sync. Rev142.
|
|
21
|
+
- **Watch focus was pinning 60+ keys** on `pypilot_web` when the Setup
|
|
22
|
+
launcher was open. Rev149-150: the launcher grid takes NO focus;
|
|
23
|
+
only the Calibration card (fullscreen) bumps the 7 documented
|
|
24
|
+
sliders. Empty focus now triggers explicit release, so the count
|
|
25
|
+
drops back to the ~15 core watches immediately instead of waiting
|
|
26
|
+
for the 60 s TTL. Well under Sean D'Epagnier's 40-path ceiling.
|
|
27
|
+
- **`[current]` prefix in Pi Zero log capture** replaced with the
|
|
28
|
+
parent directory (`[pypilot]`, `[pypilot_web]`, `[pypilot_hat]`) so
|
|
29
|
+
the merged file is actually readable. Rev149.
|
|
30
|
+
|
|
31
|
+
**Changed**
|
|
32
|
+
|
|
33
|
+
- **Rose colour palette (Carlos)**: Target/DIA diamond is now red
|
|
34
|
+
(`--target-red`, matches the AP engage button); AWS/TWS chip below
|
|
35
|
+
the rose is fully tinted amber for apparent, teal for true; corner
|
|
36
|
+
tiles tint value + sub in the matching wind colour; SOG back to
|
|
37
|
+
white; Rev151-152.
|
|
38
|
+
- **Configure-corners modal** — h4 + close button now sticky at the
|
|
39
|
+
top, body scrolls independently. X on the LEFT with the solid red
|
|
40
|
+
`--engaged` style used by the Setup fullscreen cards. Rev151-153.
|
|
41
|
+
- **Mode selector labels re-render on language change**: switching
|
|
42
|
+
language now redraws mode / pilot / profile selects immediately
|
|
43
|
+
instead of waiting for the next backend delta. Rev144.
|
|
44
|
+
|
|
3
45
|
## 2.4.0 — 2026-09-07 — Rev138..Rev145
|
|
4
46
|
|
|
5
47
|
Feature release. The plugin has learnt to behave itself around the
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const doctor_1 = require("./doctor");
|
|
|
14
14
|
const session_recorder_1 = require("./session-recorder");
|
|
15
15
|
// Rev counter bumped on every build so the user can distinguish deploys
|
|
16
16
|
// from the webapp header (feedback_revision_bump_each_build).
|
|
17
|
-
const PLUGIN_REVISION = "
|
|
17
|
+
const PLUGIN_REVISION = "Rev153";
|
|
18
18
|
// Rev59: read package.json once at load time so /status can report the
|
|
19
19
|
// npm package version alongside the internal Rev counter.
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -1314,8 +1314,16 @@ module.exports = function (app) {
|
|
|
1314
1314
|
const fresh = lines.slice(startIdx);
|
|
1315
1315
|
if (fresh.length === 0)
|
|
1316
1316
|
continue;
|
|
1317
|
+
// Rev149 (Carlos): tag each line with the parent directory
|
|
1318
|
+
// instead of the file basename. All three logs are named
|
|
1319
|
+
// "current" (svlogd convention) so the previous prefix
|
|
1320
|
+
// showed [current] for everything - useless when reviewing
|
|
1321
|
+
// the aggregated file. Now: [pypilot], [pypilot_web],
|
|
1322
|
+
// [pypilot_hat].
|
|
1323
|
+
const parts = src.split("/");
|
|
1324
|
+
const tag = parts.length >= 2 ? parts[parts.length - 2] : (parts[0] || "log");
|
|
1317
1325
|
for (const l of fresh)
|
|
1318
|
-
newLines.push(`[${
|
|
1326
|
+
newLines.push(`[${tag}] ${l}`);
|
|
1319
1327
|
logCaptureLastLineHash[src] = _lcHash(lines[lines.length - 1]);
|
|
1320
1328
|
}
|
|
1321
1329
|
if (newLines.length > 0) {
|
|
@@ -2015,7 +2023,11 @@ module.exports = function (app) {
|
|
|
2015
2023
|
engaged: engagedNow,
|
|
2016
2024
|
mode: s.mode,
|
|
2017
2025
|
tws: s.tws,
|
|
2018
|
-
|
|
2026
|
+
// Rev146: TWA comes from angleTrueWater in most SK setups
|
|
2027
|
+
// (derived-data emits that, not the legacy angleTrue).
|
|
2028
|
+
twa: (typeof (app.getSelfPath ? app.getSelfPath("environment.wind.angleTrueWater.value") : null) === "number")
|
|
2029
|
+
? app.getSelfPath("environment.wind.angleTrueWater.value")
|
|
2030
|
+
: null,
|
|
2019
2031
|
aws: s.aws,
|
|
2020
2032
|
awa: s.awa,
|
|
2021
2033
|
sog: s.sog,
|
|
@@ -2086,7 +2098,11 @@ module.exports = function (app) {
|
|
|
2086
2098
|
servoVoltage: servoV,
|
|
2087
2099
|
awa: skNum("environment.wind.angleApparent"),
|
|
2088
2100
|
aws: skNum("environment.wind.speedApparent"),
|
|
2089
|
-
|
|
2101
|
+
// Rev146 (Carlos): signalk-derived-data does not emit
|
|
2102
|
+
// environment.wind.speedTrue - it publishes speedOverGround
|
|
2103
|
+
// instead. Try the canonical path first (some setups still
|
|
2104
|
+
// populate it via NMEA MWD) and fall back to the derived one.
|
|
2105
|
+
tws: skNum("environment.wind.speedTrue") ?? skNum("environment.wind.speedOverGround"),
|
|
2090
2106
|
sog: skNum("navigation.speedOverGround"),
|
|
2091
2107
|
heel: skAttitudeField("roll"),
|
|
2092
2108
|
engaged,
|
|
@@ -2200,13 +2216,27 @@ module.exports = function (app) {
|
|
|
2200
2216
|
if (!sensorQuality)
|
|
2201
2217
|
return;
|
|
2202
2218
|
const now = Date.now();
|
|
2203
|
-
|
|
2219
|
+
// Rev148 (Carlos): feed the primary path AND every alternative the
|
|
2220
|
+
// threshold advertises so the monitor can pick the freshest source
|
|
2221
|
+
// for that metric (TWS, TWA, ...) instead of failing on the
|
|
2222
|
+
// canonical path that most SK setups don't publish.
|
|
2223
|
+
for (const [path, thr] of Object.entries(sensor_quality_1.DEFAULT_QUALITY_WATCH)) {
|
|
2204
2224
|
try {
|
|
2205
2225
|
const entry = app.getSelfPath(path);
|
|
2206
2226
|
if (entry)
|
|
2207
2227
|
sensorQuality.observe(path, entry, now);
|
|
2208
2228
|
}
|
|
2209
2229
|
catch { /* silent - a missing path stays "missing" naturally */ }
|
|
2230
|
+
if (thr.alternatives) {
|
|
2231
|
+
for (const alt of thr.alternatives) {
|
|
2232
|
+
try {
|
|
2233
|
+
const entry = app.getSelfPath(alt);
|
|
2234
|
+
if (entry)
|
|
2235
|
+
sensorQuality.observe(alt, entry, now);
|
|
2236
|
+
}
|
|
2237
|
+
catch { /* silent */ }
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2210
2240
|
}
|
|
2211
2241
|
}
|
|
2212
2242
|
// Rev95: publish the current KPI snapshot as Signal K paths under
|
package/dist/sensor-quality.js
CHANGED
|
@@ -37,7 +37,28 @@ exports.DEFAULT_QUALITY_WATCH = {
|
|
|
37
37
|
"navigation.position": { label: "GPS position", degradedAgeMs: 5000, lostAgeMs: 30000 },
|
|
38
38
|
"environment.wind.angleApparent": { label: "AWA", degradedAgeMs: 5000, lostAgeMs: 30000 },
|
|
39
39
|
"environment.wind.speedApparent": { label: "AWS", degradedAgeMs: 5000, lostAgeMs: 30000 },
|
|
40
|
-
|
|
40
|
+
// Rev148 (Carlos): TWS row is fed by whichever true-wind speed
|
|
41
|
+
// path the SK setup publishes. speedTrue is the canonical spec
|
|
42
|
+
// path but almost nobody publishes it; signalk-derived-data emits
|
|
43
|
+
// speedOverGround; a few instruments publish speedOverWater. If
|
|
44
|
+
// ANY of the three is fresh, the row turns green.
|
|
45
|
+
"environment.wind.speedOverGround": {
|
|
46
|
+
label: "TWS", degradedAgeMs: 5000, lostAgeMs: 30000,
|
|
47
|
+
alternatives: [
|
|
48
|
+
"environment.wind.speedTrue",
|
|
49
|
+
"environment.wind.speedOverWater",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
// TWA row: same idea. Different derived-data / instrument setups
|
|
53
|
+
// emit angleTrueWater, angleTrueGround, or the deprecated
|
|
54
|
+
// angleTrue - accept any of them.
|
|
55
|
+
"environment.wind.angleTrueWater": {
|
|
56
|
+
label: "TWA", degradedAgeMs: 5000, lostAgeMs: 30000,
|
|
57
|
+
alternatives: [
|
|
58
|
+
"environment.wind.angleTrueGround",
|
|
59
|
+
"environment.wind.angleTrue",
|
|
60
|
+
],
|
|
61
|
+
},
|
|
41
62
|
"steering.rudderAngle": { label: "Rudder", degradedAgeMs: 3000, lostAgeMs: 20000 },
|
|
42
63
|
};
|
|
43
64
|
class SensorQualityMonitor {
|
|
@@ -56,6 +77,26 @@ class SensorQualityMonitor {
|
|
|
56
77
|
lastValue: null,
|
|
57
78
|
lastSource: null,
|
|
58
79
|
});
|
|
80
|
+
// Rev148 (Carlos): register a hidden slot for every alternative
|
|
81
|
+
// path in the group so freshness/hz/jitter are all measured, but
|
|
82
|
+
// tag the threshold with the same label as the primary slot -
|
|
83
|
+
// the snapshot dedupes by label and keeps only the freshest.
|
|
84
|
+
if (thr.alternatives) {
|
|
85
|
+
for (const alt of thr.alternatives) {
|
|
86
|
+
if (this.slots.has(alt))
|
|
87
|
+
continue;
|
|
88
|
+
this.slots.set(alt, {
|
|
89
|
+
path: alt,
|
|
90
|
+
threshold: { ...thr, alternatives: undefined }, // avoid recursion
|
|
91
|
+
arrivals: new Array(this.bufferPerPath).fill(0),
|
|
92
|
+
head: 0,
|
|
93
|
+
count: 0,
|
|
94
|
+
lastSkTs: null,
|
|
95
|
+
lastValue: null,
|
|
96
|
+
lastSource: null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
59
100
|
}
|
|
60
101
|
}
|
|
61
102
|
/** Feed one observation into the monitor. Called from the plugin's
|
|
@@ -97,14 +138,29 @@ class SensorQualityMonitor {
|
|
|
97
138
|
slot.count += 1;
|
|
98
139
|
}
|
|
99
140
|
snapshot(nowMs = Date.now()) {
|
|
100
|
-
|
|
141
|
+
// Rev148 (Carlos): classify every slot, then dedupe by label -
|
|
142
|
+
// paths that share a label (alternatives) collapse into a single
|
|
143
|
+
// row keeping the freshest (or the greenest) as the canonical.
|
|
144
|
+
// If no alternative has ever emitted, the row stays "missing".
|
|
145
|
+
const classified = [];
|
|
101
146
|
for (const slot of this.slots.values()) {
|
|
102
|
-
|
|
147
|
+
classified.push(this.classify(slot, nowMs));
|
|
148
|
+
}
|
|
149
|
+
const bestByLabel = new Map();
|
|
150
|
+
for (const it of classified) {
|
|
151
|
+
const key = it.label || it.path;
|
|
152
|
+
const prev = bestByLabel.get(key);
|
|
153
|
+
if (!prev) {
|
|
154
|
+
bestByLabel.set(key, it);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (isBetter(it, prev))
|
|
158
|
+
bestByLabel.set(key, it);
|
|
103
159
|
}
|
|
104
160
|
return {
|
|
105
161
|
computedTs: nowMs,
|
|
106
162
|
windowSec: this.windowMs / 1000,
|
|
107
|
-
items,
|
|
163
|
+
items: [...bestByLabel.values()],
|
|
108
164
|
};
|
|
109
165
|
}
|
|
110
166
|
classify(slot, nowMs) {
|
|
@@ -172,6 +228,19 @@ class SensorQualityMonitor {
|
|
|
172
228
|
}
|
|
173
229
|
}
|
|
174
230
|
exports.SensorQualityMonitor = SensorQualityMonitor;
|
|
231
|
+
/** Rev148: rank two PathQuality reports so the dedupe by-label picks
|
|
232
|
+
* the "greenest and freshest". Ordering: good beats degraded beats
|
|
233
|
+
* lost beats missing; within the same level, the smaller ageMs wins. */
|
|
234
|
+
function isBetter(a, b) {
|
|
235
|
+
const rank = (l) => (l === "good" ? 3 : l === "degraded" ? 2 : l === "lost" ? 1 : 0);
|
|
236
|
+
const ra = rank(a.level);
|
|
237
|
+
const rb = rank(b.level);
|
|
238
|
+
if (ra !== rb)
|
|
239
|
+
return ra > rb;
|
|
240
|
+
const aa = a.ageMs ?? Infinity;
|
|
241
|
+
const bb = b.ageMs ?? Infinity;
|
|
242
|
+
return aa < bb;
|
|
243
|
+
}
|
|
175
244
|
function normaliseTs(raw) {
|
|
176
245
|
if (raw == null)
|
|
177
246
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-pypilot-newui",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
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
|
@@ -1554,6 +1554,10 @@
|
|
|
1554
1554
|
"environment.wind.angleTrueWater",
|
|
1555
1555
|
"environment.wind.angleTrueGround",
|
|
1556
1556
|
"environment.wind.speedTrue",
|
|
1557
|
+
// Rev146 (Carlos): signalk-derived-data emits speedOverGround
|
|
1558
|
+
// in place of speedTrue on most SK setups - subscribe both so
|
|
1559
|
+
// the corner tiles and Aproado modal get a real value.
|
|
1560
|
+
"environment.wind.speedOverGround",
|
|
1557
1561
|
"environment.wind.directionTrue",
|
|
1558
1562
|
"environment.depth.belowKeel",
|
|
1559
1563
|
"environment.depth.belowTransducer",
|
|
@@ -1712,6 +1716,12 @@
|
|
|
1712
1716
|
state.windAngleTrue = numericOrNull(value); break;
|
|
1713
1717
|
case "environment.wind.speedTrue":
|
|
1714
1718
|
state.windSpeedTrue = numericOrNull(value); break;
|
|
1719
|
+
case "environment.wind.speedOverGround":
|
|
1720
|
+
// Rev146: use as TWS fallback only when the canonical
|
|
1721
|
+
// speedTrue path is missing (some SK setups do publish
|
|
1722
|
+
// speedTrue via NMEA MWD - respect that value first).
|
|
1723
|
+
if (state.windSpeedTrue == null) state.windSpeedTrue = numericOrNull(value);
|
|
1724
|
+
break;
|
|
1715
1725
|
case "navigation.speedOverGround":
|
|
1716
1726
|
state.sog = numericOrNull(value); renderCogAndCurrent(); break;
|
|
1717
1727
|
case "navigation.courseOverGroundTrue":
|
|
@@ -1952,19 +1962,47 @@
|
|
|
1952
1962
|
if (!meta) continue;
|
|
1953
1963
|
const label = typeof meta.label === "function" ? meta.label() : meta.label;
|
|
1954
1964
|
const val = meta.read();
|
|
1965
|
+
// Rev151 (Carlos): colour A/T inside the corner-tile label so
|
|
1966
|
+
// "AWA/AWS" gets an amber A and "TWA/TWS" a teal T. The rest of
|
|
1967
|
+
// the label stays neutral so the tile does not look shouty.
|
|
1968
|
+
const paintedLabel = _paintABLabel(label, key);
|
|
1955
1969
|
// Wind returns {value, sub}; the rest returns a string.
|
|
1956
1970
|
if (val && typeof val === "object" && "value" in val) {
|
|
1957
|
-
el.innerHTML = `<div class="label">${
|
|
1971
|
+
el.innerHTML = `<div class="label">${paintedLabel}</div><div class="value">${val.value}</div><div class="sub">${val.sub}</div>`;
|
|
1958
1972
|
} else {
|
|
1959
|
-
el.innerHTML = `<div class="label">${
|
|
1973
|
+
el.innerHTML = `<div class="label">${paintedLabel}</div><div class="value">${val}</div>`;
|
|
1960
1974
|
}
|
|
1961
1975
|
// Rev48: color coding by data type - keep classic B&G-ish palette.
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1976
|
+
// Rev151: tgt goes red (matches the AP button); awa/aws lean amber;
|
|
1977
|
+
// twa/tws lean teal; wind combo picks amber/teal via the paint below.
|
|
1978
|
+
el.classList.remove("heading","target","wind","sog","depth","cog","mode","twa","twd","wind-app","wind-true");
|
|
1979
|
+
const colorMap = { hdg:"heading", tgt:"target",
|
|
1980
|
+
wind: state.windCornerShow === "twa" ? "wind-true" : "wind-app",
|
|
1981
|
+
awa:"wind-app", aws:"wind-app",
|
|
1982
|
+
twa:"wind-true", tws:"wind-true", twd:"twd",
|
|
1983
|
+
sog:"sog", cog:"cog", depth:"depth", mode:"mode" };
|
|
1965
1984
|
if (colorMap[key]) el.classList.add(colorMap[key]);
|
|
1966
1985
|
}
|
|
1967
1986
|
}
|
|
1987
|
+
// Rev151: wrap the leading A / T of a corner label into a coloured
|
|
1988
|
+
// span so the eye maps the tile to the matching arrow instantly.
|
|
1989
|
+
function _paintABLabel(label, key) {
|
|
1990
|
+
if (typeof label !== "string" || label.length === 0) return label;
|
|
1991
|
+
// Cases we know: "AWA/AWS", "AWS", "AWA", "TWA/TWS", "TWS", "TWA".
|
|
1992
|
+
// For the toggling `wind` corner, check state.windCornerShow.
|
|
1993
|
+
const isApp = key === "awa" || key === "aws" || (key === "wind" && state.windCornerShow !== "twa");
|
|
1994
|
+
const isTru = key === "twa" || key === "tws" || (key === "wind" && state.windCornerShow === "twa");
|
|
1995
|
+
if (!isApp && !isTru) return label;
|
|
1996
|
+
const cls = isApp ? "lbl-app" : "lbl-true";
|
|
1997
|
+
// Split around the '/' so "AWA/AWS" -> both sides paint their leader.
|
|
1998
|
+
return label.split("/").map((part) => {
|
|
1999
|
+
const trimmed = part.trim();
|
|
2000
|
+
if (!trimmed) return part;
|
|
2001
|
+
const head = trimmed[0];
|
|
2002
|
+
const tail = trimmed.slice(1);
|
|
2003
|
+
return `<span class="${cls}"><b>${head}</b>${tail}</span>`;
|
|
2004
|
+
}).join("/");
|
|
2005
|
+
}
|
|
1968
2006
|
function _dashPopulateSelectors() {
|
|
1969
2007
|
// Rev58: the corner dropdown lists CORE keys + user-enabled extras +
|
|
1970
2008
|
// any custom paths the user has added. Everything else stays hidden
|
|
@@ -2270,12 +2308,20 @@
|
|
|
2270
2308
|
}
|
|
2271
2309
|
const speedLabel = document.getElementById("rose-speed-label");
|
|
2272
2310
|
const speedText = document.getElementById("rose-speed");
|
|
2311
|
+
const chip = document.getElementById("rose-speed-chip");
|
|
2273
2312
|
if (speedLabel) speedLabel.textContent = windSpeed == null ? "" : tag;
|
|
2274
2313
|
if (speedText) {
|
|
2275
2314
|
speedText.textContent = windSpeed == null
|
|
2276
2315
|
? "--- kn"
|
|
2277
2316
|
: (windSpeed * 1.94384).toFixed(1) + " kn";
|
|
2278
2317
|
}
|
|
2318
|
+
// Rev152 (Carlos): tint the WHOLE chip (label + value) with the
|
|
2319
|
+
// matching arrow colour - AWS gets amber, TWS gets teal. Toggling
|
|
2320
|
+
// a class on the chip lets CSS colour both children at once.
|
|
2321
|
+
if (chip) {
|
|
2322
|
+
chip.classList.remove("wind-app", "wind-true");
|
|
2323
|
+
if (windSpeed != null) chip.classList.add(tag === "TWS" ? "wind-true" : "wind-app");
|
|
2324
|
+
}
|
|
2279
2325
|
}
|
|
2280
2326
|
|
|
2281
2327
|
// Rev57: target indicator - a small cyan diamond OUTSIDE the compass
|
|
@@ -5198,31 +5244,54 @@
|
|
|
5198
5244
|
return keys;
|
|
5199
5245
|
}
|
|
5200
5246
|
function _setupFocusKeys() {
|
|
5201
|
-
//
|
|
5202
|
-
//
|
|
5203
|
-
//
|
|
5204
|
-
|
|
5247
|
+
// Rev149 (Carlos, per Sean D'Epagnier): the launcher grid alone
|
|
5248
|
+
// (no card fullscreen) needs NO focus - the tile chips read
|
|
5249
|
+
// aggregate state from the SK server, not raw pypilot values.
|
|
5250
|
+
// Only bump keys when a card that actually shows raw values is
|
|
5251
|
+
// opened fullscreen, and only the keys that card renders. This
|
|
5252
|
+
// keeps the watch count well under Sean's 40-path ceiling.
|
|
5253
|
+
const fs = document.querySelector('.setup-block[data-fullscreen="1"]');
|
|
5254
|
+
if (!fs) return [];
|
|
5255
|
+
const blockKey = fs.dataset.blockKey || "";
|
|
5205
5256
|
const cat = state.catalog || {};
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5257
|
+
const keys = [];
|
|
5258
|
+
if (blockKey === "calibration") {
|
|
5259
|
+
// Only the RangeSettings we ship help text for + the alignment
|
|
5260
|
+
// metadata the calibration card actually renders.
|
|
5261
|
+
const DOCUMENTED = [
|
|
5262
|
+
"servo.max_current", "servo.max_slew_speed", "servo.min_speed",
|
|
5263
|
+
"rudder.range", "imu.heading_offset",
|
|
5264
|
+
"ap.tack.angle", "ap.tack.rate",
|
|
5265
|
+
];
|
|
5266
|
+
for (const k of DOCUMENTED) if (cat[k]) keys.push(k);
|
|
5212
5267
|
}
|
|
5268
|
+
// Doctor / Sensor Quality / Paths / Language / Emergency / Connection
|
|
5269
|
+
// do not depend on any pypilot value the core set doesn't already
|
|
5270
|
+
// subscribe to. Adding them here would just re-inflate the count.
|
|
5213
5271
|
return keys;
|
|
5214
5272
|
}
|
|
5215
5273
|
function _watchFocusForTab(id) {
|
|
5216
5274
|
_focusCurrentTab = id;
|
|
5217
5275
|
if (_focusRenewTimer) { clearInterval(_focusRenewTimer); _focusRenewTimer = null; }
|
|
5218
5276
|
const kick = () => {
|
|
5277
|
+
let keys = [];
|
|
5278
|
+
let period = 1.0;
|
|
5219
5279
|
if (_focusCurrentTab === "tune") {
|
|
5220
|
-
|
|
5280
|
+
keys = _tuneFocusKeys();
|
|
5281
|
+
period = 0.5;
|
|
5221
5282
|
} else if (_focusCurrentTab === "setup") {
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5283
|
+
keys = _setupFocusKeys();
|
|
5284
|
+
period = 1.0;
|
|
5285
|
+
}
|
|
5286
|
+
// Rev149 (Carlos, per Sean D'Epagnier): if this tab / card
|
|
5287
|
+
// combination needs no focus, actively release whatever the
|
|
5288
|
+
// previous state had rather than letting the 60 s TTL run down.
|
|
5289
|
+
// Otherwise Setup would keep 60+ keys pinned for a full minute
|
|
5290
|
+
// after the user closed the Calibration card.
|
|
5291
|
+
if (keys.length === 0) {
|
|
5292
|
+
_watchReleaseAll();
|
|
5293
|
+
} else {
|
|
5294
|
+
_watchFocusPost(keys, period, 60);
|
|
5226
5295
|
}
|
|
5227
5296
|
};
|
|
5228
5297
|
kick();
|
|
@@ -7659,6 +7728,10 @@
|
|
|
7659
7728
|
block.dataset.fullscreen = "1";
|
|
7660
7729
|
block.scrollTop = 0;
|
|
7661
7730
|
document.body.classList.add("setup-fullscreen-active");
|
|
7731
|
+
// Rev149 (Carlos, per Sean): re-evaluate focus now that the
|
|
7732
|
+
// fullscreen block is known, so the relevant card's paths get
|
|
7733
|
+
// bumped immediately instead of waiting for the 45 s heartbeat.
|
|
7734
|
+
if (typeof _watchFocusForTab === "function") _watchFocusForTab("setup");
|
|
7662
7735
|
}
|
|
7663
7736
|
function _setupFullscreenOff(block) {
|
|
7664
7737
|
if (!block) {
|
|
@@ -7679,6 +7752,11 @@
|
|
|
7679
7752
|
if (!document.querySelector('.setup-block[data-fullscreen="1"]')) {
|
|
7680
7753
|
document.body.classList.remove("setup-fullscreen-active");
|
|
7681
7754
|
}
|
|
7755
|
+
// Rev149: re-evaluate focus. With no card open, _watchFocusForTab
|
|
7756
|
+
// sees an empty key list and calls _watchReleaseAll itself, so
|
|
7757
|
+
// the previously bumped subscriptions drop back to the core set
|
|
7758
|
+
// immediately rather than after the 60 s TTL.
|
|
7759
|
+
if (typeof _watchFocusForTab === "function") _watchFocusForTab("setup");
|
|
7682
7760
|
}
|
|
7683
7761
|
function initSetupTiles() {
|
|
7684
7762
|
const setup = document.getElementById("tab-setup");
|
package/public/index.html
CHANGED
|
@@ -181,8 +181,12 @@
|
|
|
181
181
|
sean afiladas (sin curvatura extra que crea bump
|
|
182
182
|
asimétrico al renderizar en algunos ángulos de
|
|
183
183
|
rotación). Path desde la punta (Rev80) mantenido. -->
|
|
184
|
+
<!-- Rev151 (Carlos): Target/DIA en rojo AP (var(--target-red))
|
|
185
|
+
para que el hito visual coincida con el color del boton
|
|
186
|
+
engage AP. Anteriormente cian; el cian queda libre para
|
|
187
|
+
otros overlays neutros. -->
|
|
184
188
|
<path d="M 0,-68 L -21.15,-91.59 A 94 94 0 0 1 21.15,-91.59 Z"
|
|
185
|
-
fill="#
|
|
189
|
+
fill="#ff3b30" stroke="#a82420" stroke-width="0.75"
|
|
186
190
|
stroke-linejoin="miter" stroke-miterlimit="10"/>
|
|
187
191
|
</g>
|
|
188
192
|
<!-- Raya: marca radial negra dibujada DESPUÉS de las 3
|
|
@@ -391,9 +395,13 @@
|
|
|
391
395
|
any corner cell. 4 dropdowns, one per corner. Choice persists
|
|
392
396
|
in localStorage so each user gets their own layout. -->
|
|
393
397
|
<div class="popup-overlay" id="dash-help-pop" onclick="if(event.target.id==='dash-help-pop')this.classList.remove('open')">
|
|
398
|
+
<!-- Rev151 (Carlos): header (h4 + X) fijo, body scrolleable
|
|
399
|
+
en su propio contenedor. Estilos alineados con las cards
|
|
400
|
+
fullscreen de Setup (X rojo, header con fondo bg-3). -->
|
|
394
401
|
<div class="popup-box" onclick="event.stopPropagation()">
|
|
395
|
-
<button class="popup-close" onclick="document.getElementById('dash-help-pop').classList.remove('open')">✕</button>
|
|
396
402
|
<h4 data-i18n="dash.help.title">Configure the rose corners</h4>
|
|
403
|
+
<button class="popup-close" onclick="document.getElementById('dash-help-pop').classList.remove('open')">✕</button>
|
|
404
|
+
<div class="popup-body">
|
|
397
405
|
<p class="dash-help-body" data-i18n="dash.help.body">Pick which datum shows in each corner. Saved in your browser.</p>
|
|
398
406
|
<div class="dash-corner-cfg">
|
|
399
407
|
<label><span data-i18n="dash.corner.tl">Top left</span> <select id="dash-corner-tl" data-corner="tl"></select></label>
|
|
@@ -492,6 +500,7 @@
|
|
|
492
500
|
<datalist id="dash-custom-path-list"></datalist>
|
|
493
501
|
<div id="dash-custom-list"></div>
|
|
494
502
|
</div>
|
|
503
|
+
</div><!-- /.popup-body Rev151 -->
|
|
495
504
|
</div>
|
|
496
505
|
</div>
|
|
497
506
|
|
|
@@ -1513,6 +1522,6 @@
|
|
|
1513
1522
|
</div>
|
|
1514
1523
|
|
|
1515
1524
|
<!-- Cache-busting: bump the ?v= on every Rev to force Firefox to fetch fresh app.js -->
|
|
1516
|
-
<script src="app.js?v=
|
|
1525
|
+
<script src="app.js?v=Rev153"></script>
|
|
1517
1526
|
</body>
|
|
1518
1527
|
</html>
|
package/public/style.css
CHANGED
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
--border: #2b3542;
|
|
17
17
|
--radius: 14px;
|
|
18
18
|
--tap: 56px;
|
|
19
|
+
/* Rev151 (Carlos): rose data-tag palette - the corner tiles, chip
|
|
20
|
+
labels and Target diamond now inherit these tokens so the visual
|
|
21
|
+
link with each arrow is unambiguous. Apparent = amber, True =
|
|
22
|
+
teal, Target = the same red as the AP engaged button. */
|
|
23
|
+
--wind-app: #ffd54a;
|
|
24
|
+
--wind-app-dim: #cba822;
|
|
25
|
+
--wind-true: #5ce8c0;
|
|
26
|
+
--wind-true-dim: #38b596;
|
|
27
|
+
--target-red: #ff3b30;
|
|
28
|
+
--target-red-dim:#a82420;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
* { box-sizing: border-box; }
|
|
@@ -394,6 +404,28 @@ main {
|
|
|
394
404
|
white-space: nowrap;
|
|
395
405
|
}
|
|
396
406
|
.dash-rose .wind-chip:active { transform: translateX(-50%) scale(0.96); }
|
|
407
|
+
|
|
408
|
+
/* Rev151/152 (Carlos): the WHOLE AWS/TWS chip below the rose plus the
|
|
409
|
+
corner tiles gets tinted with the matching arrow colour - amber for
|
|
410
|
+
apparent, teal for true. In the tile label we still highlight the
|
|
411
|
+
leading A / T with a bold span (helps parse "AWA/AWS" in a glance)
|
|
412
|
+
but the rest of the text now shares the tint so the chip reads as
|
|
413
|
+
one unit. */
|
|
414
|
+
.dash-rose .wind-chip.wind-app .label,
|
|
415
|
+
.dash-rose .wind-chip.wind-app .value { color: var(--wind-app); }
|
|
416
|
+
.dash-rose .wind-chip.wind-true .label,
|
|
417
|
+
.dash-rose .wind-chip.wind-true .value { color: var(--wind-true); }
|
|
418
|
+
.lbl-app { color: var(--wind-app); }
|
|
419
|
+
.lbl-true { color: var(--wind-true); }
|
|
420
|
+
.lbl-app b, .lbl-true b { font-weight: 900; }
|
|
421
|
+
/* Corner tiles inherit the palette via body classes so the whole tile
|
|
422
|
+
gets a subtle tint, not just the label. */
|
|
423
|
+
.dash-rose .dash-cell.wind-app .value,
|
|
424
|
+
.dash-rose .dash-cell.wind-app .sub { color: var(--wind-app); }
|
|
425
|
+
.dash-rose .dash-cell.wind-true .value,
|
|
426
|
+
.dash-rose .dash-cell.wind-true .sub { color: var(--wind-true); }
|
|
427
|
+
.dash-rose .dash-cell.target .value,
|
|
428
|
+
.dash-rose .dash-cell.target .sub { color: var(--target-red); }
|
|
397
429
|
.dash-rose .dash-cell {
|
|
398
430
|
display: flex;
|
|
399
431
|
flex-direction: column;
|
|
@@ -483,20 +515,59 @@ main {
|
|
|
483
515
|
font-size: 22px;
|
|
484
516
|
color: var(--accent, #37b6ff);
|
|
485
517
|
}
|
|
518
|
+
/* Rev151 (Carlos): popups carrying a .popup-body get a sticky header
|
|
519
|
+
layout - the h4 + close button stay pinned while the body scrolls.
|
|
520
|
+
Legacy popups (no .popup-body) keep the previous block layout. */
|
|
521
|
+
.popup-box:has(> .popup-body) {
|
|
522
|
+
padding: 0;
|
|
523
|
+
max-height: 85vh;
|
|
524
|
+
display: grid;
|
|
525
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
526
|
+
overflow: hidden;
|
|
527
|
+
}
|
|
528
|
+
.popup-box:has(> .popup-body) > h4 {
|
|
529
|
+
/* Rev152 (Carlos): X now on the LEFT so reserve padding on that
|
|
530
|
+
side and let the right edge breathe. */
|
|
531
|
+
margin: 0;
|
|
532
|
+
padding: 14px 22px 14px 60px;
|
|
533
|
+
font-size: 20px;
|
|
534
|
+
font-weight: 800;
|
|
535
|
+
background: var(--bg-3, #1c2632);
|
|
536
|
+
border-bottom: 1px solid var(--border, #2b3542);
|
|
537
|
+
letter-spacing: 0.3px;
|
|
538
|
+
}
|
|
539
|
+
/* Rev151: dedicated scrolling wrapper for popup body. Everything
|
|
540
|
+
after the sticky h4 lives inside .popup-body. */
|
|
541
|
+
.popup-box > .popup-body {
|
|
542
|
+
overflow-y: auto;
|
|
543
|
+
padding: 18px 22px 22px;
|
|
544
|
+
-webkit-overflow-scrolling: touch;
|
|
545
|
+
}
|
|
486
546
|
.popup-close {
|
|
547
|
+
/* Rev151/152/153 (Carlos): X on the LEFT of the header, styled EXACTLY
|
|
548
|
+
like the Setup fullscreen close button - solid red background with
|
|
549
|
+
white glyph, no border. Same visual language across the whole app. */
|
|
487
550
|
position: absolute;
|
|
488
|
-
top:
|
|
489
|
-
|
|
490
|
-
background:
|
|
491
|
-
border:
|
|
492
|
-
color: #
|
|
493
|
-
width:
|
|
494
|
-
height:
|
|
495
|
-
border-radius:
|
|
496
|
-
font-size:
|
|
551
|
+
top: 8px;
|
|
552
|
+
left: 10px;
|
|
553
|
+
background: var(--engaged, #ff3b30);
|
|
554
|
+
border: none;
|
|
555
|
+
color: #fff;
|
|
556
|
+
width: 40px;
|
|
557
|
+
height: 40px;
|
|
558
|
+
border-radius: 10px;
|
|
559
|
+
font-size: 20px;
|
|
560
|
+
font-weight: 900;
|
|
561
|
+
line-height: 1;
|
|
497
562
|
cursor: pointer;
|
|
563
|
+
display: flex;
|
|
564
|
+
align-items: center;
|
|
565
|
+
justify-content: center;
|
|
566
|
+
padding: 0;
|
|
567
|
+
z-index: 1;
|
|
498
568
|
}
|
|
499
|
-
.popup-close:hover
|
|
569
|
+
.popup-close:hover { filter: brightness(1.1); }
|
|
570
|
+
.popup-close:active { transform: scale(0.94); }
|
|
500
571
|
|
|
501
572
|
/* Rev47: long-press dash-help popup content. */
|
|
502
573
|
.dash-help-body {
|
|
@@ -704,13 +775,16 @@ body.nav-hidden .nav-toggle .tri { transform: rotate(180deg); }
|
|
|
704
775
|
margin-top: 4px;
|
|
705
776
|
}
|
|
706
777
|
|
|
707
|
-
/* B&G-ish tile color coding
|
|
708
|
-
|
|
778
|
+
/* B&G-ish tile color coding.
|
|
779
|
+
Rev151/152 (Carlos): SOG back to white (was yellow), wind now split
|
|
780
|
+
into apparent (amber) / true (teal). The wind-app / wind-true rules
|
|
781
|
+
further down in this file override the legacy green .wind rules. */
|
|
709
782
|
.dash-cell.heading .value { color: #ffffff; }
|
|
710
783
|
.dash-cell.target .value { color: #37b6ff; }
|
|
711
784
|
.dash-cell.wind .value { color: #4dd982; }
|
|
712
785
|
.dash-cell.wind .sub { color: #4dd982; }
|
|
713
|
-
.dash-cell.sog .value { color: #
|
|
786
|
+
.dash-cell.sog .value { color: #ffffff; }
|
|
787
|
+
.dash-cell.sog .sub { color: #ffffff; }
|
|
714
788
|
|
|
715
789
|
.controls { display: flex; flex-direction: column; gap: 10px; }
|
|
716
790
|
|