@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 +18 -1
- package/index.js +132 -102
- package/package.json +1 -1
- /package/{sample_radar.html → debug_sample_radar.html} +0 -0
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: "
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
File without changes
|