signalk-pypilot-newui 2.4.1 → 2.5.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 +118 -0
- package/README.md +10 -0
- package/dist/alarms.js +65 -0
- package/dist/constants.js +17 -0
- package/dist/doctor.js +54 -10
- package/dist/index.js +517 -1
- package/dist/kpis.js +2 -1
- package/dist/servo-health.js +25 -1
- package/dist/session-recorder.js +57 -0
- package/package.json +1 -1
- package/public/app.js +953 -25
- package/public/index.html +223 -29
- package/public/style.css +63 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0 — 2026-09-09 — Rev154..Rev174
|
|
4
|
+
|
|
5
|
+
> ## ⚠️ Version con funciones aun sin probar en el mar
|
|
6
|
+
>
|
|
7
|
+
> Esta version trae funciones NUEVAS que **no hemos podido validar
|
|
8
|
+
> navegando** todavia (el dia de sacarla no habia viento).
|
|
9
|
+
> Instalala si te apetece ayudarnos a probarlas — y si algo va raro,
|
|
10
|
+
> **avisa**: https://github.com/Aitonos/signalk-pypilot-newui/issues
|
|
11
|
+
>
|
|
12
|
+
> Tu feedback nos vale ORO para tunear los limites antes de dejar
|
|
13
|
+
> estas funciones a punto para todos.
|
|
14
|
+
>
|
|
15
|
+
> ### ⛵ Que tenemos que probar en el mar (checklist rapida)
|
|
16
|
+
>
|
|
17
|
+
> 1. **Contador de virada** (grande sobre el barco): al pulsar TACK
|
|
18
|
+
> con el AP enganchado, cuenta atras rojo (5-4-3-2-1) y luego pasa
|
|
19
|
+
> a naranja "VIRANDO" mientras el pypilot vira. Tocar el circulo
|
|
20
|
+
> cancela.
|
|
21
|
+
> 2. **Resumen despues de virar** (panel que sale 2 min): compara la
|
|
22
|
+
> velocidad y el angulo antes y despues del bordo. Verde = buena.
|
|
23
|
+
> Amarillo = normal. Rojo = perdida.
|
|
24
|
+
> 3. **Historial de bordos** (deja pulsado el barco -> "Historial"):
|
|
25
|
+
> guarda cada bordo con retencion de velocidad y tiempo de
|
|
26
|
+
> recuperacion. Filtro por dias, resalta el mejor.
|
|
27
|
+
> 4. **Marca de rafagas** (activable en "Cambiar esquinas" ->
|
|
28
|
+
> Overlays): cuando entra una rafaga fuerte, deja una flecha
|
|
29
|
+
> amarilla fantasma con la velocidad del viento durante 10 segundos.
|
|
30
|
+
> 5. **Cerebro auto** (opcional, en la ficha Smart Pilot): si lo
|
|
31
|
+
> activas, cambia el perfil del pypilot segun el viento (suave /
|
|
32
|
+
> normal / duro) y reacciona a rafagas segun la estrategia elegida
|
|
33
|
+
> (avisar / congelar rumbo / subir D / cambiar a perfil duro).
|
|
34
|
+
> 6. **Corta automatica** (opcional): si el timon toca los topes
|
|
35
|
+
> demasiado tiempo, suelta el AP para que retomes tu.
|
|
36
|
+
>
|
|
37
|
+
> **Como nos ayudas**: usalas, y si algo se comporta raro o no te
|
|
38
|
+
> gusta, mandanos: que hiciste, que esperabas, que paso. Con eso
|
|
39
|
+
> tunearemos los limites antes de sacar la 2.5.0 estable.
|
|
40
|
+
|
|
41
|
+
### English (Rev154..Rev174, pending sea-trial confirmation)
|
|
42
|
+
|
|
43
|
+
**Added — supervisor layer ("brain over pypilot")**
|
|
44
|
+
|
|
45
|
+
- **Auto-profile by wind** — the plugin can switch the active pypilot
|
|
46
|
+
profile between light / medium / heavy based on rolling TWS. Bands
|
|
47
|
+
configurable in the visor's Smart Pilot card. Disabled by default.
|
|
48
|
+
Rev164.
|
|
49
|
+
- **Gust strategy** — configurable response when a remarkable AWS jump
|
|
50
|
+
is detected: `off`, `warn`, `freeze-target`, `boost-D`, `temp-heavy`.
|
|
51
|
+
Replaces the Rev165 warn-only detector. Restore-first logic returns
|
|
52
|
+
the pilot to the prior state when the gust window closes. Rev167.
|
|
53
|
+
- **Auto-disengage on lost authority** — if the servo saturates against
|
|
54
|
+
its rudder stops for a sustained window, the AP releases so a manned
|
|
55
|
+
helm regains control. Rev166.
|
|
56
|
+
- **Config surface moved to the visor** — the Smart Pilot card in the
|
|
57
|
+
plugin's own UI now owns every supervisor toggle. Nothing to touch in
|
|
58
|
+
SK Admin. Rev167.
|
|
59
|
+
|
|
60
|
+
**Added — rose UI**
|
|
61
|
+
|
|
62
|
+
- **Tack countdown overlay over the boat sprite** — huge red digit
|
|
63
|
+
during the pre-tack delay, then flips to orange `VIRANDO` + elapsed
|
|
64
|
+
seconds during execution. Click cancels the maneuver. Fresh delay is
|
|
65
|
+
fetched every arm so an edit in Calibration takes effect without a
|
|
66
|
+
page reload. Rev171/172.
|
|
67
|
+
- **Post-tack stats HUD** — for 2 minutes after a completed tack the
|
|
68
|
+
boat sprite glows and a small panel shows TWS / |AWA| / SOG averaged
|
|
69
|
+
30 s before vs. 30 s after (measured t+30..t+60 so the boat has time
|
|
70
|
+
to settle). Verdict is colour-coded by SOG retention. Rev171.
|
|
71
|
+
- **Persistent tack log (long-press on the boat)** — every completed
|
|
72
|
+
tack is stored locally with pre/post averages, SOG retention %, and
|
|
73
|
+
time to recover 95 % of pre-SOG. The action sheet on the boat sprite
|
|
74
|
+
now offers "Cambiar esquinas" (the old corner selector) or
|
|
75
|
+
"Historial de bordos". The log modal filters by day range and
|
|
76
|
+
highlights the best tack by retention and the fastest to recover.
|
|
77
|
+
Rev172.
|
|
78
|
+
- **Gust ghost arrow (opt-in in the corner-selector modal)** — a
|
|
79
|
+
remarkable AWS jump (≥ 6 kn over the 30-s baseline or ≥ 1.55×) or a
|
|
80
|
+
sharp AWA rotation (≥ 30° in 5 s) leaves a translucent A-shaped
|
|
81
|
+
arrow frozen at the gust angle, with the AWS reading printed
|
|
82
|
+
radially outside it, fading to zero over 10 s. Cooldown 30 s and
|
|
83
|
+
single-ghost policy so it never chases the live arrow. Card lives
|
|
84
|
+
alongside the A / T / COG / Current overlay cards. Rev172-174.
|
|
85
|
+
- **Mode selector reads full phrases** — `APARENTE` / `REAL` shown on
|
|
86
|
+
the collapsed button, `VIENTO APARENTE` / `VIENTO REAL` on the open
|
|
87
|
+
dropdown (swapped on pointerdown / blur). Rev172.
|
|
88
|
+
- **T over A on overlap** — when both wind pieces cover the same angle
|
|
89
|
+
the teal T now draws on top of the amber A, so TWA stays readable at
|
|
90
|
+
a glance. Rev172.
|
|
91
|
+
- **Rose corner sub-line resized (Carlos)** — the wind speed under the
|
|
92
|
+
angle in the top corners is now a first-class figure in portrait and
|
|
93
|
+
a compact one in landscape (media-query split). Bottom AWS/TWS chip
|
|
94
|
+
also trimmed so it stops covering half the boat on a portrait phone.
|
|
95
|
+
Rev171-173.
|
|
96
|
+
- **Tack countdown click-to-cancel** — tapping the huge red circle at
|
|
97
|
+
any moment (delay or VIRANDO) sends `ap.tack.state=none`. Guard added
|
|
98
|
+
so TACK does nothing if the AP is not engaged. Rev171.
|
|
99
|
+
|
|
100
|
+
**Changed — internal**
|
|
101
|
+
|
|
102
|
+
- Shared `SERVO_ON_MIN_A` constant across doctor / kpis / servo-health.
|
|
103
|
+
Rev154.
|
|
104
|
+
- Alarm rules extended with `servo-motor-temp`, `servo-load-high`
|
|
105
|
+
(deviation-ratio > 1.65), and `cruise-drift`. Rev155.
|
|
106
|
+
- `doctor.applyAll()` applies only the top-priority suggestion and
|
|
107
|
+
returns `mustRunFreshSession` + `remaining` so the caller can drive a
|
|
108
|
+
next-iteration loop. Rev160.
|
|
109
|
+
- Session recorder JSONL now enforces a 200 MB FIFO cap. Rev158.
|
|
110
|
+
- Servo-health snapshot persists to disk between restarts. Rev158.
|
|
111
|
+
- Hardware-ceiling finding in `doctor` (duty>0.75 & rms>5°) short-
|
|
112
|
+
circuits further advice with a "you have out-tuned the actuator"
|
|
113
|
+
message. Rev163.
|
|
114
|
+
|
|
115
|
+
**Removed**
|
|
116
|
+
|
|
117
|
+
- All "helm is manned" confirmation modals on engage / tack / Doctor
|
|
118
|
+
apply. Confirming every action was noise; the physical presence of
|
|
119
|
+
the helm is a given while at sea. Rev170.
|
|
120
|
+
|
|
3
121
|
## 2.4.1 — 2026-09-08 — Rev146..Rev153
|
|
4
122
|
|
|
5
123
|
Patch release. Fixes + UI polish on top of 2.4.0.
|
package/README.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# signalk-pypilot-newui
|
|
2
2
|
|
|
3
|
+
> ### ⚠️ 2.5.0 trae funciones que aun no hemos probado en el mar
|
|
4
|
+
>
|
|
5
|
+
> El dia de sacarla no habia viento suficiente para validar el
|
|
6
|
+
> contador de virada, el resumen post-virada, el historial de bordos,
|
|
7
|
+
> la marca de rafagas ni el "cerebro auto" que gestiona perfiles y
|
|
8
|
+
> rafagas por ti. **Nos vale ORO tu feedback:** instalala, usala y si
|
|
9
|
+
> algo va raro cuentanoslo en
|
|
10
|
+
> https://github.com/Aitonos/signalk-pypilot-newui/issues
|
|
11
|
+
> Con eso tuneamos los limites antes de dar todo por bueno.
|
|
12
|
+
|
|
3
13
|
**PyPilot New-UI + SK Paths** — a modern, touch-first control panel for the
|
|
4
14
|
open-source [pypilot](https://github.com/pypilot/pypilot) autopilot, plus every
|
|
5
15
|
pypilot value exposed as a first-class Signal K path so tools like
|
package/dist/alarms.js
CHANGED
|
@@ -50,6 +50,30 @@ exports.DEFAULT_RULES = [
|
|
|
50
50
|
return `AP heading error ${(r * 180 / Math.PI).toFixed(1)}° sustained`;
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
// Rev160 (Carlos): cruise-drift catches the slow bleed the RMS
|
|
55
|
+
// rule cannot see - a boat that consistently sits 6-8° off the
|
|
56
|
+
// target because of an unbalanced sail plan or current. Uses
|
|
57
|
+
// meanErrorRad instead of RMS. Threshold 5° | 120 s so trimming
|
|
58
|
+
// manoeuvres (short excursions) don't trip it.
|
|
59
|
+
id: "cruise-drift",
|
|
60
|
+
label: "Sustained heading drift",
|
|
61
|
+
severity: "warn",
|
|
62
|
+
defaultEnabled: true,
|
|
63
|
+
sustainSec: 120,
|
|
64
|
+
description: "Signed mean heading error > 5° for over 2 min - possible sail imbalance, current, or misaligned target.",
|
|
65
|
+
evaluate: (c) => {
|
|
66
|
+
if (!c.kpis || !c.sample || !c.sample.engaged)
|
|
67
|
+
return false;
|
|
68
|
+
const mean = c.kpis.window1m.meanErrorRad;
|
|
69
|
+
return typeof mean === "number" && Math.abs(mean) > (5 * Math.PI / 180);
|
|
70
|
+
},
|
|
71
|
+
message: (c) => {
|
|
72
|
+
const m = (c.kpis?.window1m.meanErrorRad ?? 0) * 180 / Math.PI;
|
|
73
|
+
const side = m > 0 ? "starboard" : "port";
|
|
74
|
+
return `Boat drifting ${Math.abs(m).toFixed(1)}° to ${side} of target`;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
53
77
|
{
|
|
54
78
|
id: "unable-to-steer",
|
|
55
79
|
label: "Unable to steer",
|
|
@@ -99,6 +123,47 @@ exports.DEFAULT_RULES = [
|
|
|
99
123
|
},
|
|
100
124
|
message: (c) => `Servo temp ${(c.sample?.servoTemp ?? 0).toFixed(0)} °C`,
|
|
101
125
|
},
|
|
126
|
+
{
|
|
127
|
+
// Rev156 (Carlos): coil temperature complements servo-temp-high,
|
|
128
|
+
// which watches the controller. In practice the coil heats first
|
|
129
|
+
// under prolonged full-power operation. Threshold 70 C conservative
|
|
130
|
+
// (most pypilot motors alarm at 80-85).
|
|
131
|
+
id: "servo-motor-temp",
|
|
132
|
+
label: "Servo motor temperature high",
|
|
133
|
+
severity: "warn",
|
|
134
|
+
defaultEnabled: true,
|
|
135
|
+
sustainSec: 15,
|
|
136
|
+
description: "Motor coil temperature above 70 C.",
|
|
137
|
+
evaluate: (c) => {
|
|
138
|
+
if (!c.sample || typeof c.sample.servoMotorTemp !== "number")
|
|
139
|
+
return false;
|
|
140
|
+
return c.sample.servoMotorTemp > 70;
|
|
141
|
+
},
|
|
142
|
+
message: (c) => `Servo motor temp ${(c.sample?.servoMotorTemp ?? 0).toFixed(0)} °C`,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
// Rev156 (Carlos): ServoHealth already grades the drive as
|
|
146
|
+
// learning/good/elevated/high on the deviation ratio (recent avg
|
|
147
|
+
// vs learned baseline). Surface "high" as a notification so KIP /
|
|
148
|
+
// WilhelmSK show the same red banner the visor card does. Only
|
|
149
|
+
// fires once the baseline is actually learned (deviation != null).
|
|
150
|
+
id: "servo-load-high",
|
|
151
|
+
label: "Autopilot load high",
|
|
152
|
+
severity: "warn",
|
|
153
|
+
defaultEnabled: true,
|
|
154
|
+
sustainSec: 60,
|
|
155
|
+
description: "Servo current is running >65% above the learned baseline. Check for extra drag, hard rudder, clutch slip, or heavy weather.",
|
|
156
|
+
evaluate: (c) => {
|
|
157
|
+
const sh = c.servoHealth;
|
|
158
|
+
if (!sh || sh.status !== "high" || sh.deviationRatio == null)
|
|
159
|
+
return false;
|
|
160
|
+
return sh.deviationRatio > 1.65;
|
|
161
|
+
},
|
|
162
|
+
message: (c) => {
|
|
163
|
+
const r = c.servoHealth?.deviationRatio ?? 0;
|
|
164
|
+
return `AP load ${(r * 100).toFixed(0)}% of baseline (drag / heavy weather?)`;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
102
167
|
{
|
|
103
168
|
id: "low-voltage",
|
|
104
169
|
label: "Low battery voltage",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Rev154 (Carlos): shared numeric constants that used to be
|
|
3
|
+
// duplicated as literals across doctor.ts / kpis.ts / servo-health.ts.
|
|
4
|
+
// Keeping a single source of truth so a policy change (e.g. move to
|
|
5
|
+
// 0.5 A for a bigger servo) does not miss one of the callers.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SERVO_ON_MIN_A = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* Servo current above which we consider the drive to be "on".
|
|
10
|
+
* Below this the servo is idle (or noise from the ADC) and the
|
|
11
|
+
* historian tick should NOT count toward runtime / duty / energy.
|
|
12
|
+
* Same threshold used by:
|
|
13
|
+
* - Doctor authority analysis (servoOn ratio)
|
|
14
|
+
* - KPIs servo runtime + duty cycle + amp-hour integration
|
|
15
|
+
* - ServoHealth baseline learner (only counts on-load samples)
|
|
16
|
+
*/
|
|
17
|
+
exports.SERVO_ON_MIN_A = 0.3;
|
package/dist/doctor.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.DoctorEngine = void 0;
|
|
18
18
|
const kpis_1 = require("./kpis");
|
|
19
|
+
const constants_1 = require("./constants");
|
|
19
20
|
const MIN_DURATION_SEC = 30;
|
|
20
21
|
const DEFAULT_DURATION_SEC = 180;
|
|
21
22
|
class DoctorEngine {
|
|
@@ -157,7 +158,7 @@ class DoctorEngine {
|
|
|
157
158
|
const meanDeg = mean * 180 / Math.PI;
|
|
158
159
|
const rmsDeg = rms * 180 / Math.PI;
|
|
159
160
|
// Servo duty (fraction of engaged samples with servo drawing)
|
|
160
|
-
const servoOnCount = engaged.filter((s) => typeof s.servoCurrent === "number" && s.servoCurrent >
|
|
161
|
+
const servoOnCount = engaged.filter((s) => typeof s.servoCurrent === "number" && s.servoCurrent > constants_1.SERVO_ON_MIN_A).length;
|
|
161
162
|
const servoDuty = engaged.length > 0 ? servoOnCount / engaged.length : 0;
|
|
162
163
|
// Zero-crossing count -> rough oscillation period estimate
|
|
163
164
|
let crossings = 0;
|
|
@@ -180,6 +181,29 @@ class DoctorEngine {
|
|
|
180
181
|
});
|
|
181
182
|
return this.buildResult(session, now, samples, engaged, findings, suggestions);
|
|
182
183
|
}
|
|
184
|
+
// Rev163 (Carlos, per Sean D'Epagnier forum #6): hardware ceiling
|
|
185
|
+
// finding. Sean's core message was that the best gain tuning
|
|
186
|
+
// cannot beat a saturated servo - a stronger motor is the fix.
|
|
187
|
+
// We flag "hardware ceiling" when:
|
|
188
|
+
// - servo duty > 0.75 (drive is on 3/4 of the time) AND
|
|
189
|
+
// - RMS heading error still > 5° (i.e. tracking is bad)
|
|
190
|
+
// AND emit it as a finding WITHOUT any P/I/D suggestion so the
|
|
191
|
+
// skipper doesn't chase gains that cannot help. The message
|
|
192
|
+
// explicitly names hardware, not tuning, as the fix.
|
|
193
|
+
if (servoDuty > 0.75 && rmsDeg > 5) {
|
|
194
|
+
findings.push({
|
|
195
|
+
category: "hardware-ceiling",
|
|
196
|
+
severity: "critical",
|
|
197
|
+
message: `Servo running at ${(servoDuty * 100).toFixed(0)}% duty with ${rmsDeg.toFixed(1)}° RMS error. Tuning cannot close a saturated actuator - consider a more powerful drive or reducing steering friction.`,
|
|
198
|
+
metric: `duty=${(servoDuty * 100).toFixed(0)}% rms=${rmsDeg.toFixed(1)}°`,
|
|
199
|
+
messageKey: "doctor.finding.hardwareCeiling",
|
|
200
|
+
messageArgs: { duty: (servoDuty * 100).toFixed(0), rms: rmsDeg.toFixed(1) },
|
|
201
|
+
});
|
|
202
|
+
// Intentional early return: the follow-on rules would suggest
|
|
203
|
+
// gain changes that cannot help while the drive is saturated.
|
|
204
|
+
// Present just the honest verdict.
|
|
205
|
+
return this.buildResult(session, now, samples, engaged, findings, suggestions);
|
|
206
|
+
}
|
|
183
207
|
// ---- Rule 1: Persistent bias -> increase I ----
|
|
184
208
|
if (Math.abs(meanDeg) > 3) {
|
|
185
209
|
findings.push({
|
|
@@ -393,21 +417,41 @@ class DoctorEngine {
|
|
|
393
417
|
s.dismissedTs = Date.now();
|
|
394
418
|
return { ok: true, message: "Dismissed." };
|
|
395
419
|
}
|
|
420
|
+
// Rev162 (Carlos, per Sean D'Epagnier): apply ONLY ONE gain at a
|
|
421
|
+
// time. The old applyAll iterated blindly through P + I + D in the
|
|
422
|
+
// same tick, which contradicts Sean's forum advice ("tweak the
|
|
423
|
+
// values one at a time") and made it impossible to isolate which
|
|
424
|
+
// change helped. Now applyAll picks the top-priority suggestion,
|
|
425
|
+
// applies it, and marks the rest as "waiting for a fresh Doctor
|
|
426
|
+
// session before the next change". The visor uses `mustRunFreshSession`
|
|
427
|
+
// in the response to render the banner.
|
|
396
428
|
applyAll() {
|
|
397
429
|
const applied = [];
|
|
398
430
|
const failed = [];
|
|
399
431
|
if (!this.result)
|
|
400
432
|
return { ok: false, applied, failed };
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
433
|
+
// Priority order: bias > authority > oscillation > noise. Same as
|
|
434
|
+
// pypilot forum guidance - fix the biggest-error direction first.
|
|
435
|
+
const priority = { bias: 0, authority: 1, oscillation: 2, noise: 3 };
|
|
436
|
+
const pending = this.result.suggestions
|
|
437
|
+
.filter((s) => !s.applied && !s.dismissed)
|
|
438
|
+
.sort((a, b) => (priority[a.category] ?? 99) - (priority[b.category] ?? 99));
|
|
439
|
+
if (pending.length === 0) {
|
|
440
|
+
return { ok: true, applied, failed, mustRunFreshSession: false, remaining: 0 };
|
|
409
441
|
}
|
|
410
|
-
|
|
442
|
+
const first = pending[0];
|
|
443
|
+
const r = this.applySuggestion(first.id);
|
|
444
|
+
if (r.ok)
|
|
445
|
+
applied.push(first.id);
|
|
446
|
+
else
|
|
447
|
+
failed.push({ id: first.id, message: r.message });
|
|
448
|
+
return {
|
|
449
|
+
ok: failed.length === 0,
|
|
450
|
+
applied,
|
|
451
|
+
failed,
|
|
452
|
+
mustRunFreshSession: pending.length > 1,
|
|
453
|
+
remaining: pending.length - 1,
|
|
454
|
+
};
|
|
411
455
|
}
|
|
412
456
|
reset() {
|
|
413
457
|
if (this.timer) {
|