@sailingrotevista/rotevista-dash 7.0.2 → 7.0.5

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
@@ -30,7 +30,7 @@ let CONFIG = {
30
30
  tws: { stdMax: 15, hercSpan: 2, step: 1 },
31
31
  depth: { stdMax: 5, hercSpan: 2, step: 1 }
32
32
  },
33
- server: { fallbackIp: "192.168.111.240:3000" }
33
+ server: { fallbackIp: "venus.local:3000" }
34
34
  };
35
35
 
36
36
  const RENDER_INTERVAL_MS = 1000;
@@ -47,6 +47,7 @@ let displayModeTws = 'TWS';
47
47
  let activeInstrument = 'gauge'; // Modalità di default all'avvio: 'gauge' (analogico) o 'radar' (storico)
48
48
  let socket, renderInterval, simInterval;
49
49
  let lastAvgUIUpdate = 0; // Chirurgico: Rimosse audioCtx e lastAlarmTime poiché sono già dichiarate in utils.js
50
+ const lastPathProcessTimes = {}; // Registro dei timestamp per limitazione di frequenza client-side a 1Hz
50
51
 
51
52
  let rotationTrend = 0, meteoTrend = 0;
52
53
  let lastShortAvgVal = null, lastInstantTwa = null;
@@ -320,9 +321,25 @@ function processIncomingData(path, val, source, timeMs) {
320
321
  }
321
322
  }
322
323
 
324
+ // Aggiorna sempre lo stato raw istantaneo all'ultimo millisecondo per massimizzare la precisione digitale
323
325
  store.timestamps[path] = now;
324
326
  store.raw[path] = val;
325
327
 
328
+ // Le coordinate GPS e la posizione non sono soggette alla limitazione a 1Hz
329
+ if (path === "navigation.position") {
330
+ return; // Esce subito (non richiede calcoli trigonometrici o inserimenti in smoothBuf/longBuf)
331
+ }
332
+
333
+ // LIMITATORE DI FREQUENZA (RATE LIMITER) CLIENT-SIDE A 1HZ PER PERCORSO ATTIVO:
334
+ // Evita di eseguire calcoli trigonometrici, allocare oggetti in memoria dinamica
335
+ // e popolare i buffer smoothBuf/longBuf decine di volte al secondo per singolo sensore.
336
+ if (!lastPathProcessTimes[path]) lastPathProcessTimes[path] = 0;
337
+ if (now - lastPathProcessTimes[path] < 1000) {
338
+ return; // Esce subito risparmiando cicli di calcolo del browser e batteria del tablet
339
+ }
340
+ lastPathProcessTimes[path] = now;
341
+
342
+ // Da qui in poi, l'inserimento nei buffer fisici avviene rigorosamente a 1Hz:
326
343
  if (path === "environment.wind.angleApparent") {
327
344
  safePush(store.smoothBuf.awa, val, now);
328
345
  safePush(store.longBuf.awa, val, now);
package/index.js CHANGED
@@ -24,11 +24,12 @@ module.exports = function (app) {
24
24
  const CALM_THRESHOLD_KTS = 1.5; // Soglia di calma piatta (anello a 360°)
25
25
  const PRESSURE_FILTER_RATIO = 0.40; // Filtro di pressione dinamico (40% del picco per ignorare i cali)
26
26
 
27
- // Database dello storico in RAM sul server (Sintonizzato Pro v6.0)
27
+ // Database dello storico in RAM sul server (Sintonizzato Pro v6.0)
28
28
  let histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] };
29
29
  let graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] };
30
30
  let lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0, twd: 0 };
31
31
  let raw = {};
32
+ let lastPathProcessTimes = {}; // Registro dei timestamp per limitazione di frequenza a 1Hz
32
33
 
33
34
  // Nuovo database dedicato per gli archi storici della bussola (6 ore = 12 slot)
34
35
  let windRadarSlots = [];
@@ -84,24 +85,24 @@ module.exports = function (app) {
84
85
  app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
85
86
  }
86
87
 
87
- // 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
88
- const localSubscription = {
89
- context: 'vessels.self',
90
- subscribe: [
91
- { path: 'navigation.position' }, // Chirurgico: Aggiunto l'ascolto della posizione GPS per abilitare le previsioni
92
- { path: 'navigation.speedThroughWater' },
93
- { path: 'navigation.speedOverGround' },
94
- { path: 'environment.depth.belowTransducer' },
95
- { path: 'environment.wind.speedApparent' },
96
- { path: 'environment.wind.angleApparent' },
97
- { path: 'environment.wind.speedTrue' }, // Aggiunto chirurgicamente
98
- { path: 'environment.wind.directionTrue' }, // Aggiunto chirurgicamente
99
- { path: 'navigation.headingTrue' },
100
- { path: 'navigation.headingMagnetic' },
101
- { path: 'navigation.magneticVariation' },
102
- { path: 'navigation.courseOverGroundTrue' }
103
- ]
104
- };
88
+ // 3. Iscrizione ai dati dei sensori di bordo tramite Signal K (Ottimizzata a 1Hz)
89
+ const localSubscription = {
90
+ context: 'vessels.self',
91
+ subscribe: [
92
+ { path: 'navigation.position', minPeriod: 1000 },
93
+ { path: 'navigation.speedThroughWater', minPeriod: 1000 },
94
+ { path: 'navigation.speedOverGround', minPeriod: 1000 },
95
+ { path: 'environment.depth.belowTransducer', minPeriod: 1000 },
96
+ { path: 'environment.wind.speedApparent', minPeriod: 1000 },
97
+ { path: 'environment.wind.angleApparent', minPeriod: 1000 },
98
+ { path: 'environment.wind.speedTrue', minPeriod: 1000 },
99
+ { path: 'environment.wind.directionTrue', minPeriod: 1000 },
100
+ { path: 'navigation.headingTrue', minPeriod: 1000 },
101
+ { path: 'navigation.headingMagnetic', minPeriod: 1000 },
102
+ { path: 'navigation.magneticVariation', minPeriod: 1000 },
103
+ { path: 'navigation.courseOverGroundTrue', minPeriod: 1000 }
104
+ ]
105
+ };
105
106
 
106
107
  app.subscriptionmanager.subscribe(
107
108
  localSubscription,
@@ -136,96 +137,125 @@ module.exports = function (app) {
136
137
  app.debug(msg);
137
138
  };
138
139
 
139
- /**
140
- * processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
141
- * Gestisce l'architettura "Nativo Prima, Fallback Dopo" per il vento reale.
142
- */
143
- function processIncomingDelta(path, val) {
144
- if (val === null || val === undefined) return;
145
- raw[path] = val;
140
+ /**
141
+ * processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
142
+ * Applica un filtro passa-basso continuo a ogni pacchetto e storicizza a 1Hz con dati stabilizzati.
143
+ */
144
+ function processIncomingDelta(path, val) {
145
+ if (val === null || val === undefined) return;
146
+
147
+ const now = Date.now();
148
+ const alpha = 1.0; // Passa-tutto istantaneo (i sensori di bordo ST60+ sono già calibrati con damping hardware a 12)
149
+
150
+ // FILTRO PASSA-BASSO CONTINUO IN TEMPO REALE (Previene gli Spike prima della storicizzazione)
151
+ if (path === 'navigation.position') {
152
+ raw[path] = val; // Le coordinate GPS non sono soggette a filtri di smorzamento o ritardo
153
+
154
+ // Chiamata periodica Open-Meteo
155
+ if (val.latitude !== undefined && val.longitude !== undefined) {
156
+ const current30mSlot = Math.floor(now / 1800000) * 1800000;
157
+ if (current30mSlot > lastForecast30mSlot) {
158
+ fetchOpenMeteoForecast(val, current30mSlot);
159
+ }
160
+ }
161
+ return; // Esce subito
162
+ }
146
163
 
147
- const now = Date.now();
164
+ if (path.includes('angle') || path.includes('heading') || path.includes('course') || path.includes('direction') || path.includes('twd')) {
165
+ // 1. Caso Angolare (Radianti): Calcolo differenziale circolare per gestire l'oltrepasso dello 0/360 gradi
166
+ if (raw[path] !== undefined) {
167
+ let diff = Math.atan2(Math.sin(val - raw[path]), Math.cos(val - raw[path]));
168
+ raw[path] = (raw[path] + diff * alpha + Math.PI * 2) % (Math.PI * 2);
169
+ } else {
170
+ raw[path] = val;
171
+ }
172
+ } else {
173
+ // 2. Caso Lineare (Velocità e Profondità): Smorzamento continuo
174
+ if (raw[path] !== undefined) {
175
+ raw[path] = (val * alpha) + (raw[path] * (1 - alpha));
176
+ } else {
177
+ raw[path] = val;
178
+ }
179
+ }
148
180
 
149
- // 1. Cattura dei dati nativi (Se presenti, li scrive direttamente nello storico)
150
- if (path === 'navigation.position') {
151
- // Trigger allineato all'orologio: calcoliamo il confine della mezz'ora corrente dell'orologio
152
- if (val && val.latitude !== undefined && val.longitude !== undefined) {
153
- const current30mSlot = Math.floor(Date.now() / 1800000) * 1800000;
154
- // Se siamo entrati in una nuova mezz'ora di orologio dall'ultimo download, avviamo il fetch
155
- if (current30mSlot > lastForecast30mSlot) {
156
- fetchOpenMeteoForecast(val, current30mSlot);
181
+ // LIMITATORE DI FREQUENZA (RATE LIMITER) A 1HZ PER PERCORSO ATTIVO:
182
+ // La scrittura nello storico e i calcoli derivati vengono eseguiti al massimo una volta al secondo,
183
+ // leggendo il valore "raw[path]" stabilizzato continuamente dal filtro passa-basso superiore.
184
+ if (!lastPathProcessTimes[path]) lastPathProcessTimes[path] = 0;
185
+ if (now - lastPathProcessTimes[path] < 1000) {
186
+ return; // Esce subito risparmiando la CPU se il sensore ha già aggiornato nell'ultimo secondo
157
187
  }
158
- }
159
- }
160
- else if (path === 'navigation.speedThroughWater') {
161
- manageHistory('stw', val * 1.94384);
162
- }
163
- else if (path === 'navigation.speedOverGround') {
164
- manageHistory('sog', val * 1.94384);
165
- }
166
- else if (path === 'environment.depth.belowTransducer') {
167
- manageHistory('depth', val);
168
- }
169
- else if (path === 'environment.wind.speedApparent') {
170
- manageHistory('aws', val * 1.94384);
171
- }
172
- else if (path === 'environment.wind.angleApparent') {
173
- raw[path] = val; // BUG RISOLTO: Acquisizione dell'AWA mancante inserita!
174
- }
175
- else if (path === 'environment.wind.speedTrue') {
188
+ lastPathProcessTimes[path] = now;
189
+
190
+ // Da qui in poi, l'esecuzione della storia e del vento reale avviene rigorosamente a 1Hz con dati puliti:
191
+ const smoothedVal = raw[path];
192
+
193
+ if (path === 'navigation.speedThroughWater') {
194
+ manageHistory('stw', smoothedVal * 1.94384);
195
+ }
196
+ else if (path === 'navigation.speedOverGround') {
197
+ manageHistory('sog', smoothedVal * 1.94384);
198
+ }
199
+ else if (path === 'environment.depth.belowTransducer') {
200
+ manageHistory('depth', smoothedVal);
201
+ }
202
+ else if (path === 'environment.wind.speedApparent') {
203
+ manageHistory('aws', smoothedVal * 1.94384);
204
+ }
205
+ else if (path === 'environment.wind.angleApparent') {
206
+ // Già gestito e normalizzato dal filtro passa-basso superiore
207
+ }
208
+ else if (path === 'environment.wind.speedTrue') {
176
209
  lastNativeTwsTime = now; // Rilevato TWS nativo della centralina!
177
- const cleanVal = (val && typeof val === 'object' && val.value !== undefined) ? val.value : val;
178
- manageHistory('tws', cleanVal * 1.94384);
179
- }
180
- // --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
181
- else if (path === 'navigation.headingMagnetic') {
182
- const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
183
- if (!hasTrueHdg) {
184
- const variation = raw['navigation.magneticVariation'] || 0;
185
- const cleanVal = (val && typeof val === 'object' && val.value !== undefined) ? val.value : val;
186
- raw['navigation.headingTrue'] = (cleanVal + variation + 2 * Math.PI) % (2 * Math.PI);
187
- }
188
- }
189
- else if (path === 'environment.wind.directionTrue') {
190
- lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
191
- const cleanVal = (val && typeof val === 'object' && val.value !== undefined) ? val.value : val;
192
- manageHistory('twd', cleanVal);
193
- }
210
+ manageHistory('tws', smoothedVal * 1.94384);
211
+ }
212
+ // --- DECODIFICA PRUA MAGNETICA SERVER-SIDE ---
213
+ else if (path === 'navigation.headingMagnetic') {
214
+ const hasTrueHdg = raw['navigation.headingTrue'] !== undefined;
215
+ if (!hasTrueHdg) {
216
+ const variation = raw['navigation.magneticVariation'] || 0;
217
+ raw['navigation.headingTrue'] = (smoothedVal + variation + 2 * Math.PI) % (2 * Math.PI);
218
+ }
219
+ }
220
+ else if (path === 'environment.wind.directionTrue') {
221
+ lastNativeTwdTime = now; // Rilevato TWD nativo della centralina!
222
+ manageHistory('twd', smoothedVal);
223
+ }
194
224
 
195
- // 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
196
- const aws = raw["environment.wind.speedApparent"];
197
- const awa = raw["environment.wind.angleApparent"];
198
- const stw = raw["navigation.speedThroughWater"] || 0;
199
- const sog = raw["navigation.speedOverGround"] || 0;
200
- const hdg = raw["navigation.headingTrue"];
201
- const cog = raw["navigation.courseOverGroundTrue"] || 0;
202
-
203
- if (aws !== undefined && awa !== undefined) {
204
- const awsKts = aws * 1.94384;
205
- const stwKts = stw * 1.94384;
206
- const tw_water_x = awsKts * Math.cos(awa) - stwKts;
207
- const tw_water_y = awsKts * Math.sin(awa);
208
-
209
- // Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
210
- if (now - lastNativeTwsTime > 5000) {
211
- const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
212
- manageHistory('tws', tws);
213
- }
225
+ // 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
226
+ const aws = raw["environment.wind.speedApparent"];
227
+ const awa = raw["environment.wind.angleApparent"];
228
+ const stw = raw["navigation.speedThroughWater"] || 0;
229
+ const sog = raw["navigation.speedOverGround"] || 0;
230
+ const hdg = raw["navigation.headingTrue"];
231
+ const cog = raw["navigation.courseOverGroundTrue"] || 0;
232
+
233
+ if (aws !== undefined && awa !== undefined) {
234
+ const awsKts = aws * 1.94384;
235
+ const stwKts = stw * 1.94384;
236
+ const tw_water_x = awsKts * Math.cos(awa) - stwKts;
237
+ const tw_water_y = awsKts * Math.sin(awa);
238
+
239
+ // Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
240
+ if (now - lastNativeTwsTime > 5000) {
241
+ const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
242
+ manageHistory('tws', tws);
243
+ }
214
244
 
215
- const twa = Math.atan2(tw_water_y, tw_water_x);
216
-
217
- // La VMG viene sempre calcolata a livello server poiché raramente è nativa
218
- const vmg = Math.abs(stwKts * Math.cos(twa));
219
- manageHistory('vmg', vmg);
220
-
221
- // Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
222
- if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
223
- const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
224
- manageHistory('twd', twd); // BUG RISOLTO: Rimossa la riassegnazione di "const" che mandava in crash il server
225
- }
226
- }
227
- }
245
+ const twa = Math.atan2(tw_water_y, tw_water_x);
246
+
247
+ // La VMG viene sempre calcolata a livello server poiché raramente è nativa
248
+ const vmg = Math.abs(stwKts * Math.cos(twa));
249
+ manageHistory('vmg', vmg);
228
250
 
251
+ // Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
252
+ if (hdg !== undefined && (now - lastNativeTwdTime > 5000)) {
253
+ const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
254
+ manageHistory('twd', twd);
255
+ }
256
+ }
257
+ }
258
+
229
259
  /**
230
260
  * manageHistory: Versione Server-side dell'aggregatore matematico tattico
231
261
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "7.0.2",
3
+ "version": "7.0.5",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
File without changes