@sailingrotevista/rotevista-dash 4.0.19 → 4.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.js CHANGED
@@ -17,7 +17,7 @@ let CONFIG = {
17
17
  smoothWindow: 2000,
18
18
  longWindow: 30000,
19
19
  stabilityTolerance: 2000,
20
- stabilityThreshold: 0.85,
20
+ stabilityThreshold: 0.95,
21
21
  minSpeed: 0,
22
22
  stabilityBreakout: 15
23
23
  },
@@ -35,6 +35,8 @@ const RENDER_INTERVAL_MS = 1000;
35
35
  const TIMEOUT_MS = 5000;
36
36
  const SIM_SAMPLE_INTERVAL = 1000;
37
37
  const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
38
+ const sourceLocks = {};
39
+
38
40
 
39
41
  // ==========================================================================
40
42
  // 2. STATO GLOBALE E RIFERIMENTI UI
@@ -53,6 +55,10 @@ let twDirty = false, isNavigating = false, reconnectDelay = 1000;
53
55
 
54
56
  let pressTimer, isFocusActive = false;
55
57
 
58
+ let emaTwaSin = 0;
59
+ let emaTwaCos = 0;
60
+ let firstEmaRun = true;
61
+
56
62
  // Stato dei singoli grafici (Standard vs Hercules Zoom)
57
63
  const graphModes = {
58
64
  stw: 'standard',
@@ -110,39 +116,106 @@ function getShortestRotation(curr, target) {
110
116
  /**
111
117
  * Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
112
118
  */
113
- function safePush(buffer, val, time, maxLen = 2000) {
114
- buffer.push({ val: val, time: time });
115
- if (buffer.length > maxLen) { buffer.shift(); }
119
+ /**
120
+ * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
121
+ */
122
+ function safePush(buffer, val, time) {
123
+ // --- PROTEZIONE ANTI-NaN (FONDAMENTALE ALL'AVVIO) ---
124
+ // Se il valore è nullo, non definito o non è un numero, ignoriamo l'inserimento
125
+ if (val === null || val === undefined || isNaN(val)) return;
126
+
127
+ buffer.push({
128
+ val: val,
129
+ time: time,
130
+ sin: Math.sin(val),
131
+ cos: Math.cos(val)
132
+ });
133
+
134
+ // Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
135
+ // + 1 minuto di margine
136
+ const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
137
+
138
+ while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
139
+ buffer.shift();
140
+ }
141
+
142
+ // Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
143
+ if (buffer.length > 36000) buffer.shift();
116
144
  }
117
145
 
118
146
  /**
119
- * Media Circolare Vettoriale: Calcola angolo medio, stabilità R e deviazione standard (±)
147
+ * Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
148
+ * Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
120
149
  */
121
- function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
122
- const now = Date.now();
123
- const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
124
- if (validData.length === 0) return null;
125
-
126
- let sSin = 0, sCos = 0;
127
- validData.forEach(item => {
128
- sSin += Math.sin(item.val);
129
- sCos += Math.cos(item.val);
130
- });
131
-
132
- let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
133
- let historyDuration = (validData.length > 2) ? (validData[validData.length - 1].time - validData[0].time) : 0;
134
-
135
- // Un dato è stabile se abbiamo abbastanza storia e la coerenza vettoriale R è alta
136
- let isStable = (historyDuration > 10000) && (R > CONFIG.averaging.stabilityThreshold);
137
- let avgRad = Math.atan2(sSin, sCos);
150
+ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
151
+ now = now || Date.now();
152
+ const len = bufferArray.length;
153
+ if (len === 0) return null;
154
+
155
+ let sSin = 0, sCos = 0, count = 0;
156
+ let newestTime = 0, oldestTime = 0;
157
+
158
+ // 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
159
+ let pilotSin = 0, pilotCos = 0;
160
+ const pilotSamples = Math.min(len, 15);
161
+ for (let i = len - 1; i >= len - pilotSamples; i--) {
162
+ pilotSin += bufferArray[i].sin;
163
+ pilotCos += bufferArray[i].cos;
164
+ }
165
+ const pilotRad = Math.atan2(pilotSin, pilotCos);
166
+
167
+ // Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
168
+ const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
169
+
170
+ // 2. CALCOLO AMMORTIZZATO
171
+ for (let i = len - 1; i >= 0; i--) {
172
+ const item = bufferArray[i];
173
+ if ((now - item.time) > windowMs) break;
174
+
175
+ // Troviamo la differenza angolare (da -Pi a +Pi) tra il dato e la Media Pilota
176
+ let diffRad = Math.atan2(
177
+ Math.sin(item.val - pilotRad),
178
+ Math.cos(item.val - pilotRad)
179
+ );
180
+
181
+ let finalSin, finalCos;
182
+
183
+ // Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
184
+ if (Math.abs(diffRad) > limitRad) {
185
+ // Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
186
+ const clampedDiff = Math.sign(diffRad) * limitRad;
187
+ // Ricalcoliamo l'angolo ammortizzato
188
+ const clampedRad = pilotRad + clampedDiff;
189
+
190
+ finalSin = Math.sin(clampedRad);
191
+ finalCos = Math.cos(clampedRad);
192
+ } else {
193
+ // Il dato è buono, usiamo i valori precalcolati
194
+ finalSin = item.sin;
195
+ finalCos = item.cos;
196
+ }
197
+
198
+ sSin += finalSin;
199
+ sCos += finalCos;
200
+
201
+ if (count === 0) newestTime = item.time;
202
+ oldestTime = item.time;
203
+ count++;
204
+ }
205
+
206
+ if (count === 0) return null;
138
207
 
139
- // Calcolo della Deviazione Standard Circolare (±) in gradi
140
- let deviation = (R < 1 && R > 0) ? Math.round(Math.sqrt(-2 * Math.log(R)) * (180 / Math.PI)) : 0;
208
+ const R = Math.hypot(sSin, sCos) / count;
209
+ const avgRad = Math.atan2(sSin, sCos);
210
+ const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
211
+ const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
212
+ const safeR = Math.max(R, 1e-9);
141
213
 
142
214
  return {
143
- val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI),
144
- stable: isStable,
145
- dev: deviation
215
+ val: finalVal,
216
+ stable: historyDuration > 10000 && R > CONFIG.averaging.stabilityThreshold,
217
+ dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
218
+ samples: count
146
219
  };
147
220
  }
148
221
 
@@ -291,14 +364,58 @@ function computeTrueWind() {
291
364
  }
292
365
  }
293
366
 
294
- function processIncomingData(path, val) {
295
- const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
296
- if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
297
- const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
298
- if (twPaths.includes(path)) twDirty = true;
299
- if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
300
- if (path === "navigation.headingTrue") { safePush(store.smoothBuf.hdg, val, now); safePush(store.longBuf.hdg, val, now); }
301
- if (path === "navigation.courseOverGroundTrue") { safePush(store.smoothBuf.cog, val, now); safePush(store.longBuf.cog, val, now); }
367
+ function processIncomingData(path, val, source) {
368
+ const now = Date.now();
369
+
370
+ // --- 1. FILTRO SORGENTE STICKY (Elimina conflitti yacht_device vs Unknown) ---
371
+ if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
372
+ sourceLocks[path] = { label: source, lastSeen: now };
373
+ } else {
374
+ // Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
375
+ return;
376
+ }
377
+
378
+ // --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
379
+ store.timestamps[path] = now;
380
+ store.raw[path] = val;
381
+
382
+ // Buffer per AWA (Vento Apparente)
383
+ if (path === "environment.wind.angleApparent") {
384
+ safePush(store.smoothBuf.awa, val, now);
385
+ safePush(store.longBuf.awa, val, now);
386
+ }
387
+
388
+ // Buffer per HDG (Prua)
389
+ if (path === "navigation.headingTrue") {
390
+ safePush(store.smoothBuf.hdg, val, now);
391
+ safePush(store.longBuf.hdg, val, now);
392
+ }
393
+
394
+ // Buffer per COG (Rotta Fondo)
395
+ if (path === "navigation.courseOverGroundTrue") {
396
+ safePush(store.smoothBuf.cog, val, now);
397
+ safePush(store.longBuf.cog, val, now);
398
+ }
399
+
400
+ // --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
401
+ const twPaths = [
402
+ "environment.wind.speedApparent",
403
+ "environment.wind.angleApparent",
404
+ "navigation.speedThroughWater",
405
+ "navigation.speedOverGround",
406
+ "navigation.headingTrue",
407
+ "navigation.courseOverGroundTrue"
408
+ ];
409
+
410
+ if (twPaths.includes(path)) {
411
+ twDirty = true;
412
+ // Calcolo limitato a 10Hz per non pesare sulla CPU
413
+ if (twDirty && (now - lastTWCompute > 100)) {
414
+ computeTrueWind();
415
+ lastTWCompute = now;
416
+ twDirty = false;
417
+ }
418
+ }
302
419
  }
303
420
 
304
421
  // ==========================================================================
@@ -312,8 +429,9 @@ function updateWindTrend() {
312
429
 
313
430
  // STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
314
431
  const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
315
- const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, 1800000, false); // 1.800.000 ms = 30 min
316
-
432
+ const multiplier = isNavigating ? 1 : 2;
433
+ const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
434
+ const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
317
435
  if (!twaNow || !twaRef || !twdNow || !twdRef) return;
318
436
  const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
319
437
  const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
@@ -357,12 +475,41 @@ function updateWindTrend() {
357
475
  }
358
476
  } else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
359
477
 
360
- // ALLARME STRAMBATA (GYBE)
361
- const instTwa = radToDeg(store.raw["environment.wind.angleTrueWater"] || 0);
362
- if (Math.abs(instTwa) > 155 && Math.sign(instTwa) !== Math.sign(lastInstantTwa)) {
363
- if (isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
364
- }
365
- lastInstantTwa = instTwa;
478
+ // --- LOGICA ALLARME STRAMBATA (GYBE) FILTRATA ---
479
+ const instTwaRad = store.raw["environment.wind.angleTrueWater"];
480
+
481
+ if (instTwaRad !== undefined) {
482
+ // Calcolo Alfa basato sulla Steering Precision (più precisione = filtro più lento e stabile)
483
+ const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
484
+
485
+ const currentSin = Math.sin(instTwaRad);
486
+ const currentCos = Math.cos(instTwaRad);
487
+
488
+ if (firstEmaRun) {
489
+ emaTwaSin = currentSin;
490
+ emaTwaCos = currentCos;
491
+ firstEmaRun = false;
492
+ } else {
493
+ // Media Esponenziale Vettoriale
494
+ emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
495
+ emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
496
+ }
497
+
498
+ // Angolo risultante filtrato
499
+ const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
500
+
501
+ // Verifichiamo il cambio di mure (> 155° e inversione di segno confermata dal filtro)
502
+ if (Math.abs(smoothedTwaDeg) > 155) {
503
+ if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
504
+ if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
505
+ lastGybeAlarmTime = now;
506
+ playGybeAlarm();
507
+ console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
508
+ }
509
+ }
510
+ }
511
+ lastInstantTwa = smoothedTwaDeg;
512
+ }
366
513
  }
367
514
 
368
515
  // ==========================================================================
