@sailingrotevista/rotevista-dash 5.0.5 → 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 +210 -100
- 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;
|
|
@@ -766,7 +858,6 @@ function manageHistory(type, value) {
|
|
|
766
858
|
const tempBuf = store.graphTempBuf[type];
|
|
767
859
|
|
|
768
860
|
// --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
|
|
769
|
-
// Ignora cadute a zero se il valore precedente era superiore al 50% del primo Reef
|
|
770
861
|
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
771
862
|
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
772
863
|
const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
|
|
@@ -814,11 +905,10 @@ function manageHistory(type, value) {
|
|
|
814
905
|
}
|
|
815
906
|
|
|
816
907
|
// --- 6. CLAMPING E VALIDAZIONE FINALE ---
|
|
817
|
-
// Protezione contro valori negativi (fisicamente impossibili per questi dati) e non finiti
|
|
818
908
|
if (!isFinite(finalValue)) return;
|
|
819
909
|
finalValue = Math.max(0, finalValue);
|
|
820
910
|
|
|
821
|
-
// --- 7. STORAGE STORICO
|
|
911
|
+
// --- 7. STORAGE STORICO ---
|
|
822
912
|
store.histories[type].push({ val: finalValue, time: now });
|
|
823
913
|
|
|
824
914
|
// --- 8. PRUNING DINAMICO ---
|
|
@@ -833,13 +923,47 @@ function manageHistory(type, value) {
|
|
|
833
923
|
store.graphTempBuf[type] = [];
|
|
834
924
|
store.lastUpdates[type] = now;
|
|
835
925
|
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Gestione dinamica delle scale dei grafici (Involucro Elastico e Safety Zoom)
|
|
929
|
+
*/
|
|
836
930
|
function calculateScale(type, data, mode) {
|
|
837
|
-
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) ---
|
|
838
944
|
if (mode === 'hercules') {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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 };
|
|
842
964
|
}
|
|
965
|
+
|
|
966
|
+
// Scala Standard (Autocompressione a scatti)
|
|
843
967
|
return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
844
968
|
}
|
|
845
969
|
|
|
@@ -863,7 +987,6 @@ function refreshGraph(t) {
|
|
|
863
987
|
|
|
864
988
|
if (!rawData || rawData.length < 2) return;
|
|
865
989
|
|
|
866
|
-
// ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
|
|
867
990
|
const values = rawData.map(p => p.val);
|
|
868
991
|
const mode = graphModes[boxType];
|
|
869
992
|
const cfg = calculateScale(boxType, values, mode);
|
|
@@ -872,8 +995,6 @@ function refreshGraph(t) {
|
|
|
872
995
|
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
873
996
|
|
|
874
997
|
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
875
|
-
|
|
876
|
-
// Passiamo tutto l'array (oggetti) al nuovo motore
|
|
877
998
|
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
878
999
|
}
|
|
879
1000
|
|
|
@@ -889,16 +1010,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
889
1010
|
const isDepth = (id === 'depth-graph');
|
|
890
1011
|
const now = Date.now();
|
|
891
1012
|
|
|
892
|
-
// VIEWPORT TEMPORALE DINAMICO
|
|
893
1013
|
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
894
1014
|
const viewportMs = visibleMinutes * 60000;
|
|
895
1015
|
const viewportStart = now - viewportMs;
|
|
896
1016
|
|
|
897
|
-
// FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
|
|
898
1017
|
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
899
1018
|
if (visibleData.length < 2) return;
|
|
900
1019
|
|
|
901
|
-
// COLORI
|
|
902
1020
|
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
903
1021
|
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
904
1022
|
|
|
@@ -920,18 +1038,15 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
920
1038
|
return { color, opacity, stroke };
|
|
921
1039
|
};
|
|
922
1040
|
|
|
923
|
-
// GRIGLIA ORIZZONTALE
|
|
924
1041
|
let grids = "";
|
|
925
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" />`);
|
|
926
1043
|
|
|
927
|
-
// GRIGLIA VERTICALE VERA
|
|
928
1044
|
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
929
1045
|
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
930
1046
|
const x = w - ((m / visibleMinutes) * w);
|
|
931
1047
|
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
|
|
932
1048
|
}
|
|
933
1049
|
|
|
934
|
-
// RENDER TIME-BASED
|
|
935
1050
|
let gradientStops = "", lines = "", areaPath = "";
|
|
936
1051
|
let started = false;
|
|
937
1052
|
|
|
@@ -939,59 +1054,43 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
939
1054
|
const pA = visibleData[i - 1];
|
|
940
1055
|
const pB = visibleData[i];
|
|
941
1056
|
|
|
942
|
-
// POSIZIONE X ESATTA AL MILLISECONDO
|
|
943
1057
|
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
944
1058
|
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
945
1059
|
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
946
1060
|
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
947
1061
|
|
|
948
1062
|
const props = getColorProps(pB.val);
|
|
949
|
-
|
|
950
|
-
// GESTIONE GAP TEMPORALI (Network loss)
|
|
951
1063
|
const deltaTime = pB.time - pA.time;
|
|
952
1064
|
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
953
1065
|
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
954
1066
|
|
|
955
|
-
// STOPS GRADIENTE
|
|
956
1067
|
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
957
1068
|
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
958
1069
|
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
959
1070
|
|
|
960
|
-
// --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
|
|
961
1071
|
if (isGap) {
|
|
962
|
-
// Se c'è un buco nei dati e avevamo già iniziato a disegnare...
|
|
963
1072
|
if (started) {
|
|
964
|
-
// Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
|
|
965
1073
|
areaPath += `L ${x1} ${h} `;
|
|
966
|
-
started = false;
|
|
1074
|
+
started = false;
|
|
967
1075
|
}
|
|
968
1076
|
} else {
|
|
969
|
-
// I dati sono continui, disegniamo la linea superiore
|
|
970
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;" />`;
|
|
971
|
-
|
|
972
|
-
// Gestione Area
|
|
973
1078
|
if (!started) {
|
|
974
|
-
// Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
|
|
975
1079
|
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
976
1080
|
started = true;
|
|
977
1081
|
}
|
|
978
|
-
// Aggiungiamo il punto attuale
|
|
979
1082
|
areaPath += `L ${x2} ${y2} `;
|
|
980
1083
|
}
|
|
981
1084
|
}
|
|
982
1085
|
|
|
983
|
-
// CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
|
|
984
1086
|
if (started) {
|
|
985
1087
|
const last = visibleData[visibleData.length - 1];
|
|
986
1088
|
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
987
|
-
areaPath += `L ${lastX} ${h} Z`;
|
|
1089
|
+
areaPath += `L ${lastX} ${h} Z`;
|
|
988
1090
|
}
|
|
989
1091
|
|
|
990
|
-
// GRADIENTE
|
|
991
1092
|
const gradId = `grad-${id}`;
|
|
992
1093
|
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
993
|
-
|
|
994
|
-
// RENDER FINALE
|
|
995
1094
|
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
996
1095
|
}
|
|
997
1096
|
|
|
@@ -1095,7 +1194,18 @@ function connect() {
|
|
|
1095
1194
|
const d = JSON.parse(e.data);
|
|
1096
1195
|
if (d.updates) {
|
|
1097
1196
|
d.updates.forEach(u => {
|
|
1098
|
-
|
|
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
|
+
|
|
1099
1209
|
if (u.values) {
|
|
1100
1210
|
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
1101
1211
|
}
|
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>
|