@sailingrotevista/rotevista-dash 6.0.3 → 6.0.4

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 +124 -14
  2. package/index.js +13 -11
  3. 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.8 (Dynamic Envelope Architecture)
3
+ * Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
4
4
  * ==========================================================================
5
5
  * Autore: Sailing Rotevista
6
6
  * Motore di calcolo tattico per navigazione e crociera.
@@ -35,7 +35,7 @@ let CONFIG = {
35
35
  const RENDER_INTERVAL_MS = 1000;
36
36
  const TIMEOUT_MS = 15000;
37
37
  const SIM_SAMPLE_INTERVAL = 1000;
38
- const DASH_VERSION = "6.0"; // Major Update: Smart Source Locking & Breathing Hercules Scale
38
+ const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pro v6.0)
39
39
 
40
40
  // ==========================================================================
41
41
  // 2. STATO GLOBALE E RIFERIMENTI UI
@@ -616,11 +616,14 @@ function startDisplayLoop() {
616
616
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
617
617
  ui.stw.innerText = stwKts.toFixed(1);
618
618
  ui.stw.style.color = ""; // Neutro
619
+ manageHistory('stw', stwKts);
619
620
  }
620
621
 
621
622
  // --- LOGICA SOG / VMG ---
622
623
  if (store.raw["navigation.speedOverGround"] !== undefined) {
623
624
  const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
625
+ manageHistory('vmg', vmgVal);
626
+ manageHistory('sog', sogKts);
624
627
 
625
628
  const labelSogVmg = document.getElementById('sog-vmg-label');
626
629
  if (displayModeSog === 'VMG') {
@@ -649,12 +652,16 @@ function startDisplayLoop() {
649
652
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
650
653
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
651
654
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
655
+ manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
652
656
  }
653
657
 
654
658
  // --- GESTIONE VENTO (TWS / AWS SWITCH) ---
655
659
  const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
656
660
  const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
657
661
 
662
+ if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
663
+ if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
664
+
658
665
  if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
659
666
  const labelWind = document.getElementById('tws-aws-label');
660
667
  const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
@@ -687,7 +694,7 @@ function startDisplayLoop() {
687
694
  if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
688
695
 
689
696
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
690
- let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
697
+ let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
691
698
  smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
692
699
  curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
693
700
  ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
@@ -758,6 +765,16 @@ function startDisplayLoop() {
758
765
 
759
766
  let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
760
767
 
768
+ /**
769
+ * Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
770
+ */
771
+ function getApiUrl(path) {
772
+ if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
773
+ return `http://${CONFIG.server.fallbackIp}${path}`;
774
+ }
775
+ return path;
776
+ }
777
+
761
778
  /**
762
779
  * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
763
780
  */
@@ -784,7 +801,7 @@ function applyConfigData(data) {
784
801
  */
785
802
  async function fetchServerConfig() {
786
803
  try {
787
- const response = await fetch('/rotevista-config');
804
+ const response = await fetch(getApiUrl('/rotevista-config'));
788
805
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
789
806
  const data = await response.json();
790
807
 
@@ -803,7 +820,7 @@ async function fetchServerConfig() {
803
820
  */
804
821
  async function watchConfigChanges() {
805
822
  try {
806
- const response = await fetch('/rotevista-config');
823
+ const response = await fetch(getApiUrl('/rotevista-config'));
807
824
  if (!response.ok) return;
808
825
  const data = await response.json();
809
826
 
@@ -834,7 +851,7 @@ async function watchConfigChanges() {
834
851
  */
835
852
  async function fetchServerHistory() {
836
853
  try {
837
- const response = await fetch('/rotevista-history');
854
+ const response = await fetch(getApiUrl('/rotevista-history'));
838
855
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
839
856
  const data = await response.json();
840
857
 
@@ -852,6 +869,96 @@ async function fetchServerHistory() {
852
869
  }
853
870
  }
854
871
 
872
+ /**
873
+ * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
874
+ * Integrazioni:
875
+ * 1. Strict undefined check per lastUpdates.
876
+ * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
877
+ * 3. Clamping di sicurezza (no negativi, no Infinity).
878
+ */
879
+ function manageHistory(type, value) {
880
+ // --- 1. VALIDAZIONE INPUT RIGOROSA ---
881
+ if (value === undefined || value === null || !isFinite(value)) return;
882
+
883
+ const now = Date.now();
884
+ const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
885
+ const samples = Math.max(2, CONFIG.graphs.samples || 60);
886
+ const bucketIntervalMs = (historyMinutes * 60000) / samples;
887
+
888
+ // --- 2. INIT SICURO (Strict Check) ---
889
+ if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
890
+ if (!store.histories[type]) store.histories[type] = [];
891
+ if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
892
+
893
+ const tempBuf = store.graphTempBuf[type];
894
+
895
+ // --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
896
+ if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
897
+ const lastPoint = tempBuf[tempBuf.length - 1];
898
+ const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
899
+ if (lastPoint && lastPoint.val > glitchThreshold) return;
900
+ }
901
+
902
+ // --- 4. STORAGE TEMPORANEO ---
903
+ tempBuf.push({ val: value, time: now });
904
+
905
+ // Controllo finestra temporale
906
+ const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
907
+ if (!bucketReady) return;
908
+
909
+ // --- 5. AGGREGAZIONE SEMANTICA ---
910
+ let finalValue = value;
911
+
912
+ if (tempBuf.length > 0) {
913
+ // A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
914
+ if (type === 'tws' || type === 'aws') {
915
+ const tauMs = 2500;
916
+ let ema = tempBuf[0].val;
917
+ let maxSustained = ema;
918
+
919
+ for (let i = 1; i < tempBuf.length; i++) {
920
+ const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
921
+ const alpha = 1 - Math.exp(-dt / tauMs);
922
+ ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
923
+ if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
924
+ }
925
+ finalValue = maxSustained;
926
+ }
927
+ // B. PROFONDITÀ -> MINIMO
928
+ else if (type === 'depth') {
929
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
930
+ if (vals.length > 0) finalValue = Math.min(...vals);
931
+ }
932
+ // C. VELOCITÀ -> MEDIA
933
+ else {
934
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
935
+ if (vals.length > 0) {
936
+ const sum = vals.reduce((a, b) => a + b, 0);
937
+ finalValue = sum / tempBuf.length;
938
+ }
939
+ }
940
+ }
941
+
942
+ // --- 6. CLAMPING E VALIDAZIONE FINALE ---
943
+ if (!isFinite(finalValue)) return;
944
+ finalValue = Math.max(0, finalValue);
945
+
946
+ // --- 7. STORAGE STORICO ---
947
+ store.histories[type].push({ val: finalValue, time: now });
948
+
949
+ // --- 8. PRUNING DINAMICO ---
950
+ const maxViewportMinutes = historyMinutes * 2;
951
+ const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
952
+
953
+ while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
954
+ store.histories[type].shift();
955
+ }
956
+
957
+ // Reset per il prossimo bucket
958
+ store.graphTempBuf[type] = [];
959
+ store.lastUpdates[type] = now;
960
+ }
961
+
855
962
  /**
856
963
  * Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
857
964
  * Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
@@ -880,7 +987,7 @@ function calculateScale(type, data, mode) {
880
987
  if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
881
988
 
882
989
  const now = Date.now();
883
- const depthSafetyWindowMs = 120000; // 2 minuti di stabilizzazione fissi per la sicurezza
990
+ const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
884
991
 
885
992
  // Estrazione dati reali degli ultimi 2 minuti con timestamp
886
993
  const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
@@ -1015,6 +1122,7 @@ function refreshGraph(t) {
1015
1122
 
1016
1123
  /**
1017
1124
  * drawGraph: Motore SVG con Timeline Reale e Gestione GAP
1125
+ * Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
1018
1126
  */
1019
1127
  function drawGraph(d, id, min, max, isTws, isHercules) {
1020
1128
  const svg = document.getElementById(id);
@@ -1023,7 +1131,11 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
1023
1131
  const w = 200, h = 40;
1024
1132
  const range = max - min || 1;
1025
1133
  const isDepth = (id === 'depth-graph');
1026
- const now = Date.now();
1134
+
1135
+ // --- RISOLUZIONE DISALLINEAMENTO ORARIO ---
1136
+ // Usiamo il tempo del sensore (l'ultimo dato ricevuto) invece dell'orologio del tablet
1137
+ const latestPoint = d[d.length - 1];
1138
+ const now = latestPoint ? latestPoint.time : Date.now();
1027
1139
 
1028
1140
  const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
1029
1141
  const viewportMs = visibleMinutes * 60000;
@@ -1253,19 +1365,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
1253
1365
  async function init() {
1254
1366
  loadDashboardState();
1255
1367
 
1256
- // Prova a caricare lo storico dal server. Se fallisce (test offline), gestisce il fallback
1368
+ // Prova a caricare lo storico reale dal Cerbo GX tramite l'API deviata
1257
1369
  try {
1258
1370
  await fetchServerHistory();
1259
1371
  } catch (err) {
1260
- if (simulationMode) {
1261
- // Se sei in modalità simulazione locale, puoi generare dati finti qui (opzionale)
1262
- console.log("🎮 Modalità Simulazione Locale attiva.");
1263
- }
1372
+ console.warn("⚠️ Impossibile caricare lo storico dal server.");
1264
1373
  }
1265
1374
 
1266
1375
  await fetchServerConfig();
1267
1376
  startDisplayLoop();
1268
- connect();
1377
+ connect(); // Si collegherà in tempo reale al WebSocket del Cerbo (usando l'IP di fallback se sei su Mac)
1378
+
1269
1379
  setInterval(watchConfigChanges, 10000);
1270
1380
  }
1271
1381
 
package/index.js CHANGED
@@ -37,17 +37,19 @@ module.exports = function (app) {
37
37
  lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
38
38
  raw = {};
39
39
 
40
- // 2. Registra le rotte API solo la prima volta
41
- if (!routeRegistered) {
42
- app.get('/rotevista-config', (req, res) => {
43
- res.json(currentConfig);
44
- });
45
- app.get('/rotevista-history', (req, res) => {
46
- res.json(histories);
47
- });
48
- routeRegistered = true;
49
- app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
50
- }
40
+ // 2. Registra le rotte API solo la prima volta (Abilitate per CORS remoto)
41
+ if (!routeRegistered) {
42
+ app.get('/rotevista-config', (req, res) => {
43
+ res.header("Access-Control-Allow-Origin", "*"); // Sblocca la Dashboard locale su Mac
44
+ res.json(currentConfig);
45
+ });
46
+ app.get('/rotevista-history', (req, res) => {
47
+ res.header("Access-Control-Allow-Origin", "*"); // Sblocca la Dashboard locale su Mac
48
+ res.json(histories);
49
+ });
50
+ routeRegistered = true;
51
+ app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
52
+ }
51
53
 
52
54
  // 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
53
55
  const localSubscription = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "6.0.3",
3
+ "version": "6.0.4",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {