@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 +260 -203
- package/charts.js +7 -4
- package/index.html +2 -2
- package/index.js +697 -642
- package/package.json +1 -1
- package/weather-radar.js +2 -1
package/app.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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] <
|
|
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
|
-
|
|
345
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
540
|
-
|
|
574
|
+
el.innerHTML = "---°";
|
|
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) + "°";
|
|
@@ -551,8 +586,8 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
|
551
586
|
};
|
|
552
587
|
|
|
553
588
|
/**
|
|
554
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
591
|
-
ui.status.innerText =
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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 = "---°";
|
|
773
|
+
else if (unstableH) { ui.tackHdg.innerHTML = "---°"; 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')}°`;
|
|
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 = "---°";
|
|
782
|
+
else if (unstableC) { ui.tackCog.innerHTML = "---°"; 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')}°`;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
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
|
|
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();
|