signalk-pypilot-newui 2.7.2 → 2.7.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 +47 -0
- package/dist/autopilot-provider.js +78 -14
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/public/app.js +132 -107
- package/public/index.html +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.7.4 — 2026-09-15 — Async control tightening + uppercase accents
|
|
4
|
+
|
|
5
|
+
Closes three findings from the external Rev277 review and finishes
|
|
6
|
+
the Spanish accent pass across UPPERCASE headers and badges.
|
|
7
|
+
|
|
8
|
+
### Async control
|
|
9
|
+
|
|
10
|
+
- **NAV activation can no longer survive a disengage.** `setNavMode`
|
|
11
|
+
now snapshots the engage generation BEFORE `getCourse()` and
|
|
12
|
+
`setMode("nav")`, and bails silently if a disengage arrives during
|
|
13
|
+
either await. A superseded NAV attempt no longer arms the 500 ms
|
|
14
|
+
timer that could have turned a cancelled request into a fresh
|
|
15
|
+
engage. `engage()` also filters "superseded" errors so its own
|
|
16
|
+
fallback into `setState("enabled")` cannot resurrect the cancelled
|
|
17
|
+
intent.
|
|
18
|
+
- **Stale target and mode retries can no longer overwrite newer
|
|
19
|
+
ones.** `setTarget` / `setMode` / `adjustTarget` now each bump a
|
|
20
|
+
dedicated generation counter and pass an `isStale()` callback into
|
|
21
|
+
`_setWithRetry`. If a first `setTarget(1 rad)` was still in retry
|
|
22
|
+
when a second `setTarget(2 rad)` succeeded, the first no longer
|
|
23
|
+
wakes up and pushes 1 rad. Same class of guard R01 already applied
|
|
24
|
+
to `ap.enabled`; now it covers all three write paths.
|
|
25
|
+
- **Restore verdict actually verifies mode and target.** The 10 s
|
|
26
|
+
poll used to only check `pypilotHealthy` and `engaged`; that let a
|
|
27
|
+
pypilot ignoring the mode / target orders still be announced as
|
|
28
|
+
"restored". Now the verdict also compares `state.mode` against
|
|
29
|
+
the snapshot and `state.target` against it (with a 3° tolerance
|
|
30
|
+
for pypilot's own rounding on the wire) before speaking success.
|
|
31
|
+
A mismatch fires the failure banner with a specific reason
|
|
32
|
+
("mode did not switch" / "target did not match").
|
|
33
|
+
|
|
34
|
+
### i18n
|
|
35
|
+
|
|
36
|
+
- Second Spanish accent pass covering UPPERCASE tokens the previous
|
|
37
|
+
pass missed: `ESTADÍSTICAS`, `HISTÓRICO`, `SESIÓN`, `CONEXIÓN`,
|
|
38
|
+
`CONFIGURACIÓN`, `CATÁLOGO`, `GRÁFICA`, `MÁS RÁPIDA`, `TIMÓN`,
|
|
39
|
+
`ÁNGULO`, `MÁXIMA`, and so on.
|
|
40
|
+
|
|
41
|
+
## 2.7.3 — 2026-09-14 — Spanish accents restored
|
|
42
|
+
|
|
43
|
+
The Spanish translation now carries the accents it always should have
|
|
44
|
+
had. About 140 previously plain-ASCII words in the ES dictionary and
|
|
45
|
+
a handful of hard-coded strings in the Info modal placeholders now
|
|
46
|
+
render as `Configuración`, `Conexión`, `Timón`, `Ángulo`, `Sesión`,
|
|
47
|
+
`Grabador de sesiones de navegación`, `Versión pypilot` and so on.
|
|
48
|
+
No functional change.
|
|
49
|
+
|
|
3
50
|
## 2.7.2 — 2026-09-14 — Registry score bump
|
|
4
51
|
|
|
5
52
|
No code change vs 2.7.1. Adds a smoke test suite (Node built-in test
|
|
@@ -73,6 +73,14 @@ class AutopilotProvider {
|
|
|
73
73
|
// 500 ms NAV-mode setTimeout that setNavMode arms, so a disengage
|
|
74
74
|
// during that window cannot be undone by the timer waking up.
|
|
75
75
|
this.engageGen = 0;
|
|
76
|
+
// Rev279 (audit follow-up A / B): parallel counters for target and
|
|
77
|
+
// mode writes. Bumped on every setTarget / setMode / adjustTarget
|
|
78
|
+
// and consulted by the retry loop, so a stale write that was
|
|
79
|
+
// waiting to retry aborts before overwriting a newer intent. R01
|
|
80
|
+
// only protected ap.enabled; these close the same class of race
|
|
81
|
+
// on the two other write paths.
|
|
82
|
+
this.targetGen = 0;
|
|
83
|
+
this.modeGen = 0;
|
|
76
84
|
this.navPendingTimer = null;
|
|
77
85
|
// Rev272 (audit R06): adjustTarget reads data.target BEFORE awaiting
|
|
78
86
|
// the write, so two concurrent +Δ calls could both start from the
|
|
@@ -280,11 +288,20 @@ class AutopilotProvider {
|
|
|
280
288
|
!this.data.options.modes.includes(mode)) {
|
|
281
289
|
throw new Error(`Invalid mode: ${mode}`);
|
|
282
290
|
}
|
|
291
|
+
// Rev279 (audit follow-up B): bump modeGen so a stale mode retry
|
|
292
|
+
// aborts before overwriting a newer setMode / setState intent.
|
|
293
|
+
const gen = ++this.modeGen;
|
|
283
294
|
// Rev254 (Carlos audit): fail explicit when pypilot offline.
|
|
284
295
|
// Rev255: retry with backoff.
|
|
285
|
-
if (!(await this._setWithRetry("ap.mode", mode))) {
|
|
296
|
+
if (!(await this._setWithRetry("ap.mode", mode, () => this.modeGen !== gen))) {
|
|
297
|
+
if (this.modeGen !== gen) {
|
|
298
|
+
throw new Error("mode change superseded by newer order");
|
|
299
|
+
}
|
|
286
300
|
throw new Error("pypilot offline: mode change not delivered");
|
|
287
301
|
}
|
|
302
|
+
if (this.modeGen !== gen) {
|
|
303
|
+
throw new Error("mode change superseded after write");
|
|
304
|
+
}
|
|
288
305
|
}
|
|
289
306
|
async setTarget(rad) {
|
|
290
307
|
// Rev272 (audit R07): reject at the boundary. Downstream callers
|
|
@@ -302,11 +319,22 @@ class AutopilotProvider {
|
|
|
302
319
|
// side).
|
|
303
320
|
// eslint-disable-next-line no-console
|
|
304
321
|
console.log(`[apProvider.setTarget] rad=${rad.toFixed(4)} deg=${deg.toFixed(2)}`);
|
|
322
|
+
// Rev279 (audit follow-up B): bump targetGen so a stale target
|
|
323
|
+
// retry aborts before overwriting a newer setTarget. Without this
|
|
324
|
+
// guard, target=1 rad → 2 rad → retry(1 rad) ended up leaving
|
|
325
|
+
// 1 rad on pypilot.
|
|
326
|
+
const gen = ++this.targetGen;
|
|
305
327
|
// Rev254 (Carlos audit): reject BEFORE mutating optimistic target.
|
|
306
328
|
// Rev255: retry with backoff.
|
|
307
|
-
if (!(await this._setWithRetry("ap.heading_command", deg))) {
|
|
329
|
+
if (!(await this._setWithRetry("ap.heading_command", deg, () => this.targetGen !== gen))) {
|
|
330
|
+
if (this.targetGen !== gen) {
|
|
331
|
+
throw new Error("target superseded by newer order");
|
|
332
|
+
}
|
|
308
333
|
throw new Error("pypilot offline: target not delivered");
|
|
309
334
|
}
|
|
335
|
+
if (this.targetGen !== gen) {
|
|
336
|
+
throw new Error("target superseded after write");
|
|
337
|
+
}
|
|
310
338
|
this.data.target = rad;
|
|
311
339
|
this.pendingTarget = { value: rad, until: Date.now() + TARGET_PENDING_MS };
|
|
312
340
|
this.notifyChanged("target");
|
|
@@ -326,11 +354,21 @@ class AutopilotProvider {
|
|
|
326
354
|
if (this.data.engaged) {
|
|
327
355
|
const base = typeof this.data.target === "number" ? this.data.target : 0;
|
|
328
356
|
const newRad = base + rad;
|
|
357
|
+
// Rev279 (audit follow-up B): adjustTarget also bumps targetGen
|
|
358
|
+
// so an in-flight absolute setTarget cannot silently overwrite
|
|
359
|
+
// the adjusted value from a retry (and vice versa).
|
|
360
|
+
const gen = ++this.targetGen;
|
|
329
361
|
// Rev254 (Carlos audit): same reject-before-optimistic guard.
|
|
330
362
|
// Rev255: retry with backoff.
|
|
331
|
-
if (!(await this._setWithRetry("ap.heading_command", newRad * RAD_TO_DEG))) {
|
|
363
|
+
if (!(await this._setWithRetry("ap.heading_command", newRad * RAD_TO_DEG, () => this.targetGen !== gen))) {
|
|
364
|
+
if (this.targetGen !== gen) {
|
|
365
|
+
throw new Error("target adjust superseded by newer order");
|
|
366
|
+
}
|
|
332
367
|
throw new Error("pypilot offline: target adjust not delivered");
|
|
333
368
|
}
|
|
369
|
+
if (this.targetGen !== gen) {
|
|
370
|
+
throw new Error("target adjust superseded after write");
|
|
371
|
+
}
|
|
334
372
|
this.data.target = newRad;
|
|
335
373
|
this.pendingTarget = { value: newRad, until: Date.now() + TARGET_PENDING_MS };
|
|
336
374
|
// Rev84: publish ONLY the target delta.
|
|
@@ -388,10 +426,19 @@ class AutopilotProvider {
|
|
|
388
426
|
await this.setTarget(newRad);
|
|
389
427
|
}
|
|
390
428
|
async engage() {
|
|
429
|
+
// Rev279 (audit follow-up A): the catch used to fall through to
|
|
430
|
+
// setState("enabled") on ANY setNavMode failure, including a
|
|
431
|
+
// "superseded by newer order" thrown when a disengage arrived
|
|
432
|
+
// during setMode's retry. That turned the cancellation into a
|
|
433
|
+
// fresh engage. setNavMode now returns silently on supersede
|
|
434
|
+
// (does not throw), but we still filter defensively so a
|
|
435
|
+
// superseded throw from lower layers cannot re-arm engage either.
|
|
391
436
|
try {
|
|
392
437
|
await this.setNavMode();
|
|
393
438
|
}
|
|
394
|
-
catch {
|
|
439
|
+
catch (e) {
|
|
440
|
+
if (/superseded/.test(String(e?.message)))
|
|
441
|
+
return;
|
|
395
442
|
await this.setState("enabled");
|
|
396
443
|
}
|
|
397
444
|
}
|
|
@@ -399,23 +446,40 @@ class AutopilotProvider {
|
|
|
399
446
|
await this.setState("disabled");
|
|
400
447
|
}
|
|
401
448
|
async setNavMode() {
|
|
449
|
+
// Rev279 (audit follow-up A): capture engageGen BEFORE the first
|
|
450
|
+
// await. On Rev278 the snapshot was taken after
|
|
451
|
+
// `await this.setMode("nav")`, so a disengage that landed during
|
|
452
|
+
// the mode retry ended up sharing the SAME generation as the
|
|
453
|
+
// arming step — the 500 ms timer then happily fired an engage the
|
|
454
|
+
// user had explicitly cancelled. Now: any bump between here and
|
|
455
|
+
// the timer means we bail silently. engage()'s catch is not
|
|
456
|
+
// supposed to re-fire that either, so we return normally instead
|
|
457
|
+
// of throwing (throwing would trigger engage()'s fallback into a
|
|
458
|
+
// direct setState("enabled"), reintroducing the same bug).
|
|
459
|
+
const gen = this.engageGen;
|
|
460
|
+
const isStale = () => this.engageGen !== gen;
|
|
402
461
|
const cdata = await this.app.getCourse?.();
|
|
462
|
+
if (isStale())
|
|
463
|
+
return;
|
|
403
464
|
if (cdata?.nextPoint &&
|
|
404
465
|
this.getAvailableActionIds().includes("courseCurrentPoint")) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
466
|
+
try {
|
|
467
|
+
await this.setMode("nav");
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
// setMode threw because it was superseded — bail without
|
|
471
|
+
// arming the delayed engage.
|
|
472
|
+
if (/superseded/.test(String(e?.message)))
|
|
473
|
+
return;
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
if (isStale())
|
|
477
|
+
return;
|
|
414
478
|
if (this.navPendingTimer)
|
|
415
479
|
clearTimeout(this.navPendingTimer);
|
|
416
480
|
this.navPendingTimer = setTimeout(() => {
|
|
417
481
|
this.navPendingTimer = null;
|
|
418
|
-
if (
|
|
482
|
+
if (isStale())
|
|
419
483
|
return; // superseded, do nothing
|
|
420
484
|
this.setState("enabled").catch(() => { });
|
|
421
485
|
}, 500);
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const session_recorder_1 = require("./session-recorder");
|
|
|
15
15
|
const trip_recorder_1 = require("./trip-recorder");
|
|
16
16
|
// Rev counter bumped on every build so the user can distinguish deploys
|
|
17
17
|
// from the webapp header (feedback_revision_bump_each_build).
|
|
18
|
-
const PLUGIN_REVISION = "
|
|
18
|
+
const PLUGIN_REVISION = "Rev279";
|
|
19
19
|
// Rev59: read package.json once at load time so /status can report the
|
|
20
20
|
// npm package version alongside the internal Rev counter.
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-pypilot-newui",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.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
|
@@ -564,6 +564,8 @@
|
|
|
564
564
|
"restore.working": "Restoring... verifying pypilot confirmation",
|
|
565
565
|
"restore.reason.noPy": "pypilot did not confirm",
|
|
566
566
|
"restore.reason.noEng":"AP did not engage",
|
|
567
|
+
"restore.reason.modeMismatch": "mode did not switch",
|
|
568
|
+
"restore.reason.targetMismatch": "target did not match",
|
|
567
569
|
// Alerts / prompts / confirms
|
|
568
570
|
"alert.saveOk": "Saved.",
|
|
569
571
|
"alert.saveFail": "Save failed. Are you logged in to Signal K admin?",
|
|
@@ -687,15 +689,15 @@
|
|
|
687
689
|
"tack": "VIRAR",
|
|
688
690
|
"cal": "CAL",
|
|
689
691
|
"cal.level": "Nivelar (barco adrizado)",
|
|
690
|
-
"cal.rudCentered": "
|
|
692
|
+
"cal.rudCentered": "Timón centrado",
|
|
691
693
|
"cal.rudPort": "Rango babor",
|
|
692
694
|
"cal.rudStar": "Rango estribor",
|
|
693
|
-
"cal.rudReset": "Reset
|
|
695
|
+
"cal.rudReset": "Reset timón",
|
|
694
696
|
"cal.headingOffset": "Offset rumbo IMU (grados)",
|
|
695
|
-
"cal.rudRange": "Rango
|
|
697
|
+
"cal.rudRange": "Rango timón (grados)",
|
|
696
698
|
"cal.advanced.title": "Sliders avanzados",
|
|
697
699
|
"cal.advanced.hint": "Todos los RangeSettings de pypilot, agrupados. Pulsa largo o pon el raton sobre el nombre para ver la clave pypilot cruda.",
|
|
698
|
-
"cal.grp.rudder": "
|
|
700
|
+
"cal.grp.rudder": "Timón",
|
|
699
701
|
"cal.grp.servo": "Servo",
|
|
700
702
|
"cal.grp.imu": "IMU",
|
|
701
703
|
"cal.grp.ap": "Piloto",
|
|
@@ -717,12 +719,12 @@
|
|
|
717
719
|
"setup.status.sshSummary": "SSH: {user}",
|
|
718
720
|
"setup.status.sshMissing": "SSH sin configurar",
|
|
719
721
|
"setup.status.sentences": "Sentencias activas:",
|
|
720
|
-
"setup.status.calLast": "
|
|
722
|
+
"setup.status.calLast": "Último ajuste:",
|
|
721
723
|
"setup.status.calNever": "Todavia no se ha ajustado nada.",
|
|
722
724
|
"setup.status.calNeverShort": "sin ajustar",
|
|
723
725
|
"cal.unlock": "Desbloquear sliders",
|
|
724
|
-
"configuration": "
|
|
725
|
-
"calibration.title": "
|
|
726
|
+
"configuration": "Configuración",
|
|
727
|
+
"calibration.title": "Calibración",
|
|
726
728
|
"save": "Guardar",
|
|
727
729
|
"cancel": "Cancelar",
|
|
728
730
|
"close": "Cerrar",
|
|
@@ -733,7 +735,7 @@
|
|
|
733
735
|
"paths.put": "PUT?",
|
|
734
736
|
"paths.copy": "Copiar",
|
|
735
737
|
"paths.hint": "Se aplica al instante, sin reinicio. Los paths esenciales (state / mode / target / tack / selectores) siempre publican. Con \"solo esenciales\" ON marca el check para activar un path; con OFF marca para mantener y desmarca para parar.",
|
|
736
|
-
"paths.header": "
|
|
738
|
+
"paths.header": "Catálogo en vivo de paths Signal K que este plugin publica y responde a PUT. Copia con los botones para pegar en configuraciones de widgets KIP.",
|
|
737
739
|
"paths.refresh": "Refrescar",
|
|
738
740
|
"paths.kip.title": "Sentencias de accion para KIP",
|
|
739
741
|
"paths.kip.hint": "Enlaza estas sentencias con widgets KIP \"Simple Switch\" (filas azules) o \"Put\" (filas verdes).",
|
|
@@ -743,7 +745,7 @@
|
|
|
743
745
|
"paths.kip.col.values": "Valor(es)",
|
|
744
746
|
"paths.kip.col.copy": "Copiar",
|
|
745
747
|
"paths.kip.widget.apEngage": "Activar AP",
|
|
746
|
-
"paths.kip.widget.nudge": "
|
|
748
|
+
"paths.kip.widget.nudge": "Corrección rumbo <code>-10 / -1 / +1 / +10</code>",
|
|
747
749
|
"paths.kip.widget.tack": "Virar <code>babor / estribor / cancelar</code>",
|
|
748
750
|
"paths.kip.widget.mode": "Modo <code>compass / gps / wind / true / nav</code>",
|
|
749
751
|
"paths.kip.widget.profile": "Perfil <code><nombre></code>",
|
|
@@ -754,7 +756,7 @@
|
|
|
754
756
|
"paths.kip.type.radio": "Simple Switch (radio)",
|
|
755
757
|
"paths.kip.type.radioDyn": "Simple Switch (radio, dinamico)",
|
|
756
758
|
"paths.kip.type.string": "texto",
|
|
757
|
-
"paths.kip.type.numberRad": "
|
|
759
|
+
"paths.kip.type.numberRad": "número (rad)",
|
|
758
760
|
"paths.kip.val.engage": "1 = activar, 0 = desactivar",
|
|
759
761
|
"paths.kip.val.fireReset": "1 = disparar, auto-reset",
|
|
760
762
|
"paths.kip.val.fire": "1 = disparar",
|
|
@@ -769,39 +771,39 @@
|
|
|
769
771
|
"paths.all.title": "Todos los paths publicados",
|
|
770
772
|
"paths.search.ph": "filtrar paths...",
|
|
771
773
|
"paths.essentials.label": "Publicar solo esenciales + los activados explicitamente (recomendado)",
|
|
772
|
-
"nudge.title": "Botones
|
|
773
|
-
"nudge.small": "
|
|
774
|
+
"nudge.title": "Botones corrección rumbo",
|
|
775
|
+
"nudge.small": "Pequeño (-1/+1)",
|
|
774
776
|
"nudge.big": "Grande (-10/+10)",
|
|
775
|
-
"nudge.hint": "Grados
|
|
777
|
+
"nudge.hint": "Grados añadidos al rumbo objetivo cada vez que pulsas un botón de nudge. También se configura en SK Admin -> Plugin Config -> PyPilot New-UI.",
|
|
776
778
|
"language.title": "Idioma",
|
|
777
779
|
"language.hint": "Solo en este navegador. No pasa por el servidor.",
|
|
778
780
|
// Tabs
|
|
779
781
|
"tab.control": "Control",
|
|
780
782
|
"tab.tune": "Ajustes",
|
|
781
|
-
"tab.chart": "
|
|
783
|
+
"tab.chart": "Gráfica",
|
|
782
784
|
"tab.regata": "Regata",
|
|
783
785
|
"tab.paths": "Paths & API",
|
|
784
786
|
"tab.setup": "Setup",
|
|
785
787
|
"tab.info": "Info",
|
|
786
788
|
// Chart tab (Rev96)
|
|
787
|
-
"chart.stats.title": "
|
|
788
|
-
"chart.session.empty": "
|
|
789
|
-
"chart.session.started": "
|
|
789
|
+
"chart.stats.title": "ESTADÍSTICAS SESIÓN",
|
|
790
|
+
"chart.session.empty": "Sesión no iniciada",
|
|
791
|
+
"chart.session.started": "Sesión iniciada a las",
|
|
790
792
|
"chart.session.ago": "hace",
|
|
791
793
|
"chart.session.reset": "Reiniciar",
|
|
792
|
-
"chart.session.confirm": "Poner las
|
|
794
|
+
"chart.session.confirm": "Poner las estadísticas a cero? El histórico grafico se conserva.",
|
|
793
795
|
"chart.stat.engaged": "AP activo",
|
|
794
796
|
"chart.stat.distance": "Distancia",
|
|
795
|
-
"chart.stat.energy": "
|
|
797
|
+
"chart.stat.energy": "Energía",
|
|
796
798
|
"chart.stat.meanErr": "Error medio",
|
|
797
799
|
"chart.stat.rmsErr": "RMS error",
|
|
798
800
|
"chart.stat.p95Err": "p95 error",
|
|
799
801
|
"chart.stat.servoRuntime": "Servo activo",
|
|
800
802
|
"chart.stat.tacksGybes": "Viradas / Trasluchadas",
|
|
801
803
|
"chart.stat.maxServoA": "Max servo A",
|
|
802
|
-
"chart.history.title": "
|
|
804
|
+
"chart.history.title": "HISTÓRICO",
|
|
803
805
|
"chart.history.refresh": "Actualizar",
|
|
804
|
-
"chart.empty": "
|
|
806
|
+
"chart.empty": "Aún sin datos - esperando primera muestra...",
|
|
805
807
|
"chart.servoHealth.title": "SALUD DEL SERVO",
|
|
806
808
|
"chart.servoHealth.learning":"Aprendiendo baseline...",
|
|
807
809
|
"chart.servoHealth.baseline":"Baseline",
|
|
@@ -811,12 +813,12 @@
|
|
|
811
813
|
"chart.servoHealth.temp": "Temp",
|
|
812
814
|
"chart.servoHealth.voltage": "Voltaje",
|
|
813
815
|
"setup.paths.title": "Sentencias de Signal K",
|
|
814
|
-
"setup.conn.title": "
|
|
816
|
+
"setup.conn.title": "Conexión pypilot",
|
|
815
817
|
"setup.conn.status": "Estado",
|
|
816
818
|
"setup.remoteConsole": "Consola control remoto",
|
|
817
819
|
// Alarms (Rev101)
|
|
818
820
|
"setup.alarms.title": "Alarmas",
|
|
819
|
-
"setup.alarms.hint": "Activa o desactiva cada regla y silencia temporalmente. Las
|
|
821
|
+
"setup.alarms.hint": "Activa o desactiva cada regla y silencia temporalmente. Las alarmás activas salen en el banner de arriba y publican notificaciones SK canonicas para KIP / WilhelmSK.",
|
|
820
822
|
"alarm.ack": "ACK",
|
|
821
823
|
"alarm.acked": "Ack'd",
|
|
822
824
|
"alarm.mute15": "Silenciar 15m",
|
|
@@ -827,7 +829,7 @@
|
|
|
827
829
|
"alarm.off": "OFF",
|
|
828
830
|
// Pre-departure check (Rev102)
|
|
829
831
|
"setup.precheck.title": "Comprobacion del piloto",
|
|
830
|
-
"setup.precheck.hint": "Chequeo antes de zarpar. Agrega sensores, servo, voltaje y conectividad en un veredicto
|
|
832
|
+
"setup.precheck.hint": "Chequeo antes de zarpar. Agrega sensores, servo, voltaje y conectividad en un veredicto único. Se recalcula cada 3 s mientras esta pestaña está abierta.",
|
|
831
833
|
"setup.precheck.refresh": "Actualizar",
|
|
832
834
|
"setup.precheck.unknown": "Comprobando...",
|
|
833
835
|
"setup.precheck.verdict.ready": "Listo para activar",
|
|
@@ -835,7 +837,7 @@
|
|
|
835
837
|
"setup.precheck.verdict.fail": "NO activar",
|
|
836
838
|
// Doctor (Rev104)
|
|
837
839
|
"setup.doctor.title": "Asesoramiento y diagnostico",
|
|
838
|
-
"setup.doctor.hint": "Activa el AP primero y pulsa DIAGNOSTICAR. El plugin graba el error de rumbo y respuesta del servo, y propone ajustes P/I/D. Nada se aplica
|
|
840
|
+
"setup.doctor.hint": "Activa el AP primero y pulsa DIAGNOSTICAR. El plugin graba el error de rumbo y respuesta del servo, y propone ajustes P/I/D. Nada se aplica automáticamente - revisa cada sugerencia y pulsa APLICAR.",
|
|
839
841
|
"setup.doctor.start": "DIAGNOSTICAR",
|
|
840
842
|
"setup.doctor.cancel": "Cancelar",
|
|
841
843
|
"setup.doctor.recording": "Grabando",
|
|
@@ -846,9 +848,9 @@
|
|
|
846
848
|
"setup.doctor.applied": "APLICADO",
|
|
847
849
|
"setup.doctor.applyAll": "Aplicar todo",
|
|
848
850
|
"setup.doctor.applyAllConfirm": "Aplicar todos los ajustes sugeridos a pypilot?",
|
|
849
|
-
"setup.doctor.applyTop": "Aplicar el
|
|
850
|
-
"setup.doctor.applyTopConfirm": "Aplicar el ajuste
|
|
851
|
-
"setup.doctor.freshRequired": "Aplicado 1 ajuste. Corre otra
|
|
851
|
+
"setup.doctor.applyTop": "Aplicar el más prioritario",
|
|
852
|
+
"setup.doctor.applyTopConfirm": "Aplicar el ajuste más prioritario y correr otra sesión del Doctor antes del siguiente?",
|
|
853
|
+
"setup.doctor.freshRequired": "Aplicado 1 ajuste. Corre otra sesión del Doctor antes de aplicar los siguientes {n}.",
|
|
852
854
|
"setup.doctor.confidence": "Confianza",
|
|
853
855
|
"setup.doctor.samplesSummary": "{engaged} muestras activas de {total}, piloto '{pilot}'.",
|
|
854
856
|
"setup.doctor.forkedProfile": "Cambios aplicados en un perfil NUEVO {new}. Tu perfil original {orig} queda intacto y a un toque en Ajustes → Perfil.",
|
|
@@ -862,12 +864,12 @@
|
|
|
862
864
|
"doctor.conf.low": "baja",
|
|
863
865
|
"doctor.conf.medium": "media",
|
|
864
866
|
"doctor.conf.high": "alta",
|
|
865
|
-
"doctor.reason.bias": "El barco se ha desviado del rumbo objetivo {deg}° de media. Esa deriva es lo que corrige la ganancia integral (I) con el tiempo — una I baja hace que el AP tarde mucho (o nunca) en cerrar un
|
|
867
|
+
"doctor.reason.bias": "El barco se ha desviado del rumbo objetivo {deg}° de media. Esa deriva es lo que corrige la ganancia integral (I) con el tiempo — una I baja hace que el AP tarde mucho (o nunca) en cerrar un desvío persistente.",
|
|
866
868
|
"doctor.effect.bias": "Subir I deberia borrar la deriva en segundos. Vigila la rueda: una I demasiado alta genera overshoot y empieza a bailar — si lo ves, reduce de nuevo.",
|
|
867
869
|
"doctor.reason.oscillation": "El rumbo oscila con un periodo de unos {period} segundos. Sin suficiente ganancia derivativa (D) el AP no puede amortiguar sus propias correcciones y el barco culea de un lado a otro.",
|
|
868
|
-
"doctor.effect.oscillation": "
|
|
869
|
-
"doctor.reason.authority": "El servo
|
|
870
|
-
"doctor.effect.authority": "Una P mayor mete
|
|
870
|
+
"doctor.effect.oscillation": "Más D calma el vaivén y hace que el AP se sienta menos nervioso. La respuesta al timón se vuelve algo más lenta — es un cambio razonable para conseguir un rumbo más recto.",
|
|
871
|
+
"doctor.reason.authority": "El servo está trabajando mucho pero el barco sigue sin alcanzar el rumbo. Esto es P débil clásico: el AP pide, el timón se mueve, pero la corrección es demasiado pequeña.",
|
|
872
|
+
"doctor.effect.authority": "Una P mayor mete más timón para el mismo error. Deberia apretar el seguimiento del rumbo. Si empieza a bailar con cada ola, reduce y prueba a subir D en su lugar.",
|
|
871
873
|
"setup.doctor.idle": "inactivo",
|
|
872
874
|
"setup.doctor.analyzing.short": "analizando...",
|
|
873
875
|
"setup.doctor.healthy": "sano",
|
|
@@ -876,12 +878,12 @@
|
|
|
876
878
|
"doctor.summary.clean": "Sin incidencias significativas. Las ganancias parecen sanas.",
|
|
877
879
|
"doctor.summary.notedOnly": "{issues} incidencia(s) anotada(s) (sin cambio de ganancia sugerido).",
|
|
878
880
|
"doctor.summary.detected": "{issues} incidencia(s) detectada(s). {suggestions} cambio(s) de ganancia sugerido(s).",
|
|
879
|
-
"doctor.finding.insufficientData": "Solo {n} muestras activas.
|
|
880
|
-
"doctor.finding.bias": "
|
|
881
|
+
"doctor.finding.insufficientData": "Solo {n} muestras activas. Análisis omitido - deja el piloto enganchado toda la sesión.",
|
|
882
|
+
"doctor.finding.bias": "Desvío de rumbo persistente de {deg}° (error medio).",
|
|
881
883
|
"doctor.finding.oscillation": "Rumbo oscilando con periodo ~{period}s, RMS {rms}°.",
|
|
882
884
|
"doctor.finding.authority": "Servo al {duty}% de trabajo pero el error de rumbo sigue en {rms}°. El piloto pierde autoridad.",
|
|
883
885
|
"doctor.finding.noise": "Rumbo muy ajustado ({rms}°) pero servo al {duty}% de trabajo - posible ruido en la señal de rumbo.",
|
|
884
|
-
"doctor.finding.hardwareCeiling": "Servo al {duty}% de trabajo con {rms}° de error RMS - el actuador esta saturado. Ningun ajuste de P/I/D cierra un limite hardware; considera un servo
|
|
886
|
+
"doctor.finding.hardwareCeiling": "Servo al {duty}% de trabajo con {rms}° de error RMS - el actuador esta saturado. Ningun ajuste de P/I/D cierra un limite hardware; considera un servo más potente o reducir la friccion del sistema de dirección.",
|
|
885
887
|
"alarm.summary.active": "{n} activas",
|
|
886
888
|
"alarm.summary.muted": "{n} silenciadas",
|
|
887
889
|
"alarm.summary.off": "{n} off",
|
|
@@ -904,9 +906,9 @@
|
|
|
904
906
|
"sq.col.status": "Estado",
|
|
905
907
|
"sq.action.ignore": "Ignorar",
|
|
906
908
|
"sq.action.restore": "Restaurar",
|
|
907
|
-
"alarm.rule.heading-deviation.label": "
|
|
909
|
+
"alarm.rule.heading-deviation.label": "Desvío de rumbo",
|
|
908
910
|
"alarm.rule.heading-deviation.msg": "Error de rumbo del piloto: {deg}° sostenido",
|
|
909
|
-
"alarm.rule.cruise-drift.label": "
|
|
911
|
+
"alarm.rule.cruise-drift.label": "Desvío sostenido del rumbo",
|
|
910
912
|
"alarm.rule.cruise-drift.msg": "El barco corre {deg}° a {side} del rumbo objetivo",
|
|
911
913
|
"alarm.rule.unable-to-steer.label": "Piloto sin autoridad",
|
|
912
914
|
"alarm.rule.unable-to-steer.msg": "El piloto pierde autoridad: {deg}° de error, servo al {duty}%",
|
|
@@ -926,7 +928,7 @@
|
|
|
926
928
|
// Rev265 (Carlos): mensaje viene ya formateado por el backend
|
|
927
929
|
// con h/min/s dinámicos; no se traduce con template.
|
|
928
930
|
// Setup tab
|
|
929
|
-
"setup.connection": "
|
|
931
|
+
"setup.connection": "Conexión",
|
|
930
932
|
"setup.scan.title": "Escanear LAN buscando pypilot_web",
|
|
931
933
|
"setup.scan.subnet.ph": "auto-detectar (p.ej. 192.168.1.0/24)",
|
|
932
934
|
"setup.scan.btn": "Escanear",
|
|
@@ -940,7 +942,7 @@
|
|
|
940
942
|
"setup.absorb.title": "Provider de Autopiloto (modo un-solo-socket)",
|
|
941
943
|
"setup.absorb.label": "Absorber el provider oficial",
|
|
942
944
|
"setup.absorb.btn": "Guardar y reiniciar",
|
|
943
|
-
"setup.absorb.hint": "Cuando esta ON, este plugin se registra como Signal K Autopilot Provider (WilhelmSK / freeboard / KIP se conectan por /signalk/v2/api/vessels/self/autopilots). Debes desactivar
|
|
945
|
+
"setup.absorb.hint": "Cuando esta ON, este plugin se registra como Signal K Autopilot Provider (WilhelmSK / freeboard / KIP se conectan por /signalk/v2/api/vessels/self/autopilots). Debes desactivar también el plugin 'pypilot-autopilot-provider' en SK Admin - dos providers pelearian por el pilot ID. Ventaja: un socket menos hacia la TinyPilot Pi Zero.",
|
|
944
946
|
"setup.emergency": "Emergencia",
|
|
945
947
|
"setup.ssh.user": "Usuario SSH",
|
|
946
948
|
"setup.ssh.password": "Password SSH",
|
|
@@ -961,7 +963,7 @@
|
|
|
961
963
|
"smart.autoProfile.heavy": "Perfil para TWS > 16 kn",
|
|
962
964
|
"smart.autoProfile.skip": "(saltar)",
|
|
963
965
|
"smart.gust.title": "Estrategia frente a rachas",
|
|
964
|
-
"smart.gust.hint": "Detecta cuando el AWS sube 5 kn o
|
|
966
|
+
"smart.gust.hint": "Detecta cuando el AWS sube 5 kn o más en 5 s y aplica la accion elegida.",
|
|
965
967
|
"smart.gust.strategy": "Accion en racha",
|
|
966
968
|
"smart.gust.opt.off": "OFF — no hacer nada",
|
|
967
969
|
"smart.gust.opt.warn": "Solo avisar",
|
|
@@ -987,17 +989,17 @@
|
|
|
987
989
|
"watch.detailsToggle": "Ver detalle del focus",
|
|
988
990
|
"watch.noFocus": "(sin focus activo)",
|
|
989
991
|
"sess.title": "Grabador de sesiones de navegacion",
|
|
990
|
-
"sess.hint": "Cada
|
|
991
|
-
"sess.tag": "Etiquetar
|
|
992
|
+
"sess.hint": "Cada sesión con el AP enganchado se guarda en el Pi 5 como JSONL. Comparte con el autor del plugin para recibir consejos de ajuste personalizados.",
|
|
993
|
+
"sess.tag": "Etiquetar sesión actual",
|
|
992
994
|
"sess.list": "Ver sesiones grabadas",
|
|
993
995
|
"sess.advice": "Consejos recibidos",
|
|
994
996
|
"sess.close": "Cerrar",
|
|
995
997
|
"sess.cancel": "Cancelar",
|
|
996
998
|
"sess.st.disabled": "grabador desactivado en config del plugin",
|
|
997
|
-
"sess.st.idle": "en reposo — AP no enganchado, no hay
|
|
999
|
+
"sess.st.idle": "en reposo — AP no enganchado, no hay sesión abierta",
|
|
998
1000
|
"sess.st.recording": "grabando",
|
|
999
1001
|
"sess.st.samples": "muestras",
|
|
1000
|
-
"sess.tag.title": "Etiquetar
|
|
1002
|
+
"sess.tag.title": "Etiquetar sesión actual",
|
|
1001
1003
|
"sess.tag.save": "Guardar",
|
|
1002
1004
|
"sess.tag.err": "Fallo al guardar",
|
|
1003
1005
|
"sess.tag.wind": "Viento (kn)",
|
|
@@ -1014,7 +1016,7 @@
|
|
|
1014
1016
|
"sess.motor.sail": "solo vela",
|
|
1015
1017
|
"sess.motor.motor-sailing": "motorvela",
|
|
1016
1018
|
"sess.motor.motor": "solo motor",
|
|
1017
|
-
"sess.pos.close-hauled": "
|
|
1019
|
+
"sess.pos.close-hauled": "ceñida",
|
|
1018
1020
|
"sess.pos.reaching": "traves",
|
|
1019
1021
|
"sess.pos.broad-reach": "largo",
|
|
1020
1022
|
"sess.pos.running": "empopada",
|
|
@@ -1026,10 +1028,10 @@
|
|
|
1026
1028
|
"sess.list.download": "Descargar JSON",
|
|
1027
1029
|
"sess.list.share": "Compartir por WhatsApp",
|
|
1028
1030
|
"sess.list.delete": "Borrar",
|
|
1029
|
-
"sess.list.delConfirm": "Borrar
|
|
1031
|
+
"sess.list.delConfirm": "Borrar sesión {id}?",
|
|
1030
1032
|
"sess.list.hasAdvice": "consejos recibidos",
|
|
1031
1033
|
"sess.advice.title": "Consejos recibidos",
|
|
1032
|
-
"sess.advice.empty": "No hay consejos
|
|
1034
|
+
"sess.advice.empty": "No hay consejos aún. Comparte una sesión y sube después el JSON de consejos que recibas.",
|
|
1033
1035
|
"sess.advice.findings": "Hallazgos",
|
|
1034
1036
|
"sess.advice.recs": "Recomendaciones",
|
|
1035
1037
|
"sess.advice.upload": "Subir JSON de consejos",
|
|
@@ -1044,31 +1046,31 @@
|
|
|
1044
1046
|
// "VIENTO APARENTE" / "VIENTO REAL" fully.
|
|
1045
1047
|
"mode.label.wind.long": "VIENTO APARENTE",
|
|
1046
1048
|
"mode.label.true-wind.long": "VIENTO REAL",
|
|
1047
|
-
"tune.gain.helpBtn": "
|
|
1048
|
-
"tune.gain.P.short": "Corrige el error de rumbo al instante con
|
|
1049
|
-
"tune.gain.P.long": "Subir si el AP tarda en cerrar un
|
|
1049
|
+
"tune.gain.helpBtn": "Más info",
|
|
1050
|
+
"tune.gain.P.short": "Corrige el error de rumbo al instante con más rueda. Es el reflejo 'hay que corregir ya'.",
|
|
1051
|
+
"tune.gain.P.long": "Subir si el AP tarda en cerrar un desvío. Bajar si el timón 'baila' a cada ola.",
|
|
1050
1052
|
"tune.gain.I.short": "Cierra una deriva mantenida en el tiempo: si el barco se queda 5° a un lado, la I lo empuja de vuelta.",
|
|
1051
|
-
"tune.gain.I.long": "Subir si hay
|
|
1052
|
-
"tune.gain.D.short": "Frena la
|
|
1053
|
+
"tune.gain.I.long": "Subir si hay desvío persistente que no se cierra. Bajar si el AP se pasa de largo (overshoot).",
|
|
1054
|
+
"tune.gain.D.short": "Frena la corrección cuando el barco ya vuelve al rumbo, para no pasarse.",
|
|
1053
1055
|
"tune.gain.D.long": "Subir si el AP oscila y no se estabiliza. Bajar si la respuesta se siente pastosa o lenta.",
|
|
1054
|
-
"tune.gain.DD.short": "Anticipa la velocidad a la que crece el error. Ayuda con olas cortas y
|
|
1055
|
-
"tune.gain.DD.long": "Subir en mar rizada o con olas cortas de popa. Bajar si mete nervios al
|
|
1056
|
+
"tune.gain.DD.short": "Anticipa la velocidad a la que crece el error. Ayuda con olas cortas y guiñadas rápidas.",
|
|
1057
|
+
"tune.gain.DD.long": "Subir en mar rizada o con olas cortas de popa. Bajar si mete nervios al timón.",
|
|
1056
1058
|
"tune.gain.PR.short": "Como la P pero reacciona a la velocidad de giro del barco, no al error en si.",
|
|
1057
1059
|
"tune.gain.PR.long": "Subir si el barco entra en el giro con demasiadas ganas. Bajar si va apatico.",
|
|
1058
1060
|
"tune.gain.FF.short": "Reacciona al cambio de rumbo objetivo antes de que aparezca el error.",
|
|
1059
|
-
"tune.gain.FF.long": "Subir para cambios de rumbo
|
|
1060
|
-
"tune.gain.servo.max_current.short": "Corriente
|
|
1061
|
-
"tune.gain.servo.max_current.long": "Subir si el barco necesita
|
|
1062
|
-
"tune.gain.servo.max_slew_speed.short": "Velocidad
|
|
1063
|
-
"tune.gain.servo.max_slew_speed.long": "Subir para respuesta
|
|
1064
|
-
"tune.gain.imu.heading_offset.short": "
|
|
1065
|
-
"tune.gain.imu.heading_offset.long": "No lo toques a mano salvo que sepas la
|
|
1066
|
-
"tune.gain.rudder.range.short": "Grados
|
|
1067
|
-
"tune.gain.rudder.range.long": "Ajusta al tope
|
|
1068
|
-
"tune.gain.ap.tack.angle.short": "Grados que gira una virada
|
|
1069
|
-
"tune.gain.ap.tack.angle.long": "Subir para viradas amplias (
|
|
1070
|
-
"tune.gain.ap.tack.rate.short": "Velocidad de una virada
|
|
1071
|
-
"tune.gain.ap.tack.rate.long": "Subir para virada
|
|
1061
|
+
"tune.gain.FF.long": "Subir para cambios de rumbo más nitidos. Bajar si hay overshoot al fijar rumbo nuevo.",
|
|
1062
|
+
"tune.gain.servo.max_current.short": "Corriente máxima que aceptas del motor del timón. Protege el servo de tirones.",
|
|
1063
|
+
"tune.gain.servo.max_current.long": "Subir si el barco necesita más par en mal tiempo. Bajar para saltar antes la proteccion en un actuador fragil.",
|
|
1064
|
+
"tune.gain.servo.max_slew_speed.short": "Velocidad máxima con la que el timón se mueve. Si es muy alta, el timón 'corta' el agua.",
|
|
1065
|
+
"tune.gain.servo.max_slew_speed.long": "Subir para respuesta más viva en barco ligero. Bajar en desplazamiento pesado para suavizar el movimiento.",
|
|
1066
|
+
"tune.gain.imu.heading_offset.short": "Ángulo entre la linea de crujía y el sensor IMU. Se ajusta con el botón 'Level (boat is flat)'.",
|
|
1067
|
+
"tune.gain.imu.heading_offset.long": "No lo toques a mano salvo que sepas la desalineación exacta. Usa el botón de calibración.",
|
|
1068
|
+
"tune.gain.rudder.range.short": "Grados máximos que puede mover el timón a cada banda.",
|
|
1069
|
+
"tune.gain.rudder.range.long": "Ajusta al tope físico real del timón. Muy pequeño y el AP pierde autoridad; muy grande y quemás el motor.",
|
|
1070
|
+
"tune.gain.ap.tack.angle.short": "Grados que gira una virada automática.",
|
|
1071
|
+
"tune.gain.ap.tack.angle.long": "Subir para viradas amplias (más seguras en marejada). Bajar para viradas cerradas estilo regata.",
|
|
1072
|
+
"tune.gain.ap.tack.rate.short": "Velocidad de una virada automática en grados por segundo.",
|
|
1073
|
+
"tune.gain.ap.tack.rate.long": "Subir para virada más rápida. Bajar si el barco escora demasiado o la tripulacion no sigue.",
|
|
1072
1074
|
"lc.title": "Captura de logs (Pi Zero)",
|
|
1073
1075
|
"lc.hint": "piCore guarda /var/log solo en RAM, un hard reset los pierde. Activalo para que el plugin guarde pypilot / pypilot_web / pypilot_hat en un archivo persistente del Pi 5.",
|
|
1074
1076
|
"lc.toggle": "Activar captura",
|
|
@@ -1079,7 +1081,7 @@
|
|
|
1079
1081
|
"lc.st.off": "PARADA",
|
|
1080
1082
|
"lc.st.every": "cada",
|
|
1081
1083
|
"lc.st.file": "archivo",
|
|
1082
|
-
"lc.st.last": "
|
|
1084
|
+
"lc.st.last": "última",
|
|
1083
1085
|
"setup.ssh.password.ph.set": "(configurado - reescribe para cambiarlo)",
|
|
1084
1086
|
"setup.ssh.save.btn": "Guardar",
|
|
1085
1087
|
"setup.restart.btn": "REINICIAR pypilot",
|
|
@@ -1120,45 +1122,45 @@
|
|
|
1120
1122
|
"dash.polar.title": "DATOS DE POLARES",
|
|
1121
1123
|
"dash.polar.hint": "Activa las que quieras que aparezcan en los selectores de esquinas. Requiere el plugin signalk-polar-performance-plugin.",
|
|
1122
1124
|
"dash.custom.title": "SK PATH PERSONALIZADO",
|
|
1123
|
-
"dash.custom.hint": "
|
|
1125
|
+
"dash.custom.hint": "Añade cualquier path publicado por Signal K. Se muestra como aparezca en el bus (sin conversion).",
|
|
1124
1126
|
"dash.custom.path.ph": "p.ej. environment.water.temperature",
|
|
1125
1127
|
"dash.custom.label.ph": "Etiqueta",
|
|
1126
1128
|
"dash.custom.unit.ph": "Unidad (opt)",
|
|
1127
|
-
"dash.custom.add": "
|
|
1129
|
+
"dash.custom.add": "Añadir",
|
|
1128
1130
|
"dash.helmBar.title": "BARRA INFERIOR (indicador)",
|
|
1129
|
-
"dash.helmBar.hint": "Elige que se muestra en la barra de abajo: el
|
|
1131
|
+
"dash.helmBar.hint": "Elige que se muestra en la barra de abajo: el ángulo del timón o la escora del barco.",
|
|
1130
1132
|
"dash.helmBar.heel": "Escora (por defecto)",
|
|
1131
|
-
"dash.helmBar.rudder": "
|
|
1132
|
-
"dash.overlay.gust.title": "Marca de
|
|
1133
|
+
"dash.helmBar.rudder": "Ángulo del timón",
|
|
1134
|
+
"dash.overlay.gust.title": "Marca de ráfagas",
|
|
1133
1135
|
"dash.overlay.gust.needs": "Deja una flecha A fantasma con la velocidad de la racha; se desvanece en 10 s. Necesita sensor de viento.",
|
|
1134
1136
|
"dash.overlay.gust.test": "Probar ahora",
|
|
1135
1137
|
"tack.port.title": "Virar a babor",
|
|
1136
1138
|
"tack.star.title": "Virar a estribor",
|
|
1137
|
-
"nav.toggle.aria": "Mostrar / ocultar barra de
|
|
1138
|
-
"helmChip.heelTitle": "Escora - toca para ver
|
|
1139
|
-
"helmChip.rudderTitle": "
|
|
1139
|
+
"nav.toggle.aria": "Mostrar / ocultar barra de pestañas",
|
|
1140
|
+
"helmChip.heelTitle": "Escora - toca para ver ángulo de timón",
|
|
1141
|
+
"helmChip.rudderTitle": "Timón - toca para ver escora",
|
|
1140
1142
|
// Modal historial de bordos
|
|
1141
1143
|
"tackLog.empty": "Sin bordos registrados en el rango.",
|
|
1142
1144
|
"tackLog.total": "Total",
|
|
1143
1145
|
"tackLog.totalUnit": "bordos",
|
|
1144
|
-
"tackLog.bestRet": "Mejor
|
|
1145
|
-
"tackLog.bestRec": "
|
|
1146
|
+
"tackLog.bestRet": "Mejor retención",
|
|
1147
|
+
"tackLog.bestRec": "Recuperación más rápida",
|
|
1146
1148
|
"tackLog.badgeRet": "MEJOR RET",
|
|
1147
|
-
"tackLog.badgeRec": "
|
|
1148
|
-
"tackLog.recoverLabel": "
|
|
1149
|
+
"tackLog.badgeRec": "MÁS RÁPIDA",
|
|
1150
|
+
"tackLog.recoverLabel": "Recuperación 95% SOG:",
|
|
1149
1151
|
"tackLog.clearConfirm": "¿Vaciar todo el historial de bordos?",
|
|
1150
1152
|
// Veredicto HUD de maniobra
|
|
1151
1153
|
"maneuver.tack.title": "VIRADA",
|
|
1152
1154
|
"maneuver.jibe.title": "TRASLUCHADA",
|
|
1153
1155
|
"maneuver.verdict.good": "buena virada",
|
|
1154
1156
|
"maneuver.verdict.normal": "virada normal",
|
|
1155
|
-
"maneuver.verdict.lost": "virada
|
|
1157
|
+
"maneuver.verdict.lost": "virada pérdida",
|
|
1156
1158
|
// Viajes (Setup card + lista + detalle + share card)
|
|
1157
1159
|
"setup.trips.title": "Viajes",
|
|
1158
1160
|
"setup.trips.hint": "Cada vez que sales de puerto (navigation.state pasa de moored a otro estado) se abre un viaje. Al volver a puerto se cierra y se calcula el resumen. Toca uno para ver el detalle.",
|
|
1159
1161
|
"trips.delConfirm": "¿Eliminar este viaje?",
|
|
1160
1162
|
"trips.summaryFail": "No se pudo cargar el resumen.",
|
|
1161
|
-
"trips.captureLibFail": "No hay
|
|
1163
|
+
"trips.captureLibFail": "No hay conexión para cargar la libreria de captura.",
|
|
1162
1164
|
"trips.captureFail": "Fallo la captura:",
|
|
1163
1165
|
"trips.imageFail": "No se pudo generar la imagen.",
|
|
1164
1166
|
"trips.shareTitle": "Viaje",
|
|
@@ -1177,17 +1179,17 @@
|
|
|
1177
1179
|
"trips.status.navState": "Estado nav",
|
|
1178
1180
|
"trips.status.noneOpen": "sin viaje en curso.",
|
|
1179
1181
|
"trips.loadFail": "No se pudieron cargar los viajes.",
|
|
1180
|
-
"trips.empty": "
|
|
1181
|
-
"trips.noSummary": "Viaje sin resumen
|
|
1182
|
-
"trips.map.noConn": "Sin
|
|
1182
|
+
"trips.empty": "Aún no hay viajes registrados. Sal a navegar y al volver aparecera el resumen.",
|
|
1183
|
+
"trips.noSummary": "Viaje sin resumen aún.",
|
|
1184
|
+
"trips.map.noConn": "Sin conexión a los tiles del mapa.",
|
|
1183
1185
|
"trips.map.trackFail": "No se pudo cargar el trazado.",
|
|
1184
1186
|
"trips.map.noPositions": "Este viaje no tiene posiciones validas para dibujar.",
|
|
1185
|
-
"trips.map.legend": "⛵
|
|
1187
|
+
"trips.map.legend": "⛵ ráfaga · 🌀 vel max · 🔋 volt bajo · ↻ bordo",
|
|
1186
1188
|
"trips.card.track": "TRAYECTO",
|
|
1187
1189
|
"trips.detail.loading": "Cargando...",
|
|
1188
1190
|
"trips.detail.noSummary": "Sin resumen",
|
|
1189
1191
|
"trips.kpi.distance": "Distancia",
|
|
1190
|
-
"trips.kpi.duration": "
|
|
1192
|
+
"trips.kpi.duration": "Duración",
|
|
1191
1193
|
"trips.kpi.sogAvg": "SOG med",
|
|
1192
1194
|
"trips.kpi.sogMax": "SOG max",
|
|
1193
1195
|
"trips.kpi.twsAvg": "TWS med",
|
|
@@ -1204,38 +1206,40 @@
|
|
|
1204
1206
|
"trips.kpi.upwind": "ceñida",
|
|
1205
1207
|
"trips.kpi.reach": "traves",
|
|
1206
1208
|
"trips.kpi.downwind": "popa",
|
|
1207
|
-
"trips.hl.peakWind": "
|
|
1208
|
-
"trips.hl.peakSog": "
|
|
1209
|
-
"trips.hl.minVolt": "Voltaje
|
|
1209
|
+
"trips.hl.peakWind": "Máxima ráfaga",
|
|
1210
|
+
"trips.hl.peakSog": "Máxima velocidad",
|
|
1211
|
+
"trips.hl.minVolt": "Voltaje mínimo",
|
|
1210
1212
|
"trips.hl.checkServo": "revisar cableado servo",
|
|
1211
1213
|
"trips.hl.tack": "Bordo",
|
|
1212
1214
|
"trips.hl.at": "a las",
|
|
1213
|
-
// Banner de
|
|
1215
|
+
// Banner de restauración tras reconexion
|
|
1214
1216
|
"restore.msg": "Pypilot volvio tras {sec}s. ¿Restaurar target anterior {tgt}?",
|
|
1215
1217
|
"restore.yes": "Si, restaurar",
|
|
1216
1218
|
"restore.no": "No",
|
|
1217
1219
|
"restore.speak": "Pypilot operativo. ¿Restaurar target anterior?",
|
|
1218
1220
|
"restore.currentHdg": "rumbo actual",
|
|
1219
1221
|
"restore.doneSpeak": "Target restaurado",
|
|
1220
|
-
"restore.failSpeak": "
|
|
1221
|
-
"restore.failed": "
|
|
1222
|
-
"restore.working": "Restaurando... esperando
|
|
1222
|
+
"restore.failSpeak": "Restauración fallida",
|
|
1223
|
+
"restore.failed": "Restauración fallida",
|
|
1224
|
+
"restore.working": "Restaurando... esperando confirmación pypilot",
|
|
1223
1225
|
"restore.reason.noPy": "pypilot no confirma",
|
|
1224
1226
|
"restore.reason.noEng":"AP no enganchó",
|
|
1227
|
+
"restore.reason.modeMismatch": "el modo no cambió",
|
|
1228
|
+
"restore.reason.targetMismatch": "el target no coincide",
|
|
1225
1229
|
// Alerts / prompts / confirms
|
|
1226
1230
|
"alert.saveOk": "Guardado.",
|
|
1227
1231
|
"alert.saveFail": "Guardar fallo. Estas logueado como admin en Signal K?",
|
|
1228
1232
|
"alert.configApplied": "Config guardada. Plugin reiniciando.",
|
|
1229
|
-
"alert.configFail": "Config fallo. Inicia
|
|
1233
|
+
"alert.configFail": "Config fallo. Inicia sesión como admin en Signal K primero.",
|
|
1230
1234
|
"alert.absorb.on": "Habilitado el AutopilotProvider. IMPORTANTE: ve a SK Admin y DESACTIVA el plugin 'pypilot-autopilot-provider' para que no se peleen.",
|
|
1231
1235
|
"alert.absorb.off": "Deshabilitado el AutopilotProvider. Vuelve a activar 'pypilot-autopilot-provider' en SK Admin si quieres que WilhelmSK / freeboard sigan funcionando.",
|
|
1232
1236
|
"alert.hostRequired": "Falta host",
|
|
1233
1237
|
"alert.nudgeSaved": "Guardado. Valores de nudge actualizados.",
|
|
1234
1238
|
"confirm.removeProfile": "Borrar el perfil actual?",
|
|
1235
|
-
"confirm.restartPypilot": "Reiniciar pypilot en la TinyPilot? La
|
|
1236
|
-
"confirm.resetCfg": "Borrar toda la
|
|
1239
|
+
"confirm.restartPypilot": "Reiniciar pypilot en la TinyPilot? La conexión se cortara ~10 s mientras vuelve.",
|
|
1240
|
+
"confirm.resetCfg": "Borrar toda la configuración del visor (esquinas, flechas, polares, custom paths)? Los ajustes del piloto en la TinyPilot NO se tocan.",
|
|
1237
1241
|
"prompt.profileName": "Nombre del perfil",
|
|
1238
|
-
"prompt.pasteCfg": "Pega el JSON de
|
|
1242
|
+
"prompt.pasteCfg": "Pega el JSON de configuración exportado:",
|
|
1239
1243
|
// Setup SSH + restart status
|
|
1240
1244
|
"setup.ssh.status.emptyPwd": "Password vacio. Escribelo antes de Save (aunque no lo cambies, hay que retypearlo cada vez por seguridad).",
|
|
1241
1245
|
"setup.ssh.status.saving": "Guardando...",
|
|
@@ -1273,14 +1277,14 @@
|
|
|
1273
1277
|
"setup.restart.pypilot.sub": "~10-15 s · el AP suelta el rumbo",
|
|
1274
1278
|
"setup.restart.reboot.title": "reboot de la Pi",
|
|
1275
1279
|
"setup.restart.reboot.sub": "~35-60 s · corte total",
|
|
1276
|
-
"setup.restart.hint": "Los dos botones usan el usuario+password SSH de arriba. RESTART pypilot suelta brevemente el pilotaje; reboot de la Pi es corte total. Se muestra una confirmacion pidiendo que haya alguien al
|
|
1280
|
+
"setup.restart.hint": "Los dos botones usan el usuario+password SSH de arriba. RESTART pypilot suelta brevemente el pilotaje; reboot de la Pi es corte total. Se muestra una confirmacion pidiendo que haya alguien al timón antes de ejecutar. El restart de pypilot_web (sin interrumpir pilotaje) esta como preset en la consola de Debug de abajo.",
|
|
1277
1281
|
"setup.restart.outage.web": "Se reinicia solo el servidor web de pypilot. El core sigue pilotando durante esos segundos.",
|
|
1278
1282
|
"setup.restart.outage.pypilot": "Se reinician el core pypilot y su servidor web. El AP suelta el rumbo brevemente y se reengancha tras reconectar.",
|
|
1279
1283
|
"setup.restart.outage.reboot": "Se reinicia la Raspberry Pi entera (kernel, servicios, red). NO hay autopiloto durante ~35-60 segundos. Arranque en frio.",
|
|
1280
1284
|
"helm.outage": "Corte del piloto:",
|
|
1281
|
-
"helm.warn": "ASEGURATE DE QUE ALGUIEN ESTA AL
|
|
1285
|
+
"helm.warn": "ASEGURATE DE QUE ALGUIEN ESTA AL TIMÓN antes de continuar. No es el momento de estar bajo cubierta.",
|
|
1282
1286
|
"helm.cancel": "Cancelar",
|
|
1283
|
-
"helm.confirm": "Confirmo - hay alguien al
|
|
1287
|
+
"helm.confirm": "Confirmo - hay alguien al timón",
|
|
1284
1288
|
"helm.op.engage.title": "Enganchar piloto",
|
|
1285
1289
|
"helm.op.engage.msg": "Enganchar el piloto? Confirma que alguien vigila el rumbo.",
|
|
1286
1290
|
"helm.op.modeChange.title": "Cambiar modo AP",
|
|
@@ -1316,7 +1320,7 @@
|
|
|
1316
1320
|
"ssh.p.iw": "WiFi",
|
|
1317
1321
|
"ssh.p.temp": "Temperatura",
|
|
1318
1322
|
"ssh.p.who": "Quien esta conectado",
|
|
1319
|
-
"ssh.p.ver": "
|
|
1323
|
+
"ssh.p.ver": "Versión pypilot",
|
|
1320
1324
|
"ssh.p.restartWeb": "Reiniciar web pypilot",
|
|
1321
1325
|
// Rev88 / Rev89 / Rev90: Aproado (subir velas al viento) pseudo-modo
|
|
1322
1326
|
"aproado.modal.title": "Aproarse: subir vela",
|
|
@@ -1333,7 +1337,7 @@
|
|
|
1333
1337
|
"aproado.transit.title.bow": "APROANDOSE POR PROA",
|
|
1334
1338
|
"aproado.transit.title.stern":"APROANDOSE POR POPA",
|
|
1335
1339
|
"aproado.transit.sub": "Girando al viento...",
|
|
1336
|
-
"aproado.transit.remaining": "
|
|
1340
|
+
"aproado.transit.remaining": "aún para estar aproado",
|
|
1337
1341
|
"aproado.hud.title": "APROADO",
|
|
1338
1342
|
"aproado.hud.summary": "MANIOBRA COMPLETADA",
|
|
1339
1343
|
"aproado.hud.dist": "Dist",
|
|
@@ -2951,12 +2955,21 @@
|
|
|
2951
2955
|
if (!state.engaged) {
|
|
2952
2956
|
throwIfNotOk(await apEngage(), "engage");
|
|
2953
2957
|
}
|
|
2954
|
-
//
|
|
2955
|
-
//
|
|
2956
|
-
//
|
|
2957
|
-
//
|
|
2958
|
+
// Rev279 (audit follow-up C): the previous verdict only
|
|
2959
|
+
// required healthy + engaged after 10 s. That let a pypilot
|
|
2960
|
+
// that ignored the mode / target orders (or that a competing
|
|
2961
|
+
// controller quickly overrode) still be announced as
|
|
2962
|
+
// "restaurado". Now we ALSO compare state.mode and
|
|
2963
|
+
// state.target against the snapshot the user asked to
|
|
2964
|
+
// restore, so we speak success only when the AP is engaged
|
|
2965
|
+
// AND healthy AND actually running with the requested mode
|
|
2966
|
+
// and target (within a 3° tolerance to allow for pypilot's
|
|
2967
|
+
// own rounding on the wire).
|
|
2958
2968
|
const started = Date.now();
|
|
2959
2969
|
const maxMs = 10000;
|
|
2970
|
+
const TOL_RAD = 3 * Math.PI / 180;
|
|
2971
|
+
const wantMode = snap.mode ? String(snap.mode).toLowerCase() : null;
|
|
2972
|
+
const wantTarget = typeof snap.target === "number" ? snap.target : null;
|
|
2960
2973
|
const tick = () => {
|
|
2961
2974
|
if (state.pypilotHealthy === false) {
|
|
2962
2975
|
return verdict("fail", t("restore.reason.noPy"));
|
|
@@ -2965,6 +2978,18 @@
|
|
|
2965
2978
|
return verdict("fail", t("restore.reason.noEng"));
|
|
2966
2979
|
}
|
|
2967
2980
|
if (Date.now() - started >= maxMs) {
|
|
2981
|
+
// Final check on mode and target before speaking OK.
|
|
2982
|
+
if (wantMode) {
|
|
2983
|
+
const curMode = String(state.mode || "").toLowerCase();
|
|
2984
|
+
if (curMode !== wantMode) {
|
|
2985
|
+
return verdict("fail", t("restore.reason.modeMismatch"));
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (wantTarget !== null && typeof state.target === "number") {
|
|
2989
|
+
if (Math.abs(state.target - wantTarget) > TOL_RAD) {
|
|
2990
|
+
return verdict("fail", t("restore.reason.targetMismatch"));
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2968
2993
|
return verdict("ok");
|
|
2969
2994
|
}
|
|
2970
2995
|
setTimeout(tick, 500);
|
package/public/index.html
CHANGED
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
ANTES centro col x=8, DESPUÉS centro col x=48. -->
|
|
122
122
|
<!-- Rev224 (Carlos): rediseno con separadores finos
|
|
123
123
|
horizontales, flecha "->" entre valores ANTES y
|
|
124
|
-
|
|
124
|
+
DESPUÉS y fila nueva de TIEMPO de ejecucion.
|
|
125
125
|
AWA sin barras de valor absoluto. -->
|
|
126
126
|
<rect x="-82" y="-84" width="164" height="168" rx="10"
|
|
127
127
|
fill="rgba(11,15,20,0.98)" stroke="#4a5867" stroke-width="1.5"/>
|
|
@@ -1109,13 +1109,13 @@
|
|
|
1109
1109
|
an off-boat analysis produces an "advice" JSON that lands
|
|
1110
1110
|
back here as a "Consejos recibidos" card. -->
|
|
1111
1111
|
<div class="session-recorder-panel" style="margin-top:16px;padding-top:12px;border-top:1px solid var(--border)">
|
|
1112
|
-
<div class="section-title" style="font-size:14px;margin-bottom:4px" data-i18n="sess.title">Grabador de sesiones de
|
|
1113
|
-
<div class="hint" style="font-size:11px" data-i18n="sess.hint">Cada vez que el AP se engancha, el plugin graba un JSONL con la
|
|
1112
|
+
<div class="section-title" style="font-size:14px;margin-bottom:4px" data-i18n="sess.title">Grabador de sesiones de navegación</div>
|
|
1113
|
+
<div class="hint" style="font-size:11px" data-i18n="sess.hint">Cada vez que el AP se engancha, el plugin graba un JSONL con la telemetría. Al desenganchar cierra la sesión. Compártelo para recibir consejos de ajuste personalizados.</div>
|
|
1114
1114
|
<div class="row" style="gap:10px;flex-wrap:wrap;align-items:center;margin-top:6px">
|
|
1115
1115
|
<span id="sess-status" class="hint" style="font-size:12px;font-family:ui-monospace,monospace"></span>
|
|
1116
1116
|
</div>
|
|
1117
1117
|
<div class="row" style="gap:8px;flex-wrap:wrap;margin-top:6px">
|
|
1118
|
-
<button id="sess-tag-btn" class="ghost" data-i18n="sess.tag">Etiquetar
|
|
1118
|
+
<button id="sess-tag-btn" class="ghost" data-i18n="sess.tag">Etiquetar sesión actual</button>
|
|
1119
1119
|
<button id="sess-list-btn" class="ghost" data-i18n="sess.list">Ver sesiones grabadas</button>
|
|
1120
1120
|
<button id="sess-advice-btn" class="ghost" data-i18n="sess.advice">Consejos recibidos</button>
|
|
1121
1121
|
</div>
|
|
@@ -1328,7 +1328,7 @@
|
|
|
1328
1328
|
<button class="ssh-preset debug-preset" data-preset="iwconfig" title="iwconfig / iw dev" data-i18n="ssh.p.iw">WiFi</button>
|
|
1329
1329
|
<button class="ssh-preset debug-preset" data-preset="temp" title="/sys/.../thermal" data-i18n="ssh.p.temp">Temperatura</button>
|
|
1330
1330
|
<button class="ssh-preset debug-preset" data-preset="who" title="who / last" data-i18n="ssh.p.who">Quien esta conectado</button>
|
|
1331
|
-
<button class="ssh-preset debug-preset" data-preset="pypilot.version" title="python3 -c import pypilot" data-i18n="ssh.p.ver">
|
|
1331
|
+
<button class="ssh-preset debug-preset" data-preset="pypilot.version" title="python3 -c import pypilot" data-i18n="ssh.p.ver">Versión pypilot</button>
|
|
1332
1332
|
<button class="ssh-preset debug-preset" data-preset="restart.web" title="sv restart pypilot_web" data-i18n="ssh.p.restartWeb">Reiniciar web pypilot</button>
|
|
1333
1333
|
</div>
|
|
1334
1334
|
|
|
@@ -2097,6 +2097,6 @@
|
|
|
2097
2097
|
</div>
|
|
2098
2098
|
</div>
|
|
2099
2099
|
|
|
2100
|
-
<script src="app.js?v=
|
|
2100
|
+
<script src="app.js?v=Rev279"></script>
|
|
2101
2101
|
</body>
|
|
2102
2102
|
</html>
|