@@ -385,12 +532,13 @@ function refreshGraph(t) {
385
532
  * upUI: Aggiornamento valori digitali con logica anti-ritardo (Istantaneo vs Media)
386
533
  */
387
534
  const upUI = (el, obj, instantRaw, isCompass = false) => {
388
- if (!obj || obj.val === null || instantRaw === undefined) {
389
- el.innerHTML = "---&deg;"; el.classList.remove('unstable-data');
535
+ if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
536
+ el.innerHTML = "---&deg;";
537
+ el.classList.remove('unstable-data');
390
538
  } else {
391
539
  let valDeg = Math.round(radToDeg(obj.val));
392
540
  let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "&deg;";
393
- let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.35em; opacity: 0.5; margin-left: 4px; vertical-align: middle;">&plusmn;${obj.dev}</span>` : "";
541
+ let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.8em; opacity: 0.4; margin-left: 6px;">&plusmn;${obj.dev}</span>` : "";
394
542
  el.innerHTML = mainVal + dev;
395
543
 
396
544
  let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
@@ -512,30 +660,39 @@ function startDisplayLoop() {
512
660
  upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
513
661
  upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
514
662
 
515
- // --- LOGICA TACK STRATEGICA (Riflessione geometrica su TWD) ---
663
+ // --- LOGICA TACK STRATEGICA (VETTORIALE) ---
516
664
  if (hObj && twdObj) {
517
- const tH = radToDeg((2 * twdObj.val - hObj.val + Math.PI * 2) % (Math.PI * 2));
518
- const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout || twdObj.dev > CONFIG.averaging.stabilityBreakout;
665
+ // Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
666
+ const reflectAngle = (targetRad, axisRad) => {
667
+ const diffSin = Math.sin(axisRad - targetRad);
668
+ const diffCos = Math.cos(axisRad - targetRad);
669
+ return Math.atan2(Math.sin(axisRad) * diffCos + Math.cos(axisRad) * diffSin,
670
+ Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
671
+ };
672
+
673
+ const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
519
674
 
520
675
  if (!isNavigating) {
521
- ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.remove('unstable-data');
676
+ ui.tackHdg.innerHTML = "---&deg;";
522
677
  } else if (unstableH) {
523
678
  ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data');
524
679
  } else {
525
- ui.tackHdg.innerHTML = `${Math.round((tH + 360) % 360).toString().padStart(3, '0')}&deg;`;
680
+ const reflectedH = reflectAngle(hObj.val, twdObj.val);
681
+ const outH = (radToDeg(reflectedH) + 360) % 360;
682
+ ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}&deg;`;
526
683
  ui.tackHdg.classList.remove('unstable-data');
527
684
  }
528
685
 
529
686
  if (cObj) {
530
- const tC = radToDeg((2 * twdObj.val - cObj.val + Math.PI * 2) % (Math.PI * 2));
531
- const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout || twdObj.dev > CONFIG.averaging.stabilityBreakout;
532
-
687
+ const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
533
688
  if (!isNavigating) {
534
- ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.remove('unstable-data');
689
+ ui.tackCog.innerHTML = "---&deg;";
535
690
  } else if (unstableC) {
536
691
  ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data');
537
692
  } else {
538
- ui.tackCog.innerHTML = `${Math.round((tC + 360) % 360).toString().padStart(3, '0')}&deg;`;
693
+ const reflectedC = reflectAngle(cObj.val, twdObj.val);
694
+ const outC = (radToDeg(reflectedC) + 360) % 360;
695
+ ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}&deg;`;
539
696
  ui.tackCog.classList.remove('unstable-data');
540
697
  }
541
698
  }
@@ -556,7 +713,7 @@ function startDisplayLoop() {
556
713
  // 8. CONFIGURAZIONE E GRAFICI UTILS
557
714
  // ==========================================================================
558
715
  /**
559
- * Recupera la configurazione e forza la sovrascrittura di ogni parametro.
716
+ * Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
560
717
  */
561
718
  async function fetchServerConfig() {
562
719
  try {
@@ -567,19 +724,27 @@ async function fetchServerConfig() {
567
724
  // Stampa di debug per verificare cosa riceve il client
568
725
  console.log("🔍 Configurazione ricevuta dal Server:", data);
569
726
 
570
- // Merge intelligente dei dati ricevuti nel CONFIG esistente
727
+ // Merge intelligente dei dati ricevuti
571
728
  Object.assign(CONFIG.alarms, data.alarms || {});
572
729
  Object.assign(CONFIG.graphs, data.graphs || {});
573
730
  Object.assign(CONFIG.averaging, data.averaging || {});
574
731
 
575
- // Per le scale, siccome sono nidificate, facciamo un loop
732
+ // --- LOGICA DI MIGRAZIONE SILENZIOSA ---
733
+ // Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
734
+ // Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
735
+ if (CONFIG.averaging.stabilityThreshold <= 0.85) {
736
+ CONFIG.averaging.stabilityThreshold = 0.95;
737
+ console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
738
+ }
739
+
740
+ // Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
576
741
  if (data.scales) {
577
742
  for (let key in data.scales) {
578
743
  if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
579
744
  }
580
745
  }
581
746
 
582
- console.log("✅ Configurazione applicata. Alarmi attivi:", CONFIG.alarms);
747
+ console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
583
748
  } catch (err) {
584
749
  console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
585
750
  }
@@ -691,10 +856,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
691
856
  style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
692
857
 
693
858
  if (i === 1) areaPath += `L ${x1} ${y1} `;
694
- areaPath += `L ${x2} ${y2} `;
695
- }
696
-
697
- areaPath += `L ${w} ${h} Z`;
859
+ areaPath += `L ${x2} ${y2} `;
860
+
861
+ // Salviamo l'ultima coordinata X calcolata per chiudere correttamente il path
862
+ if (i === d.length - 1) {
863
+ areaPath += `L ${x2} ${h} Z`;
864
+ }
865
+ }
698
866
 
699
867
  // 4. Iniezione del Gradiente
700
868
  const gradId = `grad-${id}`;
@@ -775,7 +943,22 @@ function connect() {
775
943
  try {
776
944
  socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
777
945
  socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
778
- socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
946
+
947
+ socket.onmessage = (e) => {
948
+ const d = JSON.parse(e.data);
949
+ if (d.updates) {
950
+ d.updates.forEach(u => {
951
+ // 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
952
+ const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
953
+
954
+ // 2. Passiamo il nome della sorgente come TERZO parametro
955
+ if (u.values) {
956
+ u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
957
+ }
958
+ });
959
+ }
960
+ };
961
+
779
962
  socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
780
963
  } catch (e) { setTimeout(connect, reconnectDelay); }
781
964
  }
package/index.js CHANGED
@@ -127,10 +127,10 @@ module.exports = function (app) {
127
127
  default: 0.5
128
128
  },
129
129
  stabilityThreshold: {
130
- type: 'number',
131
- title: 'Steering Precision (Sensitivity)',
132
- description: "Controls how strictly the system judges your course coherence. 0.95 requires pro precision; 0.85 is more realistic for cruising in waves.",
133
- default: 0.85
130
+ type: 'number',
131
+ title: 'Steering Precision (Sensitivity)',
132
+ description: "How strictly the system judges data coherence (0.0 to 1.0). Due to internal smoothing, 0.97-0.98 requires racing precision; 0.93-0.95 is ideal for cruising in waves. Below this, the display rarely alerts for instability.",
133
+ default: 0.95
134
134
  },
135
135
  stabilityBreakout: {
136
136
  type: 'number',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "4.0.19",
3
+ "version": "4.0.21",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/settings.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "alarms": {
3
3
  "depthDanger": 2.5,
4
- "depthWarning": 5.0
4
+ "depthWarning": 3.5
5
5
  },
6
6
  "graphs": {
7
7
  "reef1": 15.0,
8
8
  "reef2": 20.0,
9
- "historyMinutes": 5
9
+ "historyMinutes": 30
10
10
  },
11
11
  "averaging": {
12
12
  "longWindow": 30000,
13
13
  "smoothWindow": 2000,
14
14
  "minSpeed": 0.5,
15
- "stabilityThreshold": 0.85,
15
+ "stabilityThreshold": 0.95,
16
16
  "stabilityBreakout": 15
17
17
  },
18
18
  "scales": {
package/style.css CHANGED
@@ -122,6 +122,12 @@ body {
122
122
  .box-twd #twd-avg { font-size: clamp(1.4rem, 45cqh, 20cqw) !important; }
123
123
  }
124
124
 
125
+ /* --- OTTIMIZZAZIONE SPECIFICA TWD PER EVITARE ANDATA A CAPO in landscape--- */
126
+ .box-twd .value-large {
127
+ /* Riduciamo il limite di larghezza (18cqw) per far spazio al simbolo ± rimpicciolendo il testo se serve */
128
+ font-size: clamp(1.2rem, 55cqh, 18cqw) !important;
129
+ }
130
+
125
131
  /* --- BOX TACK (Mure Opposte) --- */
126
132
  .box-tack .dual-value-container { display: flex; flex-direction: row; justify-content: space-between; align-items: center; width: 100%; height: 100%; }
127
133
  .box-tack .dual-value-col { display: flex; flex-direction: column; justify-content: center; }
@@ -169,7 +175,7 @@ body.night-mode .alarm-danger {
169
175
 
170
176
  .focus-active .is-focused .sparkline path,
171
177
  .focus-active .is-focused .sparkline line {
172
- stroke-width: 1.5px !important;
178
+ stroke-width: 1.5px !important; /* Un briciolo più spessa per la stabilità visiva su schermi grandi */
173
179
  }
174
180
 
175
181
  .focus-active.focus-side-left {
@@ -435,10 +441,11 @@ body.night-mode {
435
441
 
436
442
  /* --- 10.3 GRAFICI (STILE DINAMICO NIGHT MODE) --- */
437
443
  .night-mode .graph-wrapper {
438
- background: rgba(10, 0, 0, 0.8) !important;
444
+ background: rgba(10, 0, 0, 0.8) !important; /* Sfondo scurissimo per alto contrasto */
439
445
  border: 1px solid #330000;
440
446
  }
441
447
 
448
+ /* Griglia temporale (minuti e livelli): quasi impercettibile */
442
449
  .night-mode .sparkline line[stroke="rgba(0,0,0,0.12)"],
443
450
  .night-mode .sparkline line[stroke="rgba(0,0,0,0.08)"] {
444
451
  stroke: rgba(255, 0, 0, 0.1) !important; /* Griglia temporale rossa soffusa */