@sailingrotevista/rotevista-dash 5.0.4 → 5.0.6
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 +287 -117
- 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,10 +24,10 @@ 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
|
};
|
|
@@ -35,8 +35,7 @@ let CONFIG = {
|
|
|
35
35
|
const RENDER_INTERVAL_MS = 1000;
|
|
36
36
|
const TIMEOUT_MS = 5000;
|
|
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,15 @@ 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: {},
|
|
74
78
|
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
75
79
|
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
76
|
-
// histories ora conterrà array di oggetti: { time: Date.now(), val: numero }
|
|
77
80
|
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
78
81
|
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
79
82
|
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
|
|
@@ -326,14 +329,62 @@ function computeTrueWind() {
|
|
|
326
329
|
}
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
// ==========================================================================
|
|
333
|
+
// GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
|
|
334
|
+
// ==========================================================================
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
|
|
338
|
+
* Più alto è il punteggio, maggiore è la priorità del sensore.
|
|
339
|
+
*/
|
|
340
|
+
function getSourcePriorityScore(sourceName) {
|
|
341
|
+
if (!sourceName) return 0;
|
|
342
|
+
const name = sourceName.toLowerCase();
|
|
343
|
+
|
|
344
|
+
// TIER 3: AIS / Trasmissioni lente (es. yacht_device.AI, VDO)
|
|
345
|
+
if (name.includes('.ai') || name.includes('ais') || name.includes('vdo')) {
|
|
346
|
+
return 10;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// TIER 2: GPS USB del Cerbo GX / Victron / Sistemi locali di servizio
|
|
350
|
+
if (name.includes('venus') || name.includes('victron') || name.includes('ttyacm') || name.includes('ttyusb') || name.includes('system')) {
|
|
351
|
+
return 50;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// TIER 1: Strumentazione ufficiale di navigazione (NMEA 2000, Yacht Devices GP/YD/AP, Gateway, ecc.)
|
|
355
|
+
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')) {
|
|
356
|
+
return 100;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return 30; // Punteggio standard per sorgenti sconosciute
|
|
360
|
+
}
|
|
361
|
+
|
|
329
362
|
function processIncomingData(path, val, source) {
|
|
330
363
|
const now = Date.now();
|
|
364
|
+
const score = getSourcePriorityScore(source);
|
|
331
365
|
|
|
332
|
-
//
|
|
333
|
-
if (!sourceLocks[path]
|
|
334
|
-
|
|
366
|
+
// Gestione dello Smart Lock
|
|
367
|
+
if (!sourceLocks[path]) {
|
|
368
|
+
// Nessun blocco attivo: aggancia la sorgente corrente
|
|
369
|
+
sourceLocks[path] = { label: source, score: score, lastSeen: now };
|
|
335
370
|
} else {
|
|
336
|
-
|
|
371
|
+
const currentLock = sourceLocks[path];
|
|
372
|
+
const isSameSource = (currentLock.label === source);
|
|
373
|
+
const isLockExpired = (now - currentLock.lastSeen > 12000); // Scadenza a 12 secondi per tollerare il GPS dello Yacht Devices
|
|
374
|
+
const hasHigherPriority = (score > currentLock.score);
|
|
375
|
+
|
|
376
|
+
if (isSameSource) {
|
|
377
|
+
// Stessa sorgente: aggiorna il timestamp e mantiene il blocco
|
|
378
|
+
currentLock.lastSeen = now;
|
|
379
|
+
currentLock.score = score;
|
|
380
|
+
} else if (isLockExpired || hasHigherPriority) {
|
|
381
|
+
// Ruba il blocco se la sorgente precedente è scaduta o se questa ha priorità superiore
|
|
382
|
+
sourceLocks[path] = { label: source, score: score, lastSeen: now };
|
|
383
|
+
console.log(`🔌 [Smart Lock] Path "${path}" switched to: ${source} (Score: ${score})`);
|
|
384
|
+
} else {
|
|
385
|
+
// Rifiuta i dati da sorgenti a priorità inferiore se quella principale è attiva
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
337
388
|
}
|
|
338
389
|
|
|
339
390
|
store.timestamps[path] = now;
|
|
@@ -373,57 +424,16 @@ function processIncomingData(path, val, source) {
|
|
|
373
424
|
// ==========================================================================
|
|
374
425
|
function updateWindTrend() {
|
|
375
426
|
const now = Date.now();
|
|
427
|
+
|
|
428
|
+
// --- 6.1 TREND TATTICO (AWA/TWA) ---
|
|
376
429
|
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
|
|
377
430
|
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
431
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
388
432
|
|
|
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)
|
|
433
|
+
// --- 6.2 ALLARME STRAMBATA CON ISTERESI (MACCHINA A STATI ANTI-BRANDEGGIO) ---
|
|
426
434
|
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
435
|
+
let smoothedTwaDeg = null;
|
|
436
|
+
|
|
427
437
|
if (instTwaRad !== undefined) {
|
|
428
438
|
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
429
439
|
const currentSin = Math.sin(instTwaRad);
|
|
@@ -436,19 +446,98 @@ function updateWindTrend() {
|
|
|
436
446
|
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
437
447
|
}
|
|
438
448
|
|
|
439
|
-
|
|
449
|
+
smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
450
|
+
const absTwaDeg = Math.abs(smoothedTwaDeg);
|
|
451
|
+
|
|
452
|
+
// Allarme VISIVO: Se siamo in zona di pericolo poppa profonda (> 155°), accendi entrambi i LED di rosso pulsante
|
|
453
|
+
if (absTwaDeg > 155) {
|
|
454
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-gybing'); gaugeDots.cw.setAttribute('fill', '#ff3b30'); }
|
|
455
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-gybing'); gaugeDots.ccw.setAttribute('fill', '#ff3b30'); }
|
|
456
|
+
|
|
457
|
+
// LOGICA MACCHINA A STATI CON ISTERESI:
|
|
458
|
+
// Rileviamo le mure solo se siamo fuori dalla zona cieca di poppa secca (> 170°).
|
|
459
|
+
// Se oscilliamo tra -175° e +178° (brandeggio), lastGybeSide NON cambia e l'allarme acustico tace.
|
|
460
|
+
let currentTack = null;
|
|
461
|
+
if (smoothedTwaDeg > 155 && smoothedTwaDeg < 170) {
|
|
462
|
+
currentTack = 'starboard';
|
|
463
|
+
} else if (smoothedTwaDeg < -155 && smoothedTwaDeg > -170) {
|
|
464
|
+
currentTack = 'port';
|
|
465
|
+
}
|
|
440
466
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
467
|
+
if (currentTack !== null) {
|
|
468
|
+
if (lastGybeSide === null) {
|
|
469
|
+
// Inizializzazione al primo ingresso nella zona di controllo
|
|
470
|
+
lastGybeSide = currentTack;
|
|
471
|
+
} else if (lastGybeSide !== currentTack) {
|
|
472
|
+
// Abbiamo eseguito una vera strambata stabile e siamo usciti dalla zona di poppa secca!
|
|
473
|
+
lastGybeSide = currentTack;
|
|
474
|
+
|
|
475
|
+
// Attivazione allarme acustico con blocco temporale di sicurezza (60 secondi)
|
|
476
|
+
if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
|
|
477
|
+
lastGybeAlarmTime = now;
|
|
478
|
+
playGybeAlarm();
|
|
479
|
+
console.log(`⚠️ GYBE ALARM TRIGGERED: Tack switched to ${currentTack} (TWA: ${smoothedTwaDeg.toFixed(1)}°)`);
|
|
480
|
+
}
|
|
447
481
|
}
|
|
448
482
|
}
|
|
483
|
+
} else {
|
|
484
|
+
// Se usciamo dalla poppa profonda (< 155°), disattiva l'allarme visivo e resetta lo stato delle mure
|
|
485
|
+
if (gaugeDots.cw) gaugeDots.cw.classList.remove('is-gybing');
|
|
486
|
+
if (gaugeDots.ccw) gaugeDots.ccw.classList.remove('is-gybing');
|
|
487
|
+
lastGybeSide = null; // Reset per la prossima poppa
|
|
449
488
|
}
|
|
450
489
|
lastInstantTwa = smoothedTwaDeg;
|
|
451
490
|
}
|
|
491
|
+
|
|
492
|
+
// Gestione normale dei Trend Tattici (Lifts/Headers) se NON siamo in allarme strambata
|
|
493
|
+
if (twaNow && twaRef && (smoothedTwaDeg === null || Math.abs(smoothedTwaDeg) <= 155)) {
|
|
494
|
+
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
495
|
+
const curTwaDeg = radToDeg(twaNow.val);
|
|
496
|
+
if (Math.abs(deltaTac) > 3.0) {
|
|
497
|
+
let absTwa = Math.abs(curTwaDeg);
|
|
498
|
+
let tacticColor;
|
|
499
|
+
if (absTwa > 75 && absTwa < 105) {
|
|
500
|
+
tacticColor = "#bbb";
|
|
501
|
+
} else {
|
|
502
|
+
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
503
|
+
if (absTwa >= 90) isLift = !isLift;
|
|
504
|
+
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
505
|
+
}
|
|
506
|
+
if (deltaTac > 0) {
|
|
507
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
508
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); }
|
|
509
|
+
} else {
|
|
510
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
|
|
511
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); }
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
[gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// --- 6.3 TREND METEO STRATEGICO (TWD) ---
|
|
519
|
+
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
520
|
+
const multiplier = isNavigating ? 1 : 2;
|
|
521
|
+
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
522
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
523
|
+
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
524
|
+
|
|
525
|
+
if (twdNow && twdRef) {
|
|
526
|
+
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
527
|
+
if (Math.abs(deltaMeteo) > 6.0) {
|
|
528
|
+
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
529
|
+
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
|
|
530
|
+
if (deltaMeteo > 0) {
|
|
531
|
+
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
532
|
+
if (compassDots.ccw) { compassDots.ccw.classList.remove('is-trending'); }
|
|
533
|
+
} else {
|
|
534
|
+
if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
|
|
535
|
+
if (compassDots.cw) { compassDots.cw.classList.remove('is-trending'); }
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
[compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
452
541
|
}
|
|
453
542
|
|
|
454
543
|
// ==========================================================================
|
|
@@ -487,6 +576,9 @@ function startDisplayLoop() {
|
|
|
487
576
|
|
|
488
577
|
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
489
578
|
|
|
579
|
+
// --- CALCOLO TREND VENTO & ALLARME STRAMBATA ---
|
|
580
|
+
updateWindTrend();
|
|
581
|
+
|
|
490
582
|
// --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
|
|
491
583
|
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
492
584
|
const requiredMs = viewportMinutes * 60000;
|
|
@@ -742,44 +834,136 @@ async function watchConfigChanges() {
|
|
|
742
834
|
}
|
|
743
835
|
}
|
|
744
836
|
|
|
745
|
-
// --------------------------------------------------------------------------
|
|
746
|
-
|
|
747
837
|
/**
|
|
748
|
-
*
|
|
838
|
+
* manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
|
|
839
|
+
* Integrazioni:
|
|
840
|
+
* 1. Strict undefined check per lastUpdates.
|
|
841
|
+
* 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
|
|
842
|
+
* 3. Clamping di sicurezza (no negativi, no Infinity).
|
|
749
843
|
*/
|
|
750
|
-
function manageHistory(
|
|
751
|
-
|
|
752
|
-
|
|
844
|
+
function manageHistory(type, value) {
|
|
845
|
+
// --- 1. VALIDAZIONE INPUT RIGOROSA ---
|
|
846
|
+
if (value === undefined || value === null || !isFinite(value)) return;
|
|
847
|
+
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
|
|
850
|
+
const samples = Math.max(2, CONFIG.graphs.samples || 60);
|
|
851
|
+
const bucketIntervalMs = (historyMinutes * 60000) / samples;
|
|
852
|
+
|
|
853
|
+
// --- 2. INIT SICURO (Strict Check) ---
|
|
854
|
+
if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
|
|
855
|
+
if (!store.histories[type]) store.histories[type] = [];
|
|
856
|
+
if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
|
|
857
|
+
|
|
858
|
+
const tempBuf = store.graphTempBuf[type];
|
|
859
|
+
|
|
860
|
+
// --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
|
|
861
|
+
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
862
|
+
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
863
|
+
const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
|
|
864
|
+
if (lastPoint && lastPoint.val > glitchThreshold) return;
|
|
865
|
+
}
|
|
753
866
|
|
|
754
|
-
|
|
755
|
-
|
|
867
|
+
// --- 4. STORAGE TEMPORANEO ---
|
|
868
|
+
tempBuf.push({ val: value, time: now });
|
|
756
869
|
|
|
757
|
-
|
|
758
|
-
|
|
870
|
+
// Controllo finestra temporale
|
|
871
|
+
const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
|
|
872
|
+
if (!bucketReady) return;
|
|
759
873
|
|
|
760
|
-
|
|
761
|
-
|
|
874
|
+
// --- 5. AGGREGAZIONE SEMANTICA ---
|
|
875
|
+
let finalValue = value;
|
|
762
876
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
877
|
+
if (tempBuf.length > 0) {
|
|
878
|
+
// A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
|
|
879
|
+
if (type === 'tws' || type === 'aws') {
|
|
880
|
+
const tauMs = 2500;
|
|
881
|
+
let ema = tempBuf[0].val;
|
|
882
|
+
let maxSustained = ema;
|
|
766
883
|
|
|
767
|
-
|
|
768
|
-
|
|
884
|
+
for (let i = 1; i < tempBuf.length; i++) {
|
|
885
|
+
const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
|
|
886
|
+
const alpha = 1 - Math.exp(-dt / tauMs);
|
|
887
|
+
ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
|
|
888
|
+
if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
|
|
889
|
+
}
|
|
890
|
+
finalValue = maxSustained;
|
|
891
|
+
}
|
|
892
|
+
// B. PROFONDITÀ -> MINIMO
|
|
893
|
+
else if (type === 'depth') {
|
|
894
|
+
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
895
|
+
if (vals.length > 0) finalValue = Math.min(...vals);
|
|
769
896
|
}
|
|
897
|
+
// C. VELOCITÀ -> MEDIA
|
|
898
|
+
else {
|
|
899
|
+
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
900
|
+
if (vals.length > 0) {
|
|
901
|
+
const sum = vals.reduce((a, b) => a + b, 0);
|
|
902
|
+
finalValue = sum / tempBuf.length;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
770
906
|
|
|
771
|
-
|
|
772
|
-
|
|
907
|
+
// --- 6. CLAMPING E VALIDAZIONE FINALE ---
|
|
908
|
+
if (!isFinite(finalValue)) return;
|
|
909
|
+
finalValue = Math.max(0, finalValue);
|
|
910
|
+
|
|
911
|
+
// --- 7. STORAGE STORICO ---
|
|
912
|
+
store.histories[type].push({ val: finalValue, time: now });
|
|
913
|
+
|
|
914
|
+
// --- 8. PRUNING DINAMICO ---
|
|
915
|
+
const maxViewportMinutes = historyMinutes * 2;
|
|
916
|
+
const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
|
|
917
|
+
|
|
918
|
+
while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
|
|
919
|
+
store.histories[type].shift();
|
|
773
920
|
}
|
|
921
|
+
|
|
922
|
+
// Reset per il prossimo bucket
|
|
923
|
+
store.graphTempBuf[type] = [];
|
|
924
|
+
store.lastUpdates[type] = now;
|
|
774
925
|
}
|
|
775
926
|
|
|
927
|
+
/**
|
|
928
|
+
* Gestione dinamica delle scale dei grafici (Involucro Elastico e Safety Zoom)
|
|
929
|
+
*/
|
|
776
930
|
function calculateScale(type, data, mode) {
|
|
777
|
-
const s = CONFIG.scales[type];
|
|
931
|
+
const s = CONFIG.scales[type];
|
|
932
|
+
let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
933
|
+
|
|
934
|
+
// --- SAFETY ZOOM PER PROFONDITÀ (FONDALE BASSO) ---
|
|
935
|
+
if (type === 'depth' && mode !== 'hercules') {
|
|
936
|
+
const currentDepth = data[data.length - 1];
|
|
937
|
+
const shallowThreshold = Math.max(s.stdMax, 10);
|
|
938
|
+
if (currentDepth <= shallowThreshold) {
|
|
939
|
+
return { min: 0, max: shallowThreshold };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// --- MODALITÀ HERCULES AGGIORNATA (INVOLUCRO DINAMICO ELASTICO) ---
|
|
778
944
|
if (mode === 'hercules') {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
945
|
+
const padding = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
|
|
946
|
+
|
|
947
|
+
let min = Math.max(0, Math.floor(aMin - padding));
|
|
948
|
+
let max = Math.ceil(aMax + padding);
|
|
949
|
+
|
|
950
|
+
// Garantisce uno span minimo configurato per evitare zoom eccessivi sul rumore di fondo
|
|
951
|
+
const currentSpan = max - min;
|
|
952
|
+
if (currentSpan < s.hercSpan) {
|
|
953
|
+
const diff = s.hercSpan - currentSpan;
|
|
954
|
+
min = Math.max(0, min - Math.floor(diff / 2));
|
|
955
|
+
max = min + s.hercSpan;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Arrotonda la griglia numerica a step prefissati per evitare sfarfallamento visivo
|
|
959
|
+
const roundStep = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
|
|
960
|
+
min = Math.floor(min / roundStep) * roundStep;
|
|
961
|
+
max = Math.ceil(max / roundStep) * roundStep;
|
|
962
|
+
|
|
963
|
+
return { min, max };
|
|
782
964
|
}
|
|
965
|
+
|
|
966
|
+
// Scala Standard (Autocompressione a scatti)
|
|
783
967
|
return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
784
968
|
}
|
|
785
969
|
|
|
@@ -803,7 +987,6 @@ function refreshGraph(t) {
|
|
|
803
987
|
|
|
804
988
|
if (!rawData || rawData.length < 2) return;
|
|
805
989
|
|
|
806
|
-
// ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
|
|
807
990
|
const values = rawData.map(p => p.val);
|
|
808
991
|
const mode = graphModes[boxType];
|
|
809
992
|
const cfg = calculateScale(boxType, values, mode);
|
|
@@ -812,8 +995,6 @@ function refreshGraph(t) {
|
|
|
812
995
|
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
813
996
|
|
|
814
997
|
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
815
|
-
|
|
816
|
-
// Passiamo tutto l'array (oggetti) al nuovo motore
|
|
817
998
|
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
818
999
|
}
|
|
819
1000
|
|
|
@@ -829,16 +1010,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
829
1010
|
const isDepth = (id === 'depth-graph');
|
|
830
1011
|
const now = Date.now();
|
|
831
1012
|
|
|
832
|
-
// VIEWPORT TEMPORALE DINAMICO
|
|
833
1013
|
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
834
1014
|
const viewportMs = visibleMinutes * 60000;
|
|
835
1015
|
const viewportStart = now - viewportMs;
|
|
836
1016
|
|
|
837
|
-
// FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
|
|
838
1017
|
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
839
1018
|
if (visibleData.length < 2) return;
|
|
840
1019
|
|
|
841
|
-
// COLORI
|
|
842
1020
|
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
843
1021
|
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
844
1022
|
|
|
@@ -860,18 +1038,15 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
860
1038
|
return { color, opacity, stroke };
|
|
861
1039
|
};
|
|
862
1040
|
|
|
863
|
-
// GRIGLIA ORIZZONTALE
|
|
864
1041
|
let grids = "";
|
|
865
1042
|
[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" />`);
|
|
866
1043
|
|
|
867
|
-
// GRIGLIA VERTICALE VERA
|
|
868
1044
|
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
869
1045
|
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
870
1046
|
const x = w - ((m / visibleMinutes) * w);
|
|
871
1047
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
872
1048
|
}
|
|
873
1049
|
|
|
874
|
-
// RENDER TIME-BASED
|
|
875
1050
|
let gradientStops = "", lines = "", areaPath = "";
|
|
876
1051
|
let started = false;
|
|
877
1052
|
|
|
@@ -879,59 +1054,43 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
879
1054
|
const pA = visibleData[i - 1];
|
|
880
1055
|
const pB = visibleData[i];
|
|
881
1056
|
|
|
882
|
-
// POSIZIONE X ESATTA AL MILLISECONDO
|
|
883
1057
|
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
884
1058
|
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
885
1059
|
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
886
1060
|
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
887
1061
|
|
|
888
1062
|
const props = getColorProps(pB.val);
|
|
889
|
-
|
|
890
|
-
// GESTIONE GAP TEMPORALI (Network loss)
|
|
891
1063
|
const deltaTime = pB.time - pA.time;
|
|
892
1064
|
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
893
1065
|
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
894
1066
|
|
|
895
|
-
// STOPS GRADIENTE
|
|
896
1067
|
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
897
1068
|
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
898
1069
|
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
899
1070
|
|
|
900
|
-
// --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
|
|
901
1071
|
if (isGap) {
|
|
902
|
-
// Se c'è un buco nei dati e avevamo già iniziato a disegnare...
|
|
903
1072
|
if (started) {
|
|
904
|
-
// Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
|
|
905
1073
|
areaPath += `L ${x1} ${h} `;
|
|
906
|
-
started = false;
|
|
1074
|
+
started = false;
|
|
907
1075
|
}
|
|
908
1076
|
} else {
|
|
909
|
-
// I dati sono continui, disegniamo la linea superiore
|
|
910
1077
|
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
|
|
911
|
-
|
|
912
|
-
// Gestione Area
|
|
913
1078
|
if (!started) {
|
|
914
|
-
// Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
|
|
915
1079
|
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
916
1080
|
started = true;
|
|
917
1081
|
}
|
|
918
|
-
// Aggiungiamo il punto attuale
|
|
919
1082
|
areaPath += `L ${x2} ${y2} `;
|
|
920
1083
|
}
|
|
921
1084
|
}
|
|
922
1085
|
|
|
923
|
-
// CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
|
|
924
1086
|
if (started) {
|
|
925
1087
|
const last = visibleData[visibleData.length - 1];
|
|
926
1088
|
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
927
|
-
areaPath += `L ${lastX} ${h} Z`;
|
|
1089
|
+
areaPath += `L ${lastX} ${h} Z`;
|
|
928
1090
|
}
|
|
929
1091
|
|
|
930
|
-
// GRADIENTE
|
|
931
1092
|
const gradId = `grad-${id}`;
|
|
932
1093
|
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
933
|
-
|
|
934
|
-
// RENDER FINALE
|
|
935
1094
|
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
936
1095
|
}
|
|
937
1096
|
|
|
@@ -1035,7 +1194,18 @@ function connect() {
|
|
|
1035
1194
|
const d = JSON.parse(e.data);
|
|
1036
1195
|
if (d.updates) {
|
|
1037
1196
|
d.updates.forEach(u => {
|
|
1038
|
-
|
|
1197
|
+
// ESTRAZIONE AVANZATA DELLA SORGENTE (Gestisce $source e stringhe native)
|
|
1198
|
+
let sourceLabel = "Unknown";
|
|
1199
|
+
if (u.$source) {
|
|
1200
|
+
sourceLabel = u.$source;
|
|
1201
|
+
} else if (u.source) {
|
|
1202
|
+
if (typeof u.source === 'object') {
|
|
1203
|
+
sourceLabel = u.source.label || u.source.talker || u.source.src || "Unknown";
|
|
1204
|
+
} else {
|
|
1205
|
+
sourceLabel = String(u.source);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1039
1209
|
if (u.values) {
|
|
1040
1210
|
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
1041
1211
|
}
|
|
@@ -1069,7 +1239,7 @@ async function init() {
|
|
|
1069
1239
|
await fetchServerConfig();
|
|
1070
1240
|
startDisplayLoop();
|
|
1071
1241
|
connect();
|
|
1072
|
-
setInterval(watchConfigChanges, 10000);
|
|
1242
|
+
setInterval(watchConfigChanges, 10000);
|
|
1073
1243
|
}
|
|
1074
1244
|
|
|
1075
1245
|
window.addEventListener('load', init);
|
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>
|