@sailingrotevista/rotevista-dash 7.0.9 → 7.0.11

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/app.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * ==========================================================================
3
- * Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
4
- * ==========================================================================
5
- * Autore: Sailing Rotevista
6
- * Motore di calcolo tattico per navigazione e crociera.
7
- * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
- * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
- * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
10
- *file app.js
11
- */
2
+ * ==========================================================================
3
+ * Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
4
+ * ==========================================================================
5
+ * Autore: Sailing Rotevista
6
+ * Motore di calcolo tattico per navigazione e crociera.
7
+ * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
+ * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
+ * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
10
+ * file app.js
11
+ */
12
12
 
13
13
  // ==========================================================================
14
14
  // 1. CONFIGURAZIONE E DEFAULT
@@ -98,17 +98,20 @@ const ui = {
98
98
  twdChevron: document.getElementById('twd-wind-chevron'),
99
99
  leewayMask: document.getElementById('leeway-mask-rect'), leewayVal: document.getElementById('leeway-val'),
100
100
  tackHdg: document.getElementById('tack-hdg'), tackCog: document.getElementById('tack-cog'),
101
- status: document.getElementById('status'), hotspot: document.getElementById('fullscreen-hotspot')
101
+ status: document.getElementById('status'), hotspot: document.getElementById('fullscreen-hotspot'),
102
+ tackLabel: document.getElementById('tack-label')
102
103
  };
103
104
 
105
+ let currentTackLabelMode = 'TACK'; // Stato dell'etichetta tattica ('TACK' o 'GYBE') con isteresi
106
+
104
107
  // ==========================================================================
105
108
  // 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA) - [Esportate in utils.js]
106
109
  // ==========================================================================
107
110
 
108
111
  /**
109
- * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
110
- * Mantiene sempre almeno 60 minuti di memoria locale per il calcolo della bussola meteo.
111
- */
112
+ * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
113
+ * Mantiene sempre almeno 60 minuti di memoria locale per il calcolo della bussola meteo.
114
+ */
112
115
  function safePush(buffer, val, time) {
113
116
  if (val === null || val === undefined || isNaN(val)) return;
114
117
 
@@ -197,16 +200,19 @@ function checkDepthAlarm(m) {
197
200
  ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
198
201
  if (m < CONFIG.alarms.depthDanger) {
199
202
  ui.depth.classList.add('alarm-danger', 'blink-alarm');
200
- playBingBing();
203
+ // Suona solo se siamo attivamente in navigazione (Harbor Silence acustico quando fermi)
204
+ if (isNavigating) {
205
+ playBingBing();
206
+ }
201
207
  } else if (m < CONFIG.alarms.depthWarning) {
202
208
  ui.depth.classList.add('alarm-warning');
203
209
  }
204
210
  }
205
211
 
206
212
  /**
207
- * computeTrueWind: Calcola TWS, TWA e TWD strategico.
208
- * Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
209
- */
213
+ * computeTrueWind: Calcola TWS, TWA e TWD strategico.
214
+ * Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
215
+ */
210
216
  function computeTrueWind() {
211
217
  const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
212
218
  const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
@@ -235,8 +241,13 @@ function computeTrueWind() {
235
241
  if (tws_water > 0.05) {
236
242
  const twa = Math.atan2(aws * Math.sin(awa), aws * Math.cos(awa) - stw);
237
243
  store.raw["environment.wind.angleTrueWater"] = twa;
244
+
245
+ // Inserimento atomico e sincronizzato di TWA e AWA nei relativi buffer mobili
246
+ // per garantire che le medie vettoriali siano perfettamente allineate in fase
238
247
  safePush(store.smoothBuf.twa, twa, now);
239
248
  safePush(store.longBuf.twa, twa, now);
249
+ safePush(store.smoothBuf.awa, awa, now);
250
+ safePush(store.longBuf.awa, awa, now);
240
251
  }
241
252
 
242
253
  // ==========================================================================
@@ -267,9 +278,9 @@ function computeTrueWind() {
267
278
  // ==========================================================================
268
279
 
269
280
  /**
270
- * Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
271
- * Più alto è il punteggio, maggiore è la priorità del sensore.
272
- */
281
+ * Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
282
+ * Più alto è il punteggio, maggiore è la priorità del sensore.
283
+ */
273
284
  function getSourcePriorityScore(sourceName) {
274
285
  if (!sourceName) return 0;
275
286
  const name = sourceName.toLowerCase();
@@ -293,6 +304,20 @@ function getSourcePriorityScore(sourceName) {
293
304
  }
294
305
 
295
306
  function processIncomingData(path, val, source, timeMs) {
307
+ // 1. Filtro anti-spike vento (> 100 nodi / 51.44 m/s)
308
+ if ((path === "environment.wind.speedApparent" || path === "environment.wind.speedTrue") && val > 51.44) {
309
+ return;
310
+ }
311
+
312
+ // 2. Filtro anti-spike velocità barca STW/SOG (> 50 nodi / 25.72 m/s)
313
+ if ((path === "navigation.speedThroughWater" || path === "navigation.speedOverGround") && val > 25.72) {
314
+ return;
315
+ }
316
+
317
+ // 3. Filtro validità profondità (ignora errori negativi e letture > 500m per lost-echo)
318
+ if (path === "environment.depth.belowTransducer" && (val < -2.0 || val > 500)) {
319
+ return;
320
+ }
296
321
  // Usiamo il tempo reale del pacchetto del server per eliminare lo sfasamento
297
322
  const now = timeMs || Date.now();
298
323
  const score = getSourcePriorityScore(source);
@@ -334,16 +359,16 @@ function processIncomingData(path, val, source, timeMs) {
334
359
  // Evita di eseguire calcoli trigonometrici, allocare oggetti in memoria dinamica
335
360
  // e popolare i buffer smoothBuf/longBuf decine di volte al secondo per singolo sensore.
336
361
  if (!lastPathProcessTimes[path]) lastPathProcessTimes[path] = 0;
337
- if (now - lastPathProcessTimes[path] < 1000) {
362
+ if (now - lastPathProcessTimes[path] < 800) {
338
363
  return; // Esce subito risparmiando cicli di calcolo del browser e batteria del tablet
339
364
  }
340
365
  lastPathProcessTimes[path] = now;
341
366
 
342
367
  // Da qui in poi, l'inserimento nei buffer fisici avviene rigorosamente a 1Hz:
343
- if (path === "environment.wind.angleApparent") {
344
- safePush(store.smoothBuf.awa, val, now);
345
- safePush(store.longBuf.awa, val, now);
346
- }
368
+ //if (path === "environment.wind.angleApparent") {
369
+ // safePush(store.smoothBuf.awa, val, now);
370
+ // safePush(store.longBuf.awa, val, now);
371
+ //}
347
372
 
348
373
  // BUG RISOLTO: Intercetta il TWD nativo e lo spinge nei buffer della bussola radar
349
374
  if (path === "environment.wind.directionTrue") {
@@ -380,6 +405,16 @@ function processIncomingData(path, val, source, timeMs) {
380
405
  if (path === "navigation.courseOverGroundTrue") {
381
406
  safePush(store.smoothBuf.cog, val, now);
382
407
  safePush(store.longBuf.cog, val, now);
408
+
409
+ // Se non è installata alcuna bussola fisica sulla rete e la barca è in movimento stabile (> 1.5 nodi),
410
+ // emuliamo la Prua usando il COG per attivare il TWD, il mini-compass e la bussola radar.
411
+ const hasCompass = store.raw["navigation.headingTrue"] !== undefined || store.raw["navigation.headingMagnetic"] !== undefined;
412
+ const sog = store.raw["navigation.speedOverGround"] || 0;
413
+ if (!hasCompass && sog > 0.77) { // 0.77 m/s = 1.5 nodi
414
+ store.raw["navigation.headingTrue"] = val;
415
+ safePush(store.smoothBuf.hdg, val, now);
416
+ safePush(store.longBuf.hdg, val, now);
417
+ }
383
418
  }
384
419
 
385
420
  const twPaths = [
@@ -435,7 +470,7 @@ function updateWindTrend() {
435
470
  // Allarme VISIVO: Se siamo in zona di pericolo poppa profonda (> 155°), accendi entrambi i LED di rosso pulsante
436
471
  if (absTwaDeg > 155) {
437
472
  if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-gybing'); gaugeDots.cw.setAttribute('fill', '#ff3b30'); }
438
- if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-gybing'); gaugeDots.ccw.setAttribute('fill', '#ff3b30'); }
473
+ if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-gybing'); }
439
474
 
440
475
  // LOGICA MACCHINA A STATI CON ISTERESI:
441
476
  // Rileviamo le mure solo se siamo fuori dalla zona cieca di poppa secca (> 170°).
@@ -510,7 +545,7 @@ function updateWindTrend() {
510
545
  const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
511
546
 
512
547
  if (twdNow && twdRef) {
513
- let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
548
+ let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
514
549
  if (Math.abs(deltaMeteo) > 6.0) {
515
550
  const isSouth = store.raw["navigation.position"]?.latitude < 0;
516
551
  let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
@@ -532,12 +567,12 @@ function updateWindTrend() {
532
567
  // ==========================================================================
533
568
 
534
569
  /**
535
- * upUI: Aggiornamento valori digitali
536
- */
570
+ * upUI: Aggiornamento valori digitali
571
+ */
537
572
  const upUI = (el, obj, instantRaw, isCompass = false) => {
538
573
  if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
539
- el.innerHTML = "---&deg;";
540
- el.classList.remove('unstable-data');
574
+ el.innerHTML = "---&deg;";
575
+ el.classList.remove('unstable-data');
541
576
  } else {
542
577
  let valDeg = Math.round(radToDeg(obj.val));
543
578
  let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "&deg;";
@@ -551,8 +586,8 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
551
586
  };
552
587
 
553
588
  /**
554
- * Loop principale di aggiornamento interfaccia (1Hz)
555
- */
589
+ * Loop principale di aggiornamento interfaccia (1Hz)
590
+ */
556
591
  function startDisplayLoop() {
557
592
  renderInterval = setInterval(() => {
558
593
  const now = Date.now();
@@ -567,43 +602,54 @@ function startDisplayLoop() {
567
602
  updateWindTrend();
568
603
 
569
604
  // --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
570
- const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
571
-
572
- if (isSocketOpen) {
573
- ui.status.className = "online"; // Colore Verde
574
- const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
575
- const requiredMs = viewportMinutes * 60000;
576
- const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
577
-
578
- if (oldestStw) {
579
- const availableMs = now - oldestStw.time;
580
- if (availableMs >= requiredMs) {
581
- ui.status.innerText = `ONLINE ${viewportMinutes}min`;
582
- } else {
583
- const availableMin = Math.max(1, Math.floor(availableMs / 60000));
584
- ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
585
- }
586
- } else {
587
- ui.status.innerText = `ONLINE`;
588
- }
605
+ const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
606
+
607
+ if (isSocketOpen) {
608
+ ui.status.className = "online"; // Colore Verde
609
+ const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
610
+ const requiredMs = viewportMinutes * 60000;
611
+ const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
612
+
613
+ if (oldestStw) {
614
+ const availableMs = now - oldestStw.time;
615
+ if (availableMs >= requiredMs) {
616
+ ui.status.innerText = `ONLINE ${viewportMinutes}min`;
589
617
  } else {
590
- ui.status.className = "offline"; // Colore Rosso
591
- ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
618
+ const availableMin = Math.max(1, Math.floor(availableMs / 60000));
619
+ ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
592
620
  }
621
+ } else {
622
+ ui.status.innerText = `ONLINE`;
623
+ }
624
+ } else {
625
+ ui.status.className = "offline"; // Colore Rosso
626
+ ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
627
+ }
593
628
 
594
629
  // --- WATCHDOG: CONTROLLO TIMEOUT ---
595
- const watch = {
596
- "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
597
- "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
598
- "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
599
- "environment.wind.speedTrue": ui.tws
600
- };
601
- for (let p in watch) {
602
- if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
603
- safeSetText(watch[p], "---"); // Sostituito innerText con la funzione protetta!
604
- delete store.raw[p];
630
+ const watch = {
631
+ "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
632
+ "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
633
+ "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
634
+ "environment.wind.speedTrue": ui.tws
635
+ };
636
+ for (let p in watch) {
637
+ if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
638
+ safeSetText(watch[p], "---"); // Sostituito innerText con la funzione protetta!
639
+ delete store.raw[p];
640
+
641
+ // Forza lo scorrimento dei dati fuori dallo schermo per i sensori in timeout
642
+ if (p === "navigation.speedThroughWater") {
643
+ refreshGraph('stw');
644
+ } else if (p === "navigation.speedOverGround") {
645
+ refreshGraph('sog');
646
+ } else if (p === "environment.depth.belowTransducer") {
647
+ refreshGraph('depth');
648
+ } else if (p === "environment.wind.speedApparent" || p === "environment.wind.speedTrue") {
649
+ refreshGraph('tws');
605
650
  }
606
651
  }
652
+ }
607
653
 
608
654
  // --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
609
655
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
@@ -642,95 +688,106 @@ function startDisplayLoop() {
642
688
  }
643
689
 
644
690
  // --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
645
- if (store.raw["environment.depth.belowTransducer"] !== undefined) {
646
- safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
647
- checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
648
- manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
649
- }
691
+ if (store.raw["environment.depth.belowTransducer"] !== undefined) {
692
+ safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
693
+ checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
694
+ manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
695
+ }
650
696
 
651
- // --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
652
-
653
- // Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
654
- const rawTws = store.raw["environment.wind.speedTrue"];
655
- const rawAws = store.raw["environment.wind.speedApparent"];
697
+ // --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
656
698
 
657
- const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
658
- const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
659
-
660
- if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
661
- if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
662
-
663
- // Disegno testo casella destra (TWS o AWS)
664
- if (rawTws !== undefined || rawAws !== undefined) {
665
- const labelWind = document.getElementById('tws-aws-label');
666
- const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
667
-
668
- safeSetText(ui.tws, currentWind.toFixed(1));
669
- if (labelWind) labelWind.textContent = displayModeTws;
670
-
671
- if (currentWind >= CONFIG.graphs.reef2) {
672
- ui.tws.style.setProperty('color', '#ff3b30', 'important');
673
- } else if (currentWind >= CONFIG.graphs.reef1) {
674
- ui.tws.style.setProperty('color', '#ff9800', 'important');
675
- } else {
676
- if (displayModeTws === 'AWS') {
677
- ui.tws.style.setProperty('color', '#5c6bc0', 'important');
678
- } else {
679
- const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
680
- ui.tws.style.setProperty('color', navyNight, 'important');
681
- }
682
- }
699
+ // Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
700
+ const rawTws = store.raw["environment.wind.speedTrue"];
701
+ const rawAws = store.raw["environment.wind.speedApparent"];
702
+
703
+ const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
704
+ const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
705
+
706
+ if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
707
+ if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
708
+
709
+ // Disegno testo casella destra (TWS o AWS)
710
+ if (rawTws !== undefined || rawAws !== undefined) {
711
+ const labelWind = document.getElementById('tws-aws-label');
712
+ const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
713
+
714
+ safeSetText(ui.tws, currentWind.toFixed(1));
715
+ if (labelWind) labelWind.textContent = displayModeTws;
716
+
717
+ if (currentWind >= CONFIG.graphs.reef2) {
718
+ ui.tws.style.setProperty('color', '#ff3b30', 'important');
719
+ } else if (currentWind >= CONFIG.graphs.reef1) {
720
+ ui.tws.style.setProperty('color', '#ff9800', 'important');
721
+ } else {
722
+ if (displayModeTws === 'AWS') {
723
+ ui.tws.style.setProperty('color', '#5c6bc0', 'important');
724
+ } else {
725
+ const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
726
+ ui.tws.style.setProperty('color', navyNight, 'important');
683
727
  }
728
+ }
729
+ }
684
730
 
685
731
  // --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
686
- if (activeInstrument === 'gauge') {
687
- updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
688
- }
689
-
690
- // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
691
- if (lastAvgUIUpdate++ % 10 === 0) {
692
- saveDashboardState();
693
- }
732
+ if (activeInstrument === 'gauge') {
733
+ updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
734
+ }
735
+
736
+ // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
737
+ if (lastAvgUIUpdate++ % 10 === 0) {
738
+ saveDashboardState();
739
+ }
694
740
 
695
- if (lastAvgUIUpdate % 3 === 0) {
696
- let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
697
- let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
698
- let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
699
- let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
700
- let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
701
-
702
- upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
703
- upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
704
- upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
705
- upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
706
- upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
707
-
708
- if (hObj && twdObj) {
709
- const reflectAngle = (targetRad, axisRad) => {
710
- const dS = Math.sin(axisRad - targetRad);
711
- const dC = Math.cos(axisRad - targetRad);
712
- return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
713
- };
714
- const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
715
- if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
716
- else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
717
- else {
718
- const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
719
- ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
720
- ui.tackHdg.classList.remove('unstable-data');
721
- }
722
- if (cObj) {
723
- const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
724
- if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
725
- else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
726
- else {
727
- const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
728
- ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
729
- ui.tackCog.classList.remove('unstable-data');
730
- }
731
- }
741
+ if (lastAvgUIUpdate % 3 === 0) {
742
+ let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
743
+ let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
744
+ let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
745
+ let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
746
+ let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
747
+
748
+ // --- GESTIONE DINAMICA ETICHETTA TACK/GYBE CON ISTERESI DI 10 GRADI ---
749
+ if (ui.tackLabel) {
750
+ const absTwa = Math.abs(radToDeg(store.raw["environment.wind.angleTrueWater"] || 0));
751
+ if (currentTackLabelMode === 'TACK' && absTwa > 95) {
752
+ currentTackLabelMode = 'GYBE';
753
+ } else if (currentTackLabelMode === 'GYBE' && absTwa < 85) {
754
+ currentTackLabelMode = 'TACK';
755
+ }
756
+ safeSetText(ui.tackLabel, currentTackLabelMode);
757
+ }
758
+
759
+ upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
760
+ upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
761
+ upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
762
+ upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
763
+ upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
764
+
765
+ if (hObj && twdObj) {
766
+ const reflectAngle = (targetRad, axisRad) => {
767
+ const dS = Math.sin(axisRad - targetRad);
768
+ const dC = Math.cos(axisRad - targetRad);
769
+ return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
770
+ };
771
+ const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
772
+ if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
773
+ else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
774
+ else {
775
+ const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
776
+ ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
777
+ ui.tackHdg.classList.remove('unstable-data');
778
+ }
779
+ if (cObj) {
780
+ const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
781
+ if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
782
+ else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
783
+ else {
784
+ const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
785
+ ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
786
+ ui.tackCog.classList.remove('unstable-data');
732
787
  }
733
788
  }
789
+ }
790
+ }
734
791
  }, RENDER_INTERVAL_MS);
735
792
  }
736
793
 
@@ -741,8 +798,8 @@ function startDisplayLoop() {
741
798
  let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
742
799
 
743
800
  /**
744
- * Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
745
- */
801
+ * Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
802
+ */
746
803
  function getApiUrl(path) {
747
804
  if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
748
805
  return `http://${CONFIG.server.fallbackIp}${path}`;
@@ -751,8 +808,8 @@ function getApiUrl(path) {
751
808
  }
752
809
 
753
810
  /**
754
- * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
755
- */
811
+ * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
812
+ */
756
813
  function applyConfigData(data) {
757
814
  Object.assign(CONFIG.alarms, data.alarms || {});
758
815
  Object.assign(CONFIG.graphs, data.graphs || {});
@@ -772,8 +829,8 @@ function applyConfigData(data) {
772
829
  }
773
830
 
774
831
  /**
775
- * Recupera la configurazione iniziale al caricamento della pagina
776
- */
832
+ * Recupera la configurazione iniziale al caricamento della pagina
833
+ */
777
834
  async function fetchServerConfig() {
778
835
  try {
779
836
  const response = await fetch(getApiUrl('/rotevista-config'));
@@ -791,8 +848,8 @@ async function fetchServerConfig() {
791
848
  }
792
849
 
793
850
  /**
794
- * Watchdog: Controlla in background se le impostazioni sul server sono cambiate
795
- */
851
+ * Watchdog: Controlla in background se le impostazioni sul server sono cambiate
852
+ */
796
853
  async function watchConfigChanges() {
797
854
  try {
798
855
  const response = await fetch(getApiUrl('/rotevista-config'));
@@ -822,8 +879,8 @@ async function watchConfigChanges() {
822
879
  }
823
880
 
824
881
  /**
825
- * Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
826
- */
882
+ * Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
883
+ */
827
884
  async function fetchServerHistory() {
828
885
  try {
829
886
  const response = await fetch(getApiUrl('/rotevista-history'));
@@ -862,12 +919,12 @@ async function fetchServerHistory() {
862
919
  }
863
920
 
864
921
  /**
865
- * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
866
- * Integrazioni:
867
- * 1. Strict undefined check per lastUpdates.
868
- * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
869
- * 3. Clamping di sicurezza (no negativi, no Infinity).
870
- */
922
+ * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
923
+ * Integrazioni:
924
+ * 1. Strict undefined check per lastUpdates.
925
+ * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
926
+ * 3. Clamping di sicurezza (no negativi, no Infinity).
927
+ */
871
928
  function manageHistory(type, value) {
872
929
  // --- 1. VALIDAZIONE INPUT RIGOROSA ---
873
930
  if (value === undefined || value === null || !isFinite(value)) return;
@@ -1133,50 +1190,50 @@ async function init() {
1133
1190
  initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
1134
1191
 
1135
1192
  // 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
1136
- const twdBox = document.querySelector('.box-twd');
1137
- if (twdBox) {
1138
- let twdPressTimer = null;
1139
- let longPressTriggered = false; // Flag di controllo della pressione prolungata
1140
-
1141
- twdBox.addEventListener('pointerdown', (e) => {
1142
- longPressTriggered = false;
1143
- if (activeInstrument === 'gauge') {
1144
- twdPressTimer = setTimeout(() => {
1145
- activeInstrument = 'radar';
1146
- document.getElementById('wind-gauge').style.display = 'none';
1147
- document.getElementById('wind-radar').style.display = 'block';
1148
- renderRadar(); // Disegna immediatamente il radar all'attivazione
1149
- twdPressTimer = null;
1150
- longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
1151
- }, 1000);
1152
- }
1153
- });
1154
- twdBox.addEventListener('pointerup', () => {
1155
- if (activeInstrument === 'gauge') {
1156
- if (twdPressTimer) {
1157
- clearTimeout(twdPressTimer);
1158
- twdPressTimer = null;
1159
- }
1160
- } else if (activeInstrument === 'radar') {
1161
- if (longPressTriggered) {
1162
- // Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
1163
- longPressTriggered = false;
1164
- } else {
1165
- // Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
1166
- activeInstrument = 'gauge';
1167
- document.getElementById('wind-radar').style.display = 'none';
1168
- document.getElementById('wind-gauge').style.display = 'block';
1169
- }
1170
- }
1171
- });
1172
- twdBox.addEventListener('pointerleave', () => {
1193
+ const twdBox = document.querySelector('.box-twd');
1194
+ if (twdBox) {
1195
+ let twdPressTimer = null;
1196
+ let longPressTriggered = false; // Flag di controllo della pressione prolungata
1197
+
1198
+ twdBox.addEventListener('pointerdown', (e) => {
1199
+ longPressTriggered = false;
1200
+ if (activeInstrument === 'gauge') {
1201
+ twdPressTimer = setTimeout(() => {
1202
+ activeInstrument = 'radar';
1203
+ document.getElementById('wind-gauge').style.display = 'none';
1204
+ document.getElementById('wind-radar').style.display = 'block';
1205
+ renderRadar(); // Disegna immediatamente il radar all'attivazione
1206
+ twdPressTimer = null;
1207
+ longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
1208
+ }, 1000);
1209
+ }
1210
+ });
1211
+ twdBox.addEventListener('pointerup', () => {
1212
+ if (activeInstrument === 'gauge') {
1173
1213
  if (twdPressTimer) {
1174
1214
  clearTimeout(twdPressTimer);
1175
1215
  twdPressTimer = null;
1176
1216
  }
1177
- longPressTriggered = false;
1178
- });
1179
- }
1217
+ } else if (activeInstrument === 'radar') {
1218
+ if (longPressTriggered) {
1219
+ // Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
1220
+ longPressTriggered = false;
1221
+ } else {
1222
+ // Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
1223
+ activeInstrument = 'gauge';
1224
+ document.getElementById('wind-radar').style.display = 'none';
1225
+ document.getElementById('wind-gauge').style.display = 'block';
1226
+ }
1227
+ }
1228
+ });
1229
+ twdBox.addEventListener('pointerleave', () => {
1230
+ if (twdPressTimer) {
1231
+ clearTimeout(twdPressTimer);
1232
+ twdPressTimer = null;
1233
+ }
1234
+ longPressTriggered = false;
1235
+ });
1236
+ }
1180
1237
 
1181
1238
  // 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
1182
1239
  const windRadarSvg = document.getElementById('wind-radar');
@@ -1239,7 +1296,7 @@ document.addEventListener('visibilitychange', () => {
1239
1296
  // Se era già chiuso o in errore, proviamo a riconnettere subito
1240
1297
  connect();
1241
1298
  } else {
1242
- // Se risulta "OPEN" ma potrebbe essere una connessione fantasma,
1299
+ // Se resulta "OPEN" ma potrebbe essere una connessione fantasma,
1243
1300
  // la chiudiamo forzatamente per scatenare la riconnessione pulita e il download della cronologia
1244
1301
  console.log("🔌 [Watchdog] Riavvio precauzionale del WebSocket per evitare connessioni fantasma.");
1245
1302
  socket.close();