@sailingrotevista/rotevista-dash 5.0.5 → 5.0.7
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 +292 -101
- package/debug.html +104 -56
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ==========================================================================
|
|
3
|
-
* Signal K Wind Dashboard - Pro Version 3.
|
|
3
|
+
* Signal K Wind Dashboard - Pro Version 3.8 (Dynamic Envelope Architecture)
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Autore: Sailing Rotevista
|
|
6
6
|
* Motore di calcolo tattico per navigazione e crociera.
|
|
@@ -24,19 +24,18 @@ let CONFIG = {
|
|
|
24
24
|
},
|
|
25
25
|
graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
|
|
26
26
|
scales: {
|
|
27
|
-
stw: { stdMax: 4, hercSpan:
|
|
28
|
-
sog: { stdMax: 4, hercSpan:
|
|
29
|
-
tws: { stdMax: 15, hercSpan:
|
|
30
|
-
depth: { stdMax: 8, hercSpan:
|
|
27
|
+
stw: { stdMax: 4, hercSpan: 2, step: 1 },
|
|
28
|
+
sog: { stdMax: 4, hercSpan: 2, step: 1 },
|
|
29
|
+
tws: { stdMax: 15, hercSpan: 2, step: 1 },
|
|
30
|
+
depth: { stdMax: 8, hercSpan: 1, step: 1 }
|
|
31
31
|
},
|
|
32
32
|
server: { fallbackIp: "192.168.111.240:3000" }
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const RENDER_INTERVAL_MS = 1000;
|
|
36
|
-
const TIMEOUT_MS =
|
|
36
|
+
const TIMEOUT_MS = 15000;
|
|
37
37
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
38
|
-
const DASH_VERSION = "3.
|
|
39
|
-
const sourceLocks = {};
|
|
38
|
+
const DASH_VERSION = "3.8"; // Major Update: Smart Source Locking & Breathing Hercules Scale
|
|
40
39
|
|
|
41
40
|
// ==========================================================================
|
|
42
41
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
@@ -59,6 +58,8 @@ let pressTimer, isFocusActive = false;
|
|
|
59
58
|
let emaTwaSin = 0;
|
|
60
59
|
let emaTwaCos = 0;
|
|
61
60
|
let firstEmaRun = true;
|
|
61
|
+
// MACCHINA A STATI ALLARME STRAMBATA (Filtro Isteresi Anti-Brandeggio)
|
|
62
|
+
let lastGybeSide = null;
|
|
62
63
|
|
|
63
64
|
const graphModes = {
|
|
64
65
|
stw: 'standard',
|
|
@@ -67,13 +68,17 @@ const graphModes = {
|
|
|
67
68
|
depth: 'standard'
|
|
68
69
|
};
|
|
69
70
|
|
|
71
|
+
// GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
|
|
72
|
+
const sourceLocks = {};
|
|
73
|
+
|
|
70
74
|
// Database centrale dello store dati
|
|
71
75
|
const store = {
|
|
72
76
|
raw: {},
|
|
73
77
|
timestamps: {},
|
|
78
|
+
depthProtectedActive: false, // Memoria per lo stato della protezione profondità
|
|
79
|
+
herculesScales: {}, // Memoria per i limiti attivi della modalità Hercules
|
|
74
80
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
75
81
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
76
|
-
// histories ora conterrà array di oggetti: { time: Date.now(), val: numero }
|
|
77
82
|
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
78
83
|
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
79
84
|
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
|
|
@@ -326,14 +331,62 @@ function computeTrueWind() {
|
|
|
326
331
|
}
|
|
327
332
|
}
|
|
328
333
|
|
|
334
|
+
// ==========================================================================
|
|
335
|
+
// GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
|
|
336
|
+
// ==========================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
|
|
340
|
+
* Più alto è il punteggio, maggiore è la priorità del sensore.
|
|
341
|
+
*/
|
|
342
|
+
function getSourcePriorityScore(sourceName) {
|
|
343
|
+
if (!sourceName) return 0;
|
|
344
|
+
const name = sourceName.toLowerCase();
|
|
345
|
+
|
|
346
|
+
// TIER 3: AIS / Trasmissioni lente (es. yacht_device.AI, VDO)
|
|
347
|
+
if (name.includes('.ai') || name.includes('ais') || name.includes('vdo')) {
|
|
348
|
+
return 10;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// TIER 2: GPS USB del Cerbo GX / Victron / Sistemi locali di servizio
|
|
352
|
+
if (name.includes('venus') || name.includes('victron') || name.includes('ttyacm') || name.includes('ttyusb') || name.includes('system')) {
|
|
353
|
+
return 50;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// TIER 1: Strumentazione ufficiale di navigazione (NMEA 2000, Yacht Devices GP/YD/AP, Gateway, ecc.)
|
|
357
|
+
if (name.includes('yacht_device') || name.includes('n2k') || name.includes('actisense') || name.includes('can') || name.includes('nmea') || name.includes('raymarine') || name.includes('garmin') || name.includes('simrad') || name.includes('b&g')) {
|
|
358
|
+
return 100;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return 30; // Punteggio standard per sorgenti sconosciute
|
|
362
|
+
}
|
|
363
|
+
|
|
329
364
|
function processIncomingData(path, val, source) {
|
|
330
365
|
const now = Date.now();
|
|
366
|
+
const score = getSourcePriorityScore(source);
|
|
331
367
|
|
|
332
|
-
//
|
|
333
|
-
if (!sourceLocks[path]
|
|
334
|
-
|
|
368
|
+
// Gestione dello Smart Lock
|
|
369
|
+
if (!sourceLocks[path]) {
|
|
370
|
+
// Nessun blocco attivo: aggancia la sorgente corrente
|
|
371
|
+
sourceLocks[path] = { label: source, score: score, lastSeen: now };
|
|
335
372
|
} else {
|
|
336
|
-
|
|
373
|
+
const currentLock = sourceLocks[path];
|
|
374
|
+
const isSameSource = (currentLock.label === source);
|
|
375
|
+
const isLockExpired = (now - currentLock.lastSeen > 12000); // Scadenza a 12 secondi per tollerare il GPS dello Yacht Devices
|
|
376
|
+
const hasHigherPriority = (score > currentLock.score);
|
|
377
|
+
|
|
378
|
+
if (isSameSource) {
|
|
379
|
+
// Stessa sorgente: aggiorna il timestamp e mantiene il blocco
|
|
380
|
+
currentLock.lastSeen = now;
|
|
381
|
+
currentLock.score = score;
|
|
382
|
+
} else if (isLockExpired || hasHigherPriority) {
|
|
383
|
+
// Ruba il blocco se la sorgente precedente è scaduta o se questa ha priorità superiore
|
|
384
|
+
sourceLocks[path] = { label: source, score: score, lastSeen: now };
|
|
385
|
+
console.log(`🔌 [Smart Lock] Path "${path}" switched to: ${source} (Score: ${score})`);
|
|
386
|
+
} else {
|
|
387
|
+
// Rifiuta i dati da sorgenti a priorità inferiore se quella principale è attiva
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
337
390
|
}
|
|
338
391
|
|
|
339
392
|
store.timestamps[path] = now;
|
|
@@ -373,57 +426,16 @@ function processIncomingData(path, val, source) {
|
|
|
373
426
|
// ==========================================================================
|
|
374
427
|
function updateWindTrend() {
|
|
375
428
|
const now = Date.now();
|
|
429
|
+
|
|
430
|
+
// --- 6.1 TREND TATTICO (AWA/TWA) ---
|
|
376
431
|
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
|
|
377
432
|
const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true);
|
|
378
|
-
|
|
379
|
-
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
380
|
-
const multiplier = isNavigating ? 1 : 2;
|
|
381
|
-
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
382
|
-
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
383
|
-
|
|
384
|
-
if (!twaNow || !twaRef || !twdNow || !twdRef) return;
|
|
385
|
-
|
|
386
|
-
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
387
433
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
388
434
|
|
|
389
|
-
//
|
|
390
|
-
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
391
|
-
if (Math.abs(deltaMeteo) > 6.0) {
|
|
392
|
-
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
393
|
-
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
|
|
394
|
-
if (deltaMeteo > 0) {
|
|
395
|
-
compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor);
|
|
396
|
-
compassDots.ccw.classList.remove('is-trending');
|
|
397
|
-
} else {
|
|
398
|
-
compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor);
|
|
399
|
-
compassDots.cw.classList.remove('is-trending');
|
|
400
|
-
}
|
|
401
|
-
} else { [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
402
|
-
|
|
403
|
-
// TREND TATTICO
|
|
404
|
-
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
405
|
-
const curTwaDeg = radToDeg(twaNow.val);
|
|
406
|
-
if (Math.abs(deltaTac) > 3.0) {
|
|
407
|
-
let absTwa = Math.abs(curTwaDeg);
|
|
408
|
-
let tacticColor;
|
|
409
|
-
if (absTwa > 75 && absTwa < 105) {
|
|
410
|
-
tacticColor = "#bbb";
|
|
411
|
-
} else {
|
|
412
|
-
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
413
|
-
if (absTwa >= 90) isLift = !isLift;
|
|
414
|
-
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
415
|
-
}
|
|
416
|
-
if (deltaTac > 0) {
|
|
417
|
-
gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor);
|
|
418
|
-
gaugeDots.ccw.classList.remove('is-trending');
|
|
419
|
-
} else {
|
|
420
|
-
gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor);
|
|
421
|
-
gaugeDots.cw.classList.remove('is-trending');
|
|
422
|
-
}
|
|
423
|
-
} else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
424
|
-
|
|
425
|
-
// ALLARME STRAMBATA (EMA FILTERED)
|
|
435
|
+
// --- 6.2 ALLARME STRAMBATA CON ISTERESI (MACCHINA A STATI ANTI-BRANDEGGIO) ---
|
|
426
436
|
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
437
|
+
let smoothedTwaDeg = null;
|
|
438
|
+
|
|
427
439
|
if (instTwaRad !== undefined) {
|
|
428
440
|
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
429
441
|
const currentSin = Math.sin(instTwaRad);
|
|
@@ -436,19 +448,98 @@ function updateWindTrend() {
|
|
|
436
448
|
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
437
449
|
}
|
|
438
450
|
|
|
439
|
-
|
|
451
|
+
smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
452
|
+
const absTwaDeg = Math.abs(smoothedTwaDeg);
|
|
453
|
+
|
|
454
|
+
// Allarme VISIVO: Se siamo in zona di pericolo poppa profonda (> 155°), accendi entrambi i LED di rosso pulsante
|
|
455
|
+
if (absTwaDeg > 155) {
|
|
456
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-gybing'); gaugeDots.cw.setAttribute('fill', '#ff3b30'); }
|
|
457
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-gybing'); gaugeDots.ccw.setAttribute('fill', '#ff3b30'); }
|
|
458
|
+
|
|
459
|
+
// LOGICA MACCHINA A STATI CON ISTERESI:
|
|
460
|
+
// Rileviamo le mure solo se siamo fuori dalla zona cieca di poppa secca (> 170°).
|
|
461
|
+
// Se oscilliamo tra -175° e +178° (brandeggio), lastGybeSide NON cambia e l'allarme acustico tace.
|
|
462
|
+
let currentTack = null;
|
|
463
|
+
if (smoothedTwaDeg > 155 && smoothedTwaDeg < 170) {
|
|
464
|
+
currentTack = 'starboard';
|
|
465
|
+
} else if (smoothedTwaDeg < -155 && smoothedTwaDeg > -170) {
|
|
466
|
+
currentTack = 'port';
|
|
467
|
+
}
|
|
440
468
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
469
|
+
if (currentTack !== null) {
|
|
470
|
+
if (lastGybeSide === null) {
|
|
471
|
+
// Inizializzazione al primo ingresso nella zona di controllo
|
|
472
|
+
lastGybeSide = currentTack;
|
|
473
|
+
} else if (lastGybeSide !== currentTack) {
|
|
474
|
+
// Abbiamo eseguito una vera strambata stabile e siamo usciti dalla zona di poppa secca!
|
|
475
|
+
lastGybeSide = currentTack;
|
|
476
|
+
|
|
477
|
+
// Attivazione allarme acustico con blocco temporale di sicurezza (60 secondi)
|
|
478
|
+
if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
|
|
479
|
+
lastGybeAlarmTime = now;
|
|
480
|
+
playGybeAlarm();
|
|
481
|
+
console.log(`⚠️ GYBE ALARM TRIGGERED: Tack switched to ${currentTack} (TWA: ${smoothedTwaDeg.toFixed(1)}°)`);
|
|
482
|
+
}
|
|
447
483
|
}
|
|
448
484
|
}
|
|
485
|
+
} else {
|
|
486
|
+
// Se usciamo dalla poppa profonda (< 155°), disattiva l'allarme visivo e resetta lo stato delle mure
|
|
487
|
+
if (gaugeDots.cw) gaugeDots.cw.classList.remove('is-gybing');
|
|
488
|
+
if (gaugeDots.ccw) gaugeDots.ccw.classList.remove('is-gybing');
|
|
489
|
+
lastGybeSide = null; // Reset per la prossima poppa
|
|
449
490
|
}
|
|
450
491
|
lastInstantTwa = smoothedTwaDeg;
|
|
451
492
|
}
|
|
493
|
+
|
|
494
|
+
// Gestione normale dei Trend Tattici (Lifts/Headers) se NON siamo in allarme strambata
|
|
495
|
+
if (twaNow && twaRef && (smoothedTwaDeg === null || Math.abs(smoothedTwaDeg) <= 155)) {
|
|
496
|
+
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
497
|
+
const curTwaDeg = radToDeg(twaNow.val);
|
|
498
|
+
if (Math.abs(deltaTac) > 3.0) {
|
|
499
|
+
let absTwa = Math.abs(curTwaDeg);
|
|
500
|
+
let tacticColor;
|
|
501
|
+
if (absTwa > 75 && absTwa < 105) {
|
|
502
|
+
tacticColor = "#bbb";
|
|
503
|
+
} else {
|
|
504
|
+
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
505
|
+
if (absTwa >= 90) isLift = !isLift;
|
|
506
|
+
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
507
|
+
}
|
|
508
|
+
if (deltaTac > 0) {
|
|
509
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
510
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); }
|
|
511
|
+
} else {
|
|
512
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
|
|
513
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); }
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
[gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// --- 6.3 TREND METEO STRATEGICO (TWD) ---
|
|
521
|
+
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
522
|
+
const multiplier = isNavigating ? 1 : 2;
|
|
523
|
+
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
524
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
525
|
+
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
526
|
+
|
|
527
|
+
if (twdNow && twdRef) {
|
|
528
|
+
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
529
|
+
if (Math.abs(deltaMeteo) > 6.0) {
|
|
530
|
+
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
531
|
+
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
|
|
532
|
+
if (deltaMeteo > 0) {
|
|
533
|
+
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
534
|
+
if (compassDots.ccw) { compassDots.ccw.classList.remove('is-trending'); }
|
|
535
|
+
} else {
|
|
536
|
+
if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
|
|
537
|
+
if (compassDots.cw) { compassDots.cw.classList.remove('is-trending'); }
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
[compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
452
543
|
}
|
|
453
544
|
|
|
454
545
|
// ==========================================================================
|
|
@@ -487,6 +578,9 @@ function startDisplayLoop() {
|
|
|
487
578
|
|
|
488
579
|
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
489
580
|
|
|
581
|
+
// --- CALCOLO TREND VENTO & ALLARME STRAMBATA ---
|
|
582
|
+
updateWindTrend();
|
|
583
|
+
|
|
490
584
|
// --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
|
|
491
585
|
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
492
586
|
const requiredMs = viewportMinutes * 60000;
|
|
@@ -766,7 +860,6 @@ function manageHistory(type, value) {
|
|
|
766
860
|
const tempBuf = store.graphTempBuf[type];
|
|
767
861
|
|
|
768
862
|
// --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
|
|
769
|
-
// Ignora cadute a zero se il valore precedente era superiore al 50% del primo Reef
|
|
770
863
|
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
771
864
|
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
772
865
|
const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
|
|
@@ -814,11 +907,10 @@ function manageHistory(type, value) {
|
|
|
814
907
|
}
|
|
815
908
|
|
|
816
909
|
// --- 6. CLAMPING E VALIDAZIONE FINALE ---
|
|
817
|
-
// Protezione contro valori negativi (fisicamente impossibili per questi dati) e non finiti
|
|
818
910
|
if (!isFinite(finalValue)) return;
|
|
819
911
|
finalValue = Math.max(0, finalValue);
|
|
820
912
|
|
|
821
|
-
// --- 7. STORAGE STORICO
|
|
913
|
+
// --- 7. STORAGE STORICO ---
|
|
822
914
|
store.histories[type].push({ val: finalValue, time: now });
|
|
823
915
|
|
|
824
916
|
// --- 8. PRUNING DINAMICO ---
|
|
@@ -833,13 +925,126 @@ function manageHistory(type, value) {
|
|
|
833
925
|
store.graphTempBuf[type] = [];
|
|
834
926
|
store.lastUpdates[type] = now;
|
|
835
927
|
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Gestione dinamica delle scale dei grafici (Involucro Elastico e Safety Zoom)
|
|
931
|
+
* Implementa la logica di sicurezza disaccoppiata a 2 minuti per la profondità.
|
|
932
|
+
*/
|
|
836
933
|
function calculateScale(type, data, mode) {
|
|
837
|
-
const s = CONFIG.scales[type];
|
|
934
|
+
const s = CONFIG.scales[type];
|
|
935
|
+
const currentVal = data[data.length - 1];
|
|
936
|
+
|
|
937
|
+
// Fallback di emergenza se il buffer è momentaneamente vuoto
|
|
938
|
+
if (currentVal === undefined || currentVal === null) {
|
|
939
|
+
return { min: 0, max: s ? s.stdMax : 10 };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Inizializzazione della memoria delle scale nello store se non esiste
|
|
943
|
+
if (!store.herculesScales) store.herculesScales = {};
|
|
944
|
+
if (!store.herculesScales[type]) {
|
|
945
|
+
store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
|
|
946
|
+
}
|
|
947
|
+
let currentScale = store.herculesScales[type];
|
|
948
|
+
|
|
949
|
+
// ==========================================================================
|
|
950
|
+
// SEZIONE PROFONDITÀ (REGOLA DI SICUREZZA DISACCOPPIATA A 2 MINUTI)
|
|
951
|
+
// ==========================================================================
|
|
952
|
+
if (type === 'depth') {
|
|
953
|
+
const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
|
|
954
|
+
if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
|
|
955
|
+
|
|
956
|
+
const now = Date.now();
|
|
957
|
+
const depthSafetyWindowMs = 120000; // 2 minuti di stabilizzazione fissi per la sicurezza
|
|
958
|
+
|
|
959
|
+
// Estrazione dati reali degli ultimi 2 minuti con timestamp
|
|
960
|
+
const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
|
|
961
|
+
const recentVals = recentPoints.map(p => p.val);
|
|
962
|
+
|
|
963
|
+
const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
|
|
964
|
+
const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
|
|
965
|
+
|
|
966
|
+
// --- NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
|
|
967
|
+
if (mode !== 'hercules') {
|
|
968
|
+
// Entrata istantanea sotto lo Standard Max (sicurezza immediata)
|
|
969
|
+
if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
|
|
970
|
+
store.depthProtectedActive = true;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Uscita ritardata: usciamo solo se il minimo degli ultimi 2 minuti è sopra soglia
|
|
974
|
+
if (store.depthProtectedActive) {
|
|
975
|
+
if (localMin > shallowThreshold) {
|
|
976
|
+
store.depthProtectedActive = false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (store.depthProtectedActive) {
|
|
981
|
+
return { min: 0, max: shallowThreshold }; // Blocco a [0 - 20m]
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Se siamo fuori, scala dinamica normale basata sull'intero buffer passato
|
|
985
|
+
const maxHistorico = Math.max(...data);
|
|
986
|
+
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// --- HERCULES PROFONDITÀ (0 IN BASSO, ZOOM SUL MASSIMO DEI 2 MINUTI) ---
|
|
990
|
+
if (mode === 'hercules') {
|
|
991
|
+
const padding = 1.0; // 1 metro di margine sopra il fondo
|
|
992
|
+
|
|
993
|
+
// Calcoliamo i limiti ideali basati solo sugli ultimi 2 minuti
|
|
994
|
+
let targetMin = 0;
|
|
995
|
+
let targetMax = Math.ceil(localMax + padding);
|
|
996
|
+
|
|
997
|
+
// Impediamo una scala troppo stretta (minimo 4 metri di range totale per sicurezza)
|
|
998
|
+
const absoluteMinSpan = 4;
|
|
999
|
+
if (targetMax < absoluteMinSpan) {
|
|
1000
|
+
targetMax = absoluteMinSpan;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Regola asimmetrica di aggiornamento
|
|
1004
|
+
if (currentVal > currentScale.max) {
|
|
1005
|
+
// Se andiamo verso il fondo profondo, allarghiamo istantaneamente
|
|
1006
|
+
currentScale.max = targetMax;
|
|
1007
|
+
} else {
|
|
1008
|
+
// Stringiamo lo zoom solo se tutti i dati degli ultimi 2 minuti sono inferiori al target
|
|
1009
|
+
const allStableInTarget = recentVals.every(val => val <= targetMax);
|
|
1010
|
+
if (allStableInTarget) {
|
|
1011
|
+
currentScale.max = targetMax;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
currentScale.min = 0;
|
|
1016
|
+
return { min: currentScale.min, max: currentScale.max };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ==========================================================================
|
|
1021
|
+
// ALTRI GRAFICI (STW, SOG, TWS): MANTENGONO IL COMPORTAMENTO ORIGINALE
|
|
1022
|
+
// ==========================================================================
|
|
1023
|
+
let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
1024
|
+
|
|
838
1025
|
if (mode === 'hercules') {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1026
|
+
const padding = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
|
|
1027
|
+
|
|
1028
|
+
let min = Math.max(0, Math.floor(aMin - padding));
|
|
1029
|
+
let max = Math.ceil(aMax + padding);
|
|
1030
|
+
|
|
1031
|
+
// Garantisce lo span minimo
|
|
1032
|
+
const currentSpan = max - min;
|
|
1033
|
+
if (currentSpan < s.hercSpan) {
|
|
1034
|
+
const diff = s.hercSpan - currentSpan;
|
|
1035
|
+
min = Math.max(0, min - Math.floor(diff / 2));
|
|
1036
|
+
max = min + s.hercSpan;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Arrotonda la griglia numerica a step prefissati
|
|
1040
|
+
const roundStep = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
|
|
1041
|
+
min = Math.floor(min / roundStep) * roundStep;
|
|
1042
|
+
max = Math.ceil(max / roundStep) * roundStep;
|
|
1043
|
+
|
|
1044
|
+
return { min, max };
|
|
842
1045
|
}
|
|
1046
|
+
|
|
1047
|
+
// Scala Standard (Autocompressione a scatti)
|
|
843
1048
|
return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
844
1049
|
}
|
|
845
1050
|
|
|
@@ -863,7 +1068,6 @@ function refreshGraph(t) {
|
|
|
863
1068
|
|
|
864
1069
|
if (!rawData || rawData.length < 2) return;
|
|
865
1070
|
|
|
866
|
-
// ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
|
|
867
1071
|
const values = rawData.map(p => p.val);
|
|
868
1072
|
const mode = graphModes[boxType];
|
|
869
1073
|
const cfg = calculateScale(boxType, values, mode);
|
|
@@ -872,8 +1076,6 @@ function refreshGraph(t) {
|
|
|
872
1076
|
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
873
1077
|
|
|
874
1078
|
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
875
|
-
|
|
876
|
-
// Passiamo tutto l'array (oggetti) al nuovo motore
|
|
877
1079
|
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
878
1080
|
}
|
|
879
1081
|
|
|
@@ -889,16 +1091,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
889
1091
|
const isDepth = (id === 'depth-graph');
|
|
890
1092
|
const now = Date.now();
|
|
891
1093
|
|
|
892
|
-
// VIEWPORT TEMPORALE DINAMICO
|
|
893
1094
|
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
894
1095
|
const viewportMs = visibleMinutes * 60000;
|
|
895
1096
|
const viewportStart = now - viewportMs;
|
|
896
1097
|
|
|
897
|
-
// FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
|
|
898
1098
|
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
899
1099
|
if (visibleData.length < 2) return;
|
|
900
1100
|
|
|
901
|
-
// COLORI
|
|
902
1101
|
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
903
1102
|
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
904
1103
|
|
|
@@ -920,18 +1119,15 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
920
1119
|
return { color, opacity, stroke };
|
|
921
1120
|
};
|
|
922
1121
|
|
|
923
|
-
// GRIGLIA ORIZZONTALE
|
|
924
1122
|
let grids = "";
|
|
925
1123
|
[0.25, 0.5, 0.75].forEach(p => grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`);
|
|
926
1124
|
|
|
927
|
-
// GRIGLIA VERTICALE VERA
|
|
928
1125
|
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
929
1126
|
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
930
1127
|
const x = w - ((m / visibleMinutes) * w);
|
|
931
1128
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
932
1129
|
}
|
|
933
1130
|
|
|
934
|
-
// RENDER TIME-BASED
|
|
935
1131
|
let gradientStops = "", lines = "", areaPath = "";
|
|
936
1132
|
let started = false;
|
|
937
1133
|
|
|
@@ -939,59 +1135,43 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
939
1135
|
const pA = visibleData[i - 1];
|
|
940
1136
|
const pB = visibleData[i];
|
|
941
1137
|
|
|
942
|
-
// POSIZIONE X ESATTA AL MILLISECONDO
|
|
943
1138
|
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
944
1139
|
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
945
1140
|
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
946
1141
|
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
947
1142
|
|
|
948
1143
|
const props = getColorProps(pB.val);
|
|
949
|
-
|
|
950
|
-
// GESTIONE GAP TEMPORALI (Network loss)
|
|
951
1144
|
const deltaTime = pB.time - pA.time;
|
|
952
1145
|
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
953
1146
|
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
954
1147
|
|
|
955
|
-
// STOPS GRADIENTE
|
|
956
1148
|
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
957
1149
|
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
958
1150
|
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
959
1151
|
|
|
960
|
-
// --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
|
|
961
1152
|
if (isGap) {
|
|
962
|
-
// Se c'è un buco nei dati e avevamo già iniziato a disegnare...
|
|
963
1153
|
if (started) {
|
|
964
|
-
// Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
|
|
965
1154
|
areaPath += `L ${x1} ${h} `;
|
|
966
|
-
started = false;
|
|
1155
|
+
started = false;
|
|
967
1156
|
}
|
|
968
1157
|
} else {
|
|
969
|
-
// I dati sono continui, disegniamo la linea superiore
|
|
970
1158
|
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
|
|
971
|
-
|
|
972
|
-
// Gestione Area
|
|
973
1159
|
if (!started) {
|
|
974
|
-
// Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
|
|
975
1160
|
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
976
1161
|
started = true;
|
|
977
1162
|
}
|
|
978
|
-
// Aggiungiamo il punto attuale
|
|
979
1163
|
areaPath += `L ${x2} ${y2} `;
|
|
980
1164
|
}
|
|
981
1165
|
}
|
|
982
1166
|
|
|
983
|
-
// CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
|
|
984
1167
|
if (started) {
|
|
985
1168
|
const last = visibleData[visibleData.length - 1];
|
|
986
1169
|
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
987
|
-
areaPath += `L ${lastX} ${h} Z`;
|
|
1170
|
+
areaPath += `L ${lastX} ${h} Z`;
|
|
988
1171
|
}
|
|
989
1172
|
|
|
990
|
-
// GRADIENTE
|
|
991
1173
|
const gradId = `grad-${id}`;
|
|
992
1174
|
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
993
|
-
|
|
994
|
-
// RENDER FINALE
|
|
995
1175
|
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
996
1176
|
}
|
|
997
1177
|
|
|
@@ -1095,7 +1275,18 @@ function connect() {
|
|
|
1095
1275
|
const d = JSON.parse(e.data);
|
|
1096
1276
|
if (d.updates) {
|
|
1097
1277
|
d.updates.forEach(u => {
|
|
1098
|
-
|
|
1278
|
+
// ESTRAZIONE AVANZATA DELLA SORGENTE (Gestisce $source e stringhe native)
|
|
1279
|
+
let sourceLabel = "Unknown";
|
|
1280
|
+
if (u.$source) {
|
|
1281
|
+
sourceLabel = u.$source;
|
|
1282
|
+
} else if (u.source) {
|
|
1283
|
+
if (typeof u.source === 'object') {
|
|
1284
|
+
sourceLabel = u.source.label || u.source.talker || u.source.src || "Unknown";
|
|
1285
|
+
} else {
|
|
1286
|
+
sourceLabel = String(u.source);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1099
1290
|
if (u.values) {
|
|
1100
1291
|
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
1101
1292
|
}
|
package/debug.html
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
/* Stili Testo Tabella */
|
|
57
57
|
.value { color: #64ffda; font-weight: bold; font-size: 16px; }
|
|
58
58
|
.unit { color: #888; font-size: 12px; margin-left: 5px; }
|
|
59
|
-
.source { color: #ffb74d; font-size: 12px; }
|
|
59
|
+
.source { color: #ffb74d; font-size: 12px; word-break: break-all; }
|
|
60
60
|
.time { color: #90caf9; font-size: 12px; }
|
|
61
61
|
.raw { color: #777; font-size: 11px; display: block; margin-top: 4px; }
|
|
62
62
|
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
color: #aaa;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/* Area di testo del Log
|
|
86
|
+
/* Area di testo del Log */
|
|
87
87
|
textarea#log-container {
|
|
88
88
|
flex-grow: 1;
|
|
89
89
|
background: transparent;
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
<span style="margin-left: 20px; color:#aaa;">Filtra Log:</span>
|
|
123
123
|
<select id="log-filter">
|
|
124
124
|
<option value="all">Tutti i dati monitorati</option>
|
|
125
|
+
<option value="navigation.position">Solo Posizione (GPS)</option>
|
|
125
126
|
<option value="navigation.speedOverGround">Solo SOG (Velocità sul fondo)</option>
|
|
126
127
|
<option value="navigation.courseOverGroundTrue">Solo COG (Rotta sul fondo)</option>
|
|
127
128
|
<option value="wind">Solo Vento (TWS/TWA/AWS/AWA)</option>
|
|
@@ -140,7 +141,7 @@
|
|
|
140
141
|
<tr>
|
|
141
142
|
<th width="30%">Path (Percorso)</th>
|
|
142
143
|
<th width="20%">Valore Convertito</th>
|
|
143
|
-
<th width="30%">Sorgente (Sensore)</th>
|
|
144
|
+
<th width="30%">Sorgente (Sensore - Dettagliato)</th>
|
|
144
145
|
<th width="20%">Ultimo Ricevuto</th>
|
|
145
146
|
</tr>
|
|
146
147
|
</thead>
|
|
@@ -161,15 +162,16 @@
|
|
|
161
162
|
</div>
|
|
162
163
|
</div>
|
|
163
164
|
|
|
164
|
-
<!-- Usiamo una textarea readonly per permettere la selezione e lo scroll perfetto di testi lunghi -->
|
|
165
165
|
<textarea id="log-container" readonly></textarea>
|
|
166
166
|
</div>
|
|
167
167
|
|
|
168
168
|
</div>
|
|
169
169
|
|
|
170
170
|
<script>
|
|
171
|
-
// Percorsi da monitorare
|
|
172
171
|
const pathsToWatch = [
|
|
172
|
+
"navigation.position",
|
|
173
|
+
"navigation.position.latitude",
|
|
174
|
+
"navigation.position.longitude",
|
|
173
175
|
"navigation.speedThroughWater",
|
|
174
176
|
"navigation.speedOverGround",
|
|
175
177
|
"navigation.headingTrue",
|
|
@@ -182,13 +184,11 @@
|
|
|
182
184
|
];
|
|
183
185
|
|
|
184
186
|
let socket = null;
|
|
185
|
-
let logLinesArray = [];
|
|
186
|
-
const MAX_LOG_LINES = 1000;
|
|
187
|
+
let logLinesArray = [];
|
|
188
|
+
const MAX_LOG_LINES = 1000;
|
|
187
189
|
|
|
188
|
-
// Memoria per rilevare i picchi anomali (Spikes)
|
|
189
190
|
let lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
|
|
190
191
|
|
|
191
|
-
// Funzioni di conversione matematica
|
|
192
192
|
const radToDeg = (rad) => rad * (180 / Math.PI);
|
|
193
193
|
const msToKts = (ms) => ms * 1.94384;
|
|
194
194
|
|
|
@@ -207,10 +207,33 @@
|
|
|
207
207
|
tbody.appendChild(tr);
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
// Intestazione fissa del file CSV nel Log
|
|
211
210
|
const csvHeader = "Timestamp,Path,Value,Unit,Source,IsSpike";
|
|
212
211
|
document.getElementById('log-container').value = csvHeader + "\n";
|
|
213
212
|
|
|
213
|
+
// =========================================================
|
|
214
|
+
// ESTRAZIONE AVANZATA DELLA SORGENTE (GPS / NMEA)
|
|
215
|
+
// =========================================================
|
|
216
|
+
function extractSourceIdentifier(update) {
|
|
217
|
+
// 1. Cerca il campo standard moderno $source (es. "can0.GP" o "dbus.gps")
|
|
218
|
+
if (update.$source) {
|
|
219
|
+
return update.$source;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 2. Se non presente, analizza l'oggetto o la stringa source tradizionale
|
|
223
|
+
if (update.source) {
|
|
224
|
+
if (typeof update.source === 'object') {
|
|
225
|
+
// Estrae label, nome talker o identificativo bus hardware se disponibili
|
|
226
|
+
return update.source.label ||
|
|
227
|
+
update.source.talker ||
|
|
228
|
+
update.source.src ||
|
|
229
|
+
(update.source.device ? `${update.source.device}:${update.source.pgn || ''}` : JSON.stringify(update.source));
|
|
230
|
+
}
|
|
231
|
+
return String(update.source);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return "Unknown-Source";
|
|
235
|
+
}
|
|
236
|
+
|
|
214
237
|
// =========================================================
|
|
215
238
|
// GESTIONE CONNESSIONE WEBSOCKET
|
|
216
239
|
// =========================================================
|
|
@@ -233,7 +256,6 @@
|
|
|
233
256
|
status.innerText = "CONNESSIONE IN CORSO...";
|
|
234
257
|
status.className = "";
|
|
235
258
|
|
|
236
|
-
// Resetting spike logic on new connection
|
|
237
259
|
lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
|
|
238
260
|
|
|
239
261
|
socket = new WebSocket(`ws://${ip}/signalk/v1/stream?subscribe=self`);
|
|
@@ -245,23 +267,27 @@
|
|
|
245
267
|
};
|
|
246
268
|
|
|
247
269
|
socket.onmessage = (event) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
data.updates
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
update.values
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
try {
|
|
271
|
+
const data = JSON.parse(event.data);
|
|
272
|
+
|
|
273
|
+
if (data.updates) {
|
|
274
|
+
data.updates.forEach(update => {
|
|
275
|
+
// Cattura l'identificativo sorgente con la nuova logica avanzata
|
|
276
|
+
const sourceName = extractSourceIdentifier(update);
|
|
277
|
+
const timeISO = new Date(update.timestamp).toISOString();
|
|
278
|
+
const timeUI = new Date(update.timestamp).toLocaleTimeString() + '.' + new Date(update.timestamp).getMilliseconds().toString().padStart(3, '0');
|
|
279
|
+
|
|
280
|
+
if (update.values) {
|
|
281
|
+
update.values.forEach(v => {
|
|
282
|
+
if (pathsToWatch.includes(v.path)) {
|
|
283
|
+
processData(v.path, v.value, sourceName, timeISO, timeUI);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error("Errore elaborazione messaggio: ", err);
|
|
265
291
|
}
|
|
266
292
|
};
|
|
267
293
|
|
|
@@ -283,17 +309,22 @@
|
|
|
283
309
|
// ELABORAZIONE DATI E SPIKE DETECTION
|
|
284
310
|
// =========================================================
|
|
285
311
|
function processData(path, rawValue, source, timeISO, timeUI) {
|
|
286
|
-
let formattedVal =
|
|
312
|
+
let formattedVal = "---";
|
|
287
313
|
let unit = "";
|
|
288
314
|
let isSpike = false;
|
|
289
315
|
|
|
290
|
-
// Conversioni
|
|
291
316
|
if (rawValue !== null && rawValue !== undefined) {
|
|
292
|
-
if (path.
|
|
317
|
+
if (path === "navigation.position") {
|
|
318
|
+
if (typeof rawValue === 'object' && rawValue.latitude !== undefined && rawValue.longitude !== undefined) {
|
|
319
|
+
formattedVal = `${rawValue.latitude.toFixed(6)};${rawValue.longitude.toFixed(6)}`;
|
|
320
|
+
unit = "lat;lon";
|
|
321
|
+
} else {
|
|
322
|
+
formattedVal = "Invalid Position";
|
|
323
|
+
}
|
|
324
|
+
} else if (path.includes("speed")) {
|
|
293
325
|
formattedVal = msToKts(rawValue).toFixed(3);
|
|
294
326
|
unit = "kts";
|
|
295
327
|
|
|
296
|
-
// Rilevamento Spike SOG (> 5 nodi in un colpo solo)
|
|
297
328
|
if (path === "navigation.speedOverGround") {
|
|
298
329
|
let diff = Math.abs(formattedVal - lastValues[path]);
|
|
299
330
|
if (diff > 5.0 && lastValues[path] > 0) isSpike = true;
|
|
@@ -304,7 +335,6 @@
|
|
|
304
335
|
formattedVal = radToDeg(rawValue).toFixed(2);
|
|
305
336
|
unit = "deg";
|
|
306
337
|
|
|
307
|
-
// Rilevamento Spike COG (> 45 gradi improvviso)
|
|
308
338
|
if (path === "navigation.courseOverGroundTrue") {
|
|
309
339
|
let diff = Math.abs(formattedVal - lastValues[path]);
|
|
310
340
|
if (diff > 180) diff = 360 - diff;
|
|
@@ -315,6 +345,12 @@
|
|
|
315
345
|
} else if (path.includes("depth")) {
|
|
316
346
|
formattedVal = rawValue.toFixed(2);
|
|
317
347
|
unit = "m";
|
|
348
|
+
} else {
|
|
349
|
+
if (typeof rawValue === 'number') {
|
|
350
|
+
formattedVal = rawValue.toFixed(6);
|
|
351
|
+
} else {
|
|
352
|
+
formattedVal = String(rawValue);
|
|
353
|
+
}
|
|
318
354
|
}
|
|
319
355
|
}
|
|
320
356
|
|
|
@@ -330,31 +366,54 @@
|
|
|
330
366
|
|
|
331
367
|
function updateTableRow(path, val, unit, raw, source, timeUI) {
|
|
332
368
|
const safeId = path.replace(/\./g, '-');
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
369
|
+
|
|
370
|
+
let rawStr = "---";
|
|
371
|
+
if (raw !== null && raw !== undefined) {
|
|
372
|
+
if (typeof raw === 'number') {
|
|
373
|
+
rawStr = raw.toFixed(4);
|
|
374
|
+
} else if (typeof raw === 'object') {
|
|
375
|
+
rawStr = JSON.stringify(raw);
|
|
376
|
+
} else {
|
|
377
|
+
rawStr = String(raw);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const valCell = document.getElementById(`val-${safeId}`);
|
|
382
|
+
if (valCell) {
|
|
383
|
+
valCell.innerHTML = `<span class="value">${val}</span><span class="unit">${unit}</span><span class="raw">Raw: ${rawStr}</span>`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const srcCell = document.getElementById(`src-${safeId}`);
|
|
387
|
+
if (srcCell) {
|
|
388
|
+
srcCell.innerHTML = `<span class="source">${source}</span>`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const timeCell = document.getElementById(`time-${safeId}`);
|
|
392
|
+
if (timeCell) {
|
|
393
|
+
timeCell.innerHTML = `<span class="time">${timeUI}</span>`;
|
|
394
|
+
}
|
|
336
395
|
|
|
337
396
|
const row = document.getElementById(`row-${safeId}`);
|
|
338
|
-
row
|
|
339
|
-
|
|
397
|
+
if (row) {
|
|
398
|
+
row.style.backgroundColor = "#334a33";
|
|
399
|
+
setTimeout(() => row.style.backgroundColor = "", 150);
|
|
400
|
+
}
|
|
340
401
|
}
|
|
341
402
|
|
|
342
403
|
// =========================================================
|
|
343
404
|
// GESTIONE DEL LOG CSV E COPIA APPUNTI
|
|
344
405
|
// =========================================================
|
|
345
406
|
function addLogEntry(timeISO, path, value, unit, source, isSpike) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const safeSource = source.includes(',') ? `"${source}"` : source;
|
|
407
|
+
const safeValue = (value.includes(',') || value.includes(';')) ? `"${value}"` : value;
|
|
408
|
+
const safeSource = (source.includes(',') || source.includes(';')) ? `"${source}"` : source;
|
|
349
409
|
const spikeFlag = isSpike ? "TRUE" : "FALSE";
|
|
350
410
|
|
|
351
|
-
const csvLine = `${timeISO},${path},${
|
|
411
|
+
const csvLine = `${timeISO},${path},${safeValue},${unit},${safeSource},${spikeFlag}`;
|
|
352
412
|
|
|
353
413
|
logLinesArray.push(csvLine);
|
|
354
414
|
|
|
355
|
-
// Mantieni la memoria leggera
|
|
356
415
|
if (logLinesArray.length > MAX_LOG_LINES) {
|
|
357
|
-
logLinesArray.shift();
|
|
416
|
+
logLinesArray.shift();
|
|
358
417
|
}
|
|
359
418
|
|
|
360
419
|
renderLogTextArea();
|
|
@@ -362,11 +421,7 @@
|
|
|
362
421
|
|
|
363
422
|
function renderLogTextArea() {
|
|
364
423
|
const textArea = document.getElementById('log-container');
|
|
365
|
-
|
|
366
|
-
// Ricostruisci il testo unendo l'intestazione e l'array con \n (a capo)
|
|
367
424
|
textArea.value = csvHeader + "\n" + logLinesArray.join("\n");
|
|
368
|
-
|
|
369
|
-
// Auto-scroll verso il basso
|
|
370
425
|
textArea.scrollTop = textArea.scrollHeight;
|
|
371
426
|
}
|
|
372
427
|
|
|
@@ -377,14 +432,10 @@
|
|
|
377
432
|
|
|
378
433
|
function copyLogCSV() {
|
|
379
434
|
const textArea = document.getElementById('log-container');
|
|
380
|
-
const notif = document.getElementById('copy-notification');
|
|
381
|
-
|
|
382
|
-
// Seleziona il testo
|
|
383
435
|
textArea.select();
|
|
384
|
-
textArea.setSelectionRange(0, 999999);
|
|
436
|
+
textArea.setSelectionRange(0, 999999);
|
|
385
437
|
|
|
386
438
|
try {
|
|
387
|
-
// Copia negli appunti (API moderna Navigator o execCommand classico)
|
|
388
439
|
if (navigator.clipboard) {
|
|
389
440
|
navigator.clipboard.writeText(textArea.value).then(() => showCopyNotif());
|
|
390
441
|
} else {
|
|
@@ -401,11 +452,8 @@
|
|
|
401
452
|
const notif = document.getElementById('copy-notification');
|
|
402
453
|
notif.style.opacity = 1;
|
|
403
454
|
setTimeout(() => notif.style.opacity = 0, 2000);
|
|
404
|
-
|
|
405
|
-
// Deseleziona il testo
|
|
406
455
|
window.getSelection().removeAllRanges();
|
|
407
456
|
}
|
|
408
|
-
|
|
409
457
|
</script>
|
|
410
458
|
</body>
|
|
411
459
|
</html>
|