@sailingrotevista/rotevista-dash 7.0.31 → 7.0.33

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.
Files changed (3) hide show
  1. package/app.js +61 -57
  2. package/package.json +1 -1
  3. package/utils.js +7 -6
package/app.js CHANGED
@@ -323,6 +323,11 @@ function getSourcePriorityScore(sourceName) {
323
323
  }
324
324
 
325
325
  function processIncomingData(path, val, source, timeMs) {
326
+ // 0. Filtro di validità assoluta: ignora all'istante pacchetti vuoti, null o non numerici
327
+ if (val === null || val === undefined || (typeof val === 'number' && isNaN(val))) {
328
+ return;
329
+ }
330
+
326
331
  // 1. Filtro anti-spike vento (> 100 nodi / 51.44 m/s)
327
332
  if ((path === "environment.wind.speedApparent" || path === "environment.wind.speedTrue") && val > 51.44) {
328
333
  return;
@@ -348,21 +353,22 @@ function processIncomingData(path, val, source, timeMs) {
348
353
  } else {
349
354
  const currentLock = sourceLocks[path];
350
355
  const isSameSource = (currentLock.label === source);
351
- const isLockExpired = (now - currentLock.lastSeen > 12000); // Scadenza a 12 secondi
356
+ // Tolleranza 5s: ignora il GPS Victron durante le micro-pause di Yacht Device per evitare imprecisioni
357
+ const isLockExpired = (now - currentLock.lastSeen > 5000);
352
358
  const hasHigherPriority = (score > currentLock.score);
353
- // Consente lo switch tra dispositivi di pari priorità (es. AP e YD) se la sorgente precedente è inattiva da > 1.5s
359
+ // Switch immediato tra apparati principali di pari priorità (es. YD e AP) se inattivi da > 1.5s
354
360
  const isSamePriorityStale = (score === currentLock.score && (now - currentLock.lastSeen > 1500));
355
361
 
356
362
  if (isSameSource) {
357
363
  // Stessa sorgente: aggiorna il timestamp e mantiene il blocco
358
364
  currentLock.lastSeen = now;
359
365
  currentLock.score = score;
360
- } else if (isLockExpired || hasHigherPriority || isSamePriorityStale) {
361
- // Ruba il blocco se la sorgente precedente è scaduta, ha priorità superiore o è inattiva da 1.5s a pari priorità
366
+ } else if (hasHigherPriority || isLockExpired || isSamePriorityStale) {
367
+ // Passa a sorgenti secondarie (Victron) solo se Yacht Device è spento da > 5s
362
368
  sourceLocks[path] = { label: source, score: score, lastSeen: now };
363
369
  console.log(`🔌 [Smart Lock] Path "${path}" switched to: ${source} (Score: ${score})`);
364
370
  } else {
365
- // Rifiuta i dati da sorgenti a priorità inferiore se quella principale è attiva
371
+ // Scarta sorgenti imprecise di servizio mentre Yacht Device è la sorgente principale attiva
366
372
  return;
367
373
  }
368
374
  }
@@ -803,62 +809,60 @@ function startDisplayLoop() {
803
809
  saveDashboardState();
804
810
  }
805
811
 
806
- if (lastAvgUIUpdate % 3 === 0) {
807
- const baseThreshold = CONFIG.averaging.stabilityThreshold || 0.95;
808
- const breakout = CONFIG.averaging.stabilityBreakout || 15;
809
-
810
- // CALCOLO DIFFERENZIALE: Tolleranza del vento proporzionale alla sensibilità di base.
811
- // Rilassiamo la soglia del vento di 0.13 rispetto alla prua (es. se la prua richiede 0.95, il vento richiede 0.82)
812
- const windThreshold = Math.max(0.60, baseThreshold - 0.13);
812
+ // AGGIORNAMENTO FLUIDO MEDIE UI (Esecuzione a 1Hz costante ad ogni secondo)
813
+ const baseThreshold = CONFIG.averaging.stabilityThreshold || 0.95;
814
+ const breakout = CONFIG.averaging.stabilityBreakout || 15;
815
+
816
+ // CALCOLO DIFFERENZIALE: Tolleranza del vento proporzionale alla sensibilità di base
817
+ const windThreshold = Math.max(0.60, baseThreshold - 0.13);
813
818
 
814
- let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false, now, baseThreshold, breakout);
815
- let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false, now, baseThreshold, breakout);
816
-
817
- // Applichiamo la soglia differenziale ottimizzata per i tre dati legati al vento
818
- let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true, now, windThreshold, breakout);
819
- let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true, now, windThreshold, breakout);
820
- let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false, now, windThreshold, breakout);
821
-
822
- // --- GESTIONE DINAMICA ETICHETTA TACK/GYBE CON ISTERESI DI 10 GRADI ---
823
- if (ui.tackLabel) {
824
- const absTwa = Math.abs(radToDeg(store.raw["environment.wind.angleTrueWater"] || 0));
825
- if (currentTackLabelMode === 'TACK' && absTwa > 95) {
826
- currentTackLabelMode = 'GYBE';
827
- } else if (currentTackLabelMode === 'GYBE' && absTwa < 85) {
828
- currentTackLabelMode = 'TACK';
829
- }
830
- safeSetText(ui.tackLabel, currentTackLabelMode);
819
+ let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false, now, baseThreshold, breakout);
820
+ let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false, now, baseThreshold, breakout);
821
+
822
+ // Applichiamo la soglia differenziale ottimizzata per i tre dati legati al vento
823
+ let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true, now, windThreshold, breakout);
824
+ let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true, now, windThreshold, breakout);
825
+ let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false, now, windThreshold, breakout);
826
+
827
+ // --- GESTIONE DINAMICA ETICHETTA TACK/GYBE CON ISTERESI DI 10 GRADI ---
828
+ if (ui.tackLabel) {
829
+ const absTwa = Math.abs(radToDeg(store.raw["environment.wind.angleTrueWater"] || 0));
830
+ if (currentTackLabelMode === 'TACK' && absTwa > 95) {
831
+ currentTackLabelMode = 'GYBE';
832
+ } else if (currentTackLabelMode === 'GYBE' && absTwa < 85) {
833
+ currentTackLabelMode = 'TACK';
831
834
  }
835
+ safeSetText(ui.tackLabel, currentTackLabelMode);
836
+ }
832
837
 
833
- upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
834
- upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
835
- upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
836
- upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
837
- upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
838
-
839
- if (hObj && twdObj) {
840
- const reflectAngle = (targetRad, axisRad) => {
841
- const dS = Math.sin(axisRad - targetRad);
842
- const dC = Math.cos(axisRad - targetRad);
843
- return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
844
- };
845
- const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
846
- if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
847
- else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
838
+ upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
839
+ upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
840
+ upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
841
+ upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
842
+ upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
843
+
844
+ if (hObj && twdObj) {
845
+ const reflectAngle = (targetRad, axisRad) => {
846
+ const dS = Math.sin(axisRad - targetRad);
847
+ const dC = Math.cos(axisRad - targetRad);
848
+ return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
849
+ };
850
+ const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
851
+ if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
852
+ else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
853
+ else {
854
+ const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
855
+ ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
856
+ ui.tackHdg.classList.remove('unstable-data');
857
+ }
858
+ if (cObj) {
859
+ const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
860
+ if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
861
+ else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
848
862
  else {
849
- const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
850
- ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
851
- ui.tackHdg.classList.remove('unstable-data');
852
- }
853
- if (cObj) {
854
- const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
855
- if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
856
- else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
857
- else {
858
- const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
859
- ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
860
- ui.tackCog.classList.remove('unstable-data');
861
- }
863
+ const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
864
+ ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
865
+ ui.tackCog.classList.remove('unstable-data');
862
866
  }
863
867
  }
864
868
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "7.0.31",
3
+ "version": "7.0.33",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/utils.js CHANGED
@@ -36,10 +36,9 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
36
36
  const len = bufferArray.length;
37
37
  if (len === 0) return null;
38
38
 
39
- // RIFERIMENTO TEMPORALE IMMUNE DA DESINCRONIZZAZIONE:
40
- // Usiamo come 'ora attuale' l'ultimo timestamp presente nel buffer stesso,
41
- // garantendo che la finestra di 2s sia sempre calcolata sui dati NMEA reali arrivati.
39
+ // RIFERIMENTO TEMPORALE IMMUNE DA DESINCRONIZZAZIONE (Usa l'ultimo dato del buffer)
42
40
  const referenceTime = bufferArray[len - 1].time;
41
+
43
42
  let sSin = 0, sCos = 0, count = 0;
44
43
  let newestTime = 0, oldestTime = 0;
45
44
 
@@ -54,7 +53,7 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
54
53
 
55
54
  for (let i = len - 1; i >= 0; i--) {
56
55
  const item = bufferArray[i];
57
- if ((referenceTime - item.time) > windowMs) break; // Usa referenceTime invece di 'now'
56
+ if ((referenceTime - item.time) > windowMs) break;
58
57
 
59
58
  let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
60
59
  let finalSin, finalCos;
@@ -82,12 +81,14 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
82
81
  const R = Math.hypot(sSin, sCos) / count;
83
82
  const avgRad = Math.atan2(sSin, sCos);
84
83
  const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
85
- const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
86
84
  const safeR = Math.max(R, 1e-9);
87
85
 
86
+ // Stabilità valida se ci sono almeno 2 campioni nel buffer e la varianza radiale supera la soglia
87
+ const isStable = (count >= 2) && (R > stabilityThreshold);
88
+
88
89
  return {
89
90
  val: finalVal,
90
- stable: historyDuration > 10000 && R > stabilityThreshold,
91
+ stable: isStable,
91
92
  dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
92
93
  samples: count
93
94
  };