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 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 > 0.3).length;
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
- for (const s of this.result.suggestions) {
402
- if (s.applied || s.dismissed)
403
- continue;
404
- const r = this.applySuggestion(s.id);
405
- if (r.ok)
406
- applied.push(s.id);
407
- else
408
- failed.push({ id: s.id, message: r.message });
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
- return { ok: failed.length === 0, applied, failed };
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) {