@sailingrotevista/rotevista-dash 5.0.8 → 6.0.1

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 +28 -220
  2. package/index.js +182 -9
  3. package/package.json +1 -1
package/app.js CHANGED
@@ -616,14 +616,11 @@ 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);
620
619
  }
621
620
 
622
621
  // --- LOGICA SOG / VMG ---
623
622
  if (store.raw["navigation.speedOverGround"] !== undefined) {
624
623
  const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
625
- manageHistory('vmg', vmgVal);
626
- manageHistory('sog', sogKts);
627
624
 
628
625
  const labelSogVmg = document.getElementById('sog-vmg-label');
629
626
  if (displayModeSog === 'VMG') {
@@ -652,16 +649,12 @@ function startDisplayLoop() {
652
649
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
653
650
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
654
651
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
655
- manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
656
652
  }
657
653
 
658
654
  // --- GESTIONE VENTO (TWS / AWS SWITCH) ---
659
655
  const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
660
656
  const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
661
657
 
662
- if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
663
- if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
664
-
665
658
  if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
666
659
  const labelWind = document.getElementById('tws-aws-label');
667
660
  const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
@@ -801,7 +794,7 @@ async function fetchServerConfig() {
801
794
  applyConfigData(data);
802
795
  console.log("✅ Configurazione iniziale applicata con successo.");
803
796
  } catch (err) {
804
- console.warn("⚠️ Impossibile raggiungere il server per le configurazioni. Utilizzo default locali. Motivo:", err.message);
797
+ console.warn("⚠️ Impossibile raggiungere le configurazioni dal server. Utilizzo default.");
805
798
  }
806
799
  }
807
800
 
@@ -837,222 +830,25 @@ async function watchConfigChanges() {
837
830
  }
838
831
 
839
832
  /**
840
- * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
841
- * Integrazioni:
842
- * 1. Strict undefined check per lastUpdates.
843
- * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
844
- * 3. Clamping di sicurezza (no negativi, no Infinity).
845
- */
846
- function manageHistory(type, value) {
847
- // --- 1. VALIDAZIONE INPUT RIGOROSA ---
848
- if (value === undefined || value === null || !isFinite(value)) return;
849
-
850
- const now = Date.now();
851
- const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
852
- const samples = Math.max(2, CONFIG.graphs.samples || 60);
853
- const bucketIntervalMs = (historyMinutes * 60000) / samples;
854
-
855
- // --- 2. INIT SICURO (Strict Check) ---
856
- if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
857
- if (!store.histories[type]) store.histories[type] = [];
858
- if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
859
-
860
- const tempBuf = store.graphTempBuf[type];
861
-
862
- // --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
863
- if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
864
- const lastPoint = tempBuf[tempBuf.length - 1];
865
- const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
866
- if (lastPoint && lastPoint.val > glitchThreshold) return;
867
- }
868
-
869
- // --- 4. STORAGE TEMPORANEO ---
870
- tempBuf.push({ val: value, time: now });
871
-
872
- // Controllo finestra temporale
873
- const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
874
- if (!bucketReady) return;
875
-
876
- // --- 5. AGGREGAZIONE SEMANTICA ---
877
- let finalValue = value;
878
-
879
- if (tempBuf.length > 0) {
880
- // A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
881
- if (type === 'tws' || type === 'aws') {
882
- const tauMs = 2500;
883
- let ema = tempBuf[0].val;
884
- let maxSustained = ema;
885
-
886
- for (let i = 1; i < tempBuf.length; i++) {
887
- const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
888
- const alpha = 1 - Math.exp(-dt / tauMs);
889
- ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
890
- if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
891
- }
892
- finalValue = maxSustained;
893
- }
894
- // B. PROFONDITÀ -> MINIMO
895
- else if (type === 'depth') {
896
- const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
897
- if (vals.length > 0) finalValue = Math.min(...vals);
898
- }
899
- // C. VELOCITÀ -> MEDIA
900
- else {
901
- const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
902
- if (vals.length > 0) {
903
- const sum = vals.reduce((a, b) => a + b, 0);
904
- finalValue = sum / tempBuf.length;
905
- }
906
- }
907
- }
908
-
909
- // --- 6. CLAMPING E VALIDAZIONE FINALE ---
910
- if (!isFinite(finalValue)) return;
911
- finalValue = Math.max(0, finalValue);
912
-
913
- // --- 7. STORAGE STORICO ---
914
- store.histories[type].push({ val: finalValue, time: now });
915
-
916
- // --- 8. PRUNING DINAMICO ---
917
- const maxViewportMinutes = historyMinutes * 2;
918
- const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
919
-
920
- while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
921
- store.histories[type].shift();
922
- }
923
-
924
- // Reset per il prossimo bucket
925
- store.graphTempBuf[type] = [];
926
- store.lastUpdates[type] = now;
927
- }
928
-
929
- /**
930
- * Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
931
- * Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
833
+ * Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
932
834
  */
933
- function calculateScale(type, data, mode) {
934
- const s = CONFIG.scales[type];
935
- const currentVal = data[data.length - 1];
936
-
937
- // Fallback di emergenza se il buffer è momentaneamente vuoto
938
- if (currentVal === undefined || currentVal === null) {
939
- return { min: 0, max: s ? s.stdMax : 10 };
940
- }
941
-
942
- // Inizializzazione della memoria delle scale nello store se non esiste
943
- if (!store.herculesScales) store.herculesScales = {};
944
- if (!store.herculesScales[type]) {
945
- store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
946
- }
947
- let currentScale = store.herculesScales[type];
948
-
949
- // ==========================================================================
950
- // 1. SEZIONE PROFONDITÀ (DEDICATA A 2 MINUTI DI SICUREZZA, MINIMO FISSO A 0)
951
- // ==========================================================================
952
- if (type === 'depth') {
953
- const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
954
- if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
955
-
956
- const now = Date.now();
957
- const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
835
+ async function fetchServerHistory() {
836
+ try {
837
+ const response = await fetch('/rotevista-history');
838
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
839
+ const data = await response.json();
958
840
 
959
- // Estrazione dati reali degli ultimi 2 minuti con timestamp
960
- const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
961
- const recentVals = recentPoints.map(p => p.val);
962
-
963
- const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
964
- const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
965
-
966
- // --- 1.1 NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
967
- if (mode !== 'hercules') {
968
- if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
969
- store.depthProtectedActive = true;
970
- }
971
- if (store.depthProtectedActive) {
972
- if (localMin > shallowThreshold) {
973
- store.depthProtectedActive = false;
974
- }
975
- }
976
- if (store.depthProtectedActive) {
977
- return { min: 0, max: shallowThreshold };
978
- }
979
- const maxHistorico = Math.max(...data);
980
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
981
- }
982
-
983
- // --- 1.2 HERCULES PROFONDITÀ (0 IN BASSO, SNAP SUL MAX SENZA PADDING) ---
984
- if (mode === 'hercules') {
985
- const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
986
-
987
- let targetMin = 0;
988
- let targetMax = Math.ceil(localMax / roundStep) * roundStep;
989
-
990
- // Impediamo una scala inferiore a 4 metri per sicurezza visiva
991
- const absoluteMinSpan = 4;
992
- if (targetMax < absoluteMinSpan) {
993
- targetMax = absoluteMinSpan;
994
- }
995
-
996
- // Regola asimmetrica per il MAX (Espansione istantanea, contrazione a 2 minuti)
997
- if (currentVal > currentScale.max) {
998
- currentScale.max = targetMax;
999
- } else {
1000
- const allStableInTarget = recentVals.every(val => val <= targetMax);
1001
- if (allStableInTarget) {
1002
- currentScale.max = targetMax;
841
+ if (data && typeof data === 'object') {
842
+ for (let key in data) {
843
+ if (store.histories[key] !== undefined) {
844
+ store.histories[key] = data[key];
1003
845
  }
1004
846
  }
1005
-
1006
- currentScale.min = 0;
1007
- return { min: currentScale.min, max: currentScale.max };
847
+ console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
1008
848
  }
1009
- }
1010
-
1011
- // ==========================================================================
1012
- // 2. ALTRI GRAFICI (STW, SOG, TWS): COMPORTAMENTO ADATTIVO SU TERZO DEL GRAFICO
1013
- // ==========================================================================
1014
-
1015
- // --- 2.1 MODALITÀ NORMALE ---
1016
- if (mode !== 'hercules') {
1017
- const maxHistorico = Math.max(...data);
1018
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
1019
- }
1020
-
1021
- // --- 2.2 MODALITÀ HERCULES AD ALTO CONTRASTO (SNAP SENZA PADDING) ---
1022
- if (mode === 'hercules') {
1023
- const roundStep = s.hercSpan; // Passo di griglia (ex hercSpan)
1024
-
1025
- const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
1026
- const recentThirdData = data.slice(-oneThirdCount);
1027
-
1028
- const localMin = Math.min(...recentThirdData);
1029
- const localMax = Math.max(...recentThirdData);
1030
-
1031
- // Applichiamo lo snap diretto ai multipli della griglia
1032
- let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
1033
- let targetMax = Math.ceil(localMax / roundStep) * roundStep;
1034
-
1035
- // Se lo span calcolato è nullo (es. velocità costante), forziamo lo span minimo
1036
- if (targetMax - targetMin === 0) {
1037
- targetMax = targetMin + roundStep;
1038
- }
1039
-
1040
- // APPLICAZIONE REGOLE ASIMMETRICHE ANTI-CLIPPING
1041
- // A. Espansione ISTANTANEA in alto o in basso (sicurezza)
1042
- if (currentVal < currentScale.min || currentVal > currentScale.max) {
1043
- currentScale.min = Math.min(currentScale.min, targetMin);
1044
- currentScale.max = Math.max(currentScale.max, targetMax);
1045
- }
1046
- // B. Contrazione RITARDATA (solo se tutto il terzo recente si è assestato nel target)
1047
- else {
1048
- const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
1049
- if (allStableInTarget) {
1050
- currentScale.min = targetMin;
1051
- currentScale.max = targetMax;
1052
- }
1053
- }
1054
-
1055
- return { min: currentScale.min, max: currentScale.max };
849
+ } catch (err) {
850
+ console.warn("⚠️ Impossibile caricare lo storico dal server. Utilizzo dati vuoti/simulati.");
851
+ throw err; // Rilancia l'errore per far attivare il fallback nella funzione init()
1056
852
  }
1057
853
  }
1058
854
 
@@ -1128,7 +924,8 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
1128
924
  };
1129
925
 
1130
926
  let grids = "";
1131
- [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" />`);
927
+ // Una sola linea di griglia centrale al 50%, matematicamente sempre intera e allineata all'etichetta
928
+ [0.5].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" />`);
1132
929
 
1133
930
  const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
1134
931
  for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
@@ -1325,6 +1122,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
1325
1122
 
1326
1123
  async function init() {
1327
1124
  loadDashboardState();
1125
+
1126
+ // Prova a caricare lo storico dal server. Se fallisce (test offline), gestisce il fallback
1127
+ try {
1128
+ await fetchServerHistory();
1129
+ } catch (err) {
1130
+ if (simulationMode) {
1131
+ // Se sei in modalità simulazione locale, puoi generare dati finti qui (opzionale)
1132
+ console.log("🎮 Modalità Simulazione Locale attiva.");
1133
+ }
1134
+ }
1135
+
1328
1136
  await fetchServerConfig();
1329
1137
  startDisplayLoop();
1330
1138
  connect();
package/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * ==========================================================================
3
- * Rotevista Dash Configuration Plugin
3
+ * Rotevista Dash Configuration & History Plugin (Pro v6.0)
4
4
  * ==========================================================================
5
5
  * Definisce l'interfaccia di configurazione in Signal K Admin e crea
6
- * l'endpoint pubblico per la comunicazione con la Dashboard.
6
+ * gli endpoint pubblici per la Dashboard, mantenendo lo storico in RAM.
7
7
  */
8
8
 
9
9
  module.exports = function (app) {
@@ -14,40 +14,213 @@ module.exports = function (app) {
14
14
 
15
15
  let currentConfig = {};
16
16
  let routeRegistered = false;
17
+ let unsubscribes = [];
18
+
19
+ // Database dello storico in RAM sul server
20
+ let histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
21
+ let graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
22
+ let lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
23
+ let raw = {};
17
24
 
18
25
  /**
19
26
  * plugin.start: Inizializza il plugin.
20
27
  * Viene chiamato all'avvio e OGNI VOLTA che clicchi "Save" nelle impostazioni.
21
28
  */
22
29
  plugin.start = function (options) {
23
- // 1. Aggiorna la configurazione in memoria (per l'endpoint pubblico)
30
+ // 1. Aggiorna la configurazione in memoria
24
31
  currentConfig = options;
25
-
26
- // 2. Log di debug nel server Signal K
27
32
  app.debug(`${plugin.name} started/updated with new options`);
28
33
 
29
- // 3. Registra la rotta API solo la prima volta
34
+ // Reset dello storico al riavvio del plugin per evitare incoerenze
35
+ histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
36
+ graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
37
+ lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
38
+ raw = {};
39
+
40
+ // 2. Registra le rotte API solo la prima volta
30
41
  if (!routeRegistered) {
31
42
  app.get('/rotevista-config', (req, res) => {
32
43
  res.json(currentConfig);
33
44
  });
45
+ app.get('/rotevista-history', (req, res) => {
46
+ res.json(histories);
47
+ });
34
48
  routeRegistered = true;
35
- app.debug('Public API endpoint registered at /rotevista-config');
49
+ app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
36
50
  }
51
+
52
+ // 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
53
+ const localSubscription = {
54
+ context: 'vessels.self',
55
+ subscribe: [
56
+ { path: 'navigation.speedThroughWater' },
57
+ { path: 'navigation.speedOverGround' },
58
+ { path: 'environment.depth.belowTransducer' },
59
+ { path: 'environment.wind.speedApparent' },
60
+ { path: 'environment.wind.angleApparent' },
61
+ { path: 'navigation.headingTrue' },
62
+ { path: 'navigation.courseOverGroundTrue' }
63
+ ]
64
+ };
65
+
66
+ app.subscriptionmanager.subscribe(
67
+ localSubscription,
68
+ unsubscribes,
69
+ subscriptionError => {
70
+ app.error('Subscription error: ' + subscriptionError);
71
+ },
72
+ delta => {
73
+ if (delta.updates) {
74
+ delta.updates.forEach(update => {
75
+ if (update.values) {
76
+ update.values.forEach(v => {
77
+ processIncomingDelta(v.path, v.value);
78
+ });
79
+ }
80
+ });
81
+ }
82
+ }
83
+ );
37
84
  };
38
85
 
39
86
  /**
40
- * plugin.stop: Chiamato quando il plugin viene disattivato o prima di un aggiornamento.
87
+ * plugin.stop: Chiamato quando il plugin viene disattivato.
41
88
  */
42
89
  plugin.stop = function () {
90
+ unsubscribes.forEach(f => f());
91
+ unsubscribes = [];
43
92
  app.debug(`${plugin.name} stopped`);
44
93
  };
45
94
 
46
- // Se desideri avere una funzione plugin.debug personalizzata (opzionale)
47
95
  plugin.debug = function(msg) {
48
96
  app.debug(msg);
49
97
  };
50
98
 
99
+ /**
100
+ * processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
101
+ */
102
+ function processIncomingDelta(path, val) {
103
+ if (val === null || val === undefined) return;
104
+ raw[path] = val;
105
+
106
+ // Elaborazione e invio alla macchina a stati temporali dello storico
107
+ if (path === 'navigation.speedThroughWater') {
108
+ manageHistory('stw', val * 1.94384);
109
+ }
110
+ else if (path === 'navigation.speedOverGround') {
111
+ manageHistory('sog', val * 1.94384);
112
+ }
113
+ else if (path === 'environment.depth.belowTransducer') {
114
+ manageHistory('depth', val);
115
+ }
116
+ else if (path === 'environment.wind.speedApparent') {
117
+ manageHistory('aws', val * 1.94384);
118
+ }
119
+
120
+ // Calcolo combinato del Vento Reale (TWS) e della VMG a livello Server
121
+ const aws = raw["environment.wind.speedApparent"];
122
+ const awa = raw["environment.wind.angleApparent"];
123
+ const stw = raw["navigation.speedThroughWater"] || 0;
124
+ const sog = raw["navigation.speedOverGround"] || 0;
125
+ const hdg = raw["navigation.headingTrue"] || 0;
126
+ const cog = raw["navigation.courseOverGroundTrue"] || 0;
127
+
128
+ if (aws !== undefined && awa !== undefined) {
129
+ const awsKts = aws * 1.94384;
130
+ const stwKts = stw * 1.94384;
131
+ const tw_water_x = awsKts * Math.cos(awa) - stwKts;
132
+ const tw_water_y = awsKts * Math.sin(awa);
133
+ const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
134
+
135
+ manageHistory('tws', tws);
136
+
137
+ const twa = Math.atan2(tw_water_y, tw_water_x);
138
+ const vmg = Math.abs(stwKts * Math.cos(twa));
139
+ manageHistory('vmg', vmg);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * manageHistory: Versione Server-side dell'aggregatore matematico tattico
145
+ */
146
+ function manageHistory(type, value) {
147
+ if (value === undefined || value === null || !isFinite(value)) return;
148
+
149
+ const now = Date.now();
150
+ const historyMinutes = currentConfig.graphs ? currentConfig.graphs.historyMinutes : 5;
151
+ const samples = 60;
152
+ const bucketIntervalMs = (historyMinutes * 60000) / samples;
153
+
154
+ if (!graphTempBuf[type]) graphTempBuf[type] = [];
155
+ if (!histories[type]) histories[type] = [];
156
+ if (lastUpdates[type] === undefined) lastUpdates[type] = 0;
157
+
158
+ const tempBuf = graphTempBuf[type];
159
+
160
+ // Anti-dropout dinamico sul vento forte
161
+ if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
162
+ const lastPoint = tempBuf[tempBuf.length - 1];
163
+ const reef1 = currentConfig.graphs ? currentConfig.graphs.reef1 : 15;
164
+ const glitchThreshold = reef1 * 0.5;
165
+ if (lastPoint && lastPoint.val > glitchThreshold) return;
166
+ }
167
+
168
+ tempBuf.push({ val: value, time: now });
169
+
170
+ // Controllo avanzamento del secchiello temporale (Bucket)
171
+ const bucketReady = (now - lastUpdates[type] > bucketIntervalMs) || histories[type].length === 0;
172
+ if (!bucketReady) return;
173
+
174
+ let finalValue = value;
175
+
176
+ if (tempBuf.length > 0) {
177
+ // A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
178
+ if (type === 'tws' || type === 'aws') {
179
+ const tauMs = 2500;
180
+ let ema = tempBuf[0].val;
181
+ let maxSustained = ema;
182
+
183
+ for (let i = 1; i < tempBuf.length; i++) {
184
+ const dt = Math.max(1, tempBuf[i].time - tempBuf[i-1].time);
185
+ const alpha = 1 - Math.exp(-dt / tauMs);
186
+ ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
187
+ if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
188
+ }
189
+ finalValue = maxSustained;
190
+ }
191
+ // B. PROFONDITÀ -> MINIMO
192
+ else if (type === 'depth') {
193
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
194
+ if (vals.length > 0) finalValue = Math.min(...vals);
195
+ }
196
+ // C. VELOCITÀ -> MEDIA
197
+ else {
198
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
199
+ if (vals.length > 0) {
200
+ const sum = vals.reduce((a, b) => a + b, 0);
201
+ finalValue = sum / tempBuf.length;
202
+ }
203
+ }
204
+ }
205
+
206
+ if (!isFinite(finalValue)) return;
207
+ finalValue = Math.max(0, finalValue);
208
+
209
+ // Salvataggio nel ring buffer dello storico principale
210
+ histories[type].push({ val: finalValue, time: now });
211
+
212
+ // Pruning automatico basato sulle impostazioni di timeline
213
+ const maxViewportMinutes = historyMinutes * 2;
214
+ const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
215
+
216
+ while (histories[type].length > 0 && (now - histories[type][0].time) > maxHistoryMs) {
217
+ histories[type].shift();
218
+ }
219
+
220
+ graphTempBuf[type] = [];
221
+ lastUpdates[type] = now;
222
+ }
223
+
51
224
  /**
52
225
  * plugin.schema: Definisce l'interfaccia grafica in Signal K Admin.
53
226
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "5.0.8",
3
+ "version": "6.0.1",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {