@sailingrotevista/rotevista-dash 7.0.12 → 7.0.14
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 +44 -10
- package/index.js +33 -7
- package/package.json +1 -1
- package/style.css +37 -16
package/app.js
CHANGED
|
@@ -210,18 +210,23 @@ function checkDepthAlarm(m) {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
* computeTrueWind: Calcola TWS, TWA e TWD strategico.
|
|
214
|
+
* Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
|
|
215
|
+
*/
|
|
216
216
|
function computeTrueWind() {
|
|
217
217
|
const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
|
|
218
|
-
const stw = store.raw["navigation.speedThroughWater"]
|
|
218
|
+
const stw = store.raw["navigation.speedThroughWater"], sog = store.raw["navigation.speedOverGround"] || 0;
|
|
219
219
|
const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
|
|
220
220
|
if (aws === undefined || awa === undefined) return;
|
|
221
221
|
|
|
222
222
|
// Usiamo il tempo esatto di arrivo del pacchetto del vento per la coerenza dei buffer
|
|
223
223
|
const now = store.timestamps["environment.wind.speedApparent"] || Date.now();
|
|
224
224
|
|
|
225
|
+
// Verifica se lo STW fisicamente attivo ha trasmesso dati negli ultimi 15 secondi
|
|
226
|
+
const hasStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
227
|
+
// Se lo STW non è disponibile, usa automaticamente la velocità del GPS (SOG) come riferimento
|
|
228
|
+
const speedRef = hasStw && stw !== undefined ? stw : sog;
|
|
229
|
+
|
|
225
230
|
// ==========================================================================
|
|
226
231
|
// 1. GESTIONE TWS (NATIVO vs FALLBACK)
|
|
227
232
|
// ==========================================================================
|
|
@@ -232,14 +237,14 @@ function computeTrueWind() {
|
|
|
232
237
|
// Se la barca invia il TWS nativo, usiamo direttamente quello
|
|
233
238
|
tws_water = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
234
239
|
} else {
|
|
235
|
-
// Altrimenti eseguiamo il calcolo vettoriale tattico
|
|
236
|
-
tws_water = Math.sqrt(aws * aws +
|
|
240
|
+
// Altrimenti eseguiamo il calcolo vettoriale tattico utilizzando la velocità di riferimento (STW o SOG)
|
|
241
|
+
tws_water = Math.sqrt(aws * aws + speedRef * speedRef - 2 * aws * speedRef * Math.cos(awa));
|
|
237
242
|
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
// BUG RISOLTO: Calcoliamo il TWA sempre, indipendentemente dal TWS nativo!
|
|
241
246
|
if (tws_water > 0.05) {
|
|
242
|
-
const twa = Math.atan2(aws * Math.sin(awa), aws * Math.cos(awa) -
|
|
247
|
+
const twa = Math.atan2(aws * Math.sin(awa), aws * Math.cos(awa) - speedRef);
|
|
243
248
|
store.raw["environment.wind.angleTrueWater"] = twa;
|
|
244
249
|
|
|
245
250
|
// Inserimento atomico e sincronizzato di TWA e AWA nei relativi buffer mobili
|
|
@@ -523,10 +528,10 @@ function updateWindTrend() {
|
|
|
523
528
|
}
|
|
524
529
|
if (deltaTac > 0) {
|
|
525
530
|
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
526
|
-
if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); }
|
|
531
|
+
if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); gaugeDots.ccw.setAttribute('fill', '#bbb'); } // Resetta l'attributo di riempimento del pallino inattivo
|
|
527
532
|
} else {
|
|
528
533
|
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
|
|
529
|
-
if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); }
|
|
534
|
+
if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); gaugeDots.cw.setAttribute('fill', '#bbb'); } // Resetta l'attributo di riempimento del pallino inattivo
|
|
530
535
|
}
|
|
531
536
|
} else {
|
|
532
537
|
[gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
@@ -1123,13 +1128,42 @@ function connect() {
|
|
|
1123
1128
|
if (simulationMode) return;
|
|
1124
1129
|
let addr = window.location.host || CONFIG.server.fallbackIp;
|
|
1125
1130
|
try {
|
|
1126
|
-
|
|
1131
|
+
// Connessione con subscribe=none per impedire a Signal K l'invio automatico di tutto lo stream di bordo
|
|
1132
|
+
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=none`);
|
|
1127
1133
|
|
|
1128
1134
|
socket.onopen = async () => {
|
|
1129
1135
|
ui.status.className = "online";
|
|
1130
1136
|
ui.status.innerText = "ONLINE";
|
|
1131
1137
|
reconnectDelay = 1000;
|
|
1132
1138
|
|
|
1139
|
+
// Sottoscrizione selettiva bilanciata: applichiamo un filtro a 3 Hz (333ms) per i dati rapidi,
|
|
1140
|
+
// garantendo fluidità matematica senza ritardi e ottimizzando profondità (1s) e GPS (10s)
|
|
1141
|
+
const subscriptionPayload = {
|
|
1142
|
+
context: "vessels.self",
|
|
1143
|
+
subscribe: [
|
|
1144
|
+
{ path: "navigation.position", minPeriod: 60000 }, // Posizione GPS lenta (60s)
|
|
1145
|
+
{ path: "navigation.magneticVariation", minPeriod: 60000 }, // Declinazione lenta (60s)
|
|
1146
|
+
{ path: "environment.depth.belowTransducer", minPeriod: 1000 }, // Profondità di sicurezza (1s)
|
|
1147
|
+
{ path: "navigation.speedThroughWater", minPeriod: 333 }, // Velocità barca a 3 Hz (333ms)
|
|
1148
|
+
{ path: "navigation.speedOverGround", minPeriod: 333 }, // Velocità GPS a 3 Hz
|
|
1149
|
+
{ path: "navigation.courseOverGroundTrue", minPeriod: 333 }, // COG a 3 Hz
|
|
1150
|
+
{ path: "navigation.headingTrue", minPeriod: 333 }, // Heading True a 3 Hz
|
|
1151
|
+
{ path: "navigation.headingMagnetic", minPeriod: 333 }, // Heading Fallback a 3 Hz
|
|
1152
|
+
{ path: "environment.wind.speedApparent", minPeriod: 333 }, // AWS a 3 Hz
|
|
1153
|
+
{ path: "environment.wind.angleApparent", minPeriod: 333 }, // AWA a 3 Hz
|
|
1154
|
+
{ path: "environment.wind.speedTrue", minPeriod: 333 }, // TWS (Nativo) a 3 Hz
|
|
1155
|
+
{ path: "environment.wind.directionTrue", minPeriod: 333 } // TWD (Nativo) a 3 Hz
|
|
1156
|
+
]
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// Invio del payload per configurare lo streaming selettivo a 3 Hz
|
|
1160
|
+
try {
|
|
1161
|
+
socket.send(JSON.stringify(subscriptionPayload));
|
|
1162
|
+
console.log("🔌 [WebSocket] Sottoscrizione selettiva a 3 Hz inviata con successo (Latenza azzerata).");
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
console.error("❌ [WebSocket] Impossibile inviare il payload di sottoscrizione:", err);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1133
1167
|
// SINCRONIZZAZIONE AUTOMATICA: Ogni volta che la connessione si apre o si riapre,
|
|
1134
1168
|
// scarichiamo lo storico fresco dal server e ridisegnamo i grafici
|
|
1135
1169
|
try {
|
package/index.js
CHANGED
|
@@ -83,7 +83,7 @@ module.exports = function (app) {
|
|
|
83
83
|
const localSubscription = {
|
|
84
84
|
context: 'vessels.self',
|
|
85
85
|
subscribe: [
|
|
86
|
-
{ path: 'navigation.position', minPeriod:
|
|
86
|
+
{ path: 'navigation.position', minPeriod: 60000 },
|
|
87
87
|
{ path: 'navigation.speedThroughWater', minPeriod: 1000 },
|
|
88
88
|
{ path: 'navigation.speedOverGround', minPeriod: 1000 },
|
|
89
89
|
{ path: 'environment.depth.belowTransducer', minPeriod: 1000 },
|
|
@@ -93,7 +93,7 @@ module.exports = function (app) {
|
|
|
93
93
|
{ path: 'environment.wind.directionTrue', minPeriod: 1000 },
|
|
94
94
|
{ path: 'navigation.headingTrue', minPeriod: 1000 },
|
|
95
95
|
{ path: 'navigation.headingMagnetic', minPeriod: 1000 },
|
|
96
|
-
{ path: 'navigation.magneticVariation', minPeriod:
|
|
96
|
+
{ path: 'navigation.magneticVariation', minPeriod: 60000 },
|
|
97
97
|
{ path: 'navigation.courseOverGroundTrue', minPeriod: 1000 }
|
|
98
98
|
]
|
|
99
99
|
};
|
|
@@ -141,12 +141,33 @@ module.exports = function (app) {
|
|
|
141
141
|
app.debug(msg);
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
+
// Helper per verificare se due valori sono identici (supporta anche oggetti complessi come la posizione lat/lon)
|
|
145
|
+
function isValueEqual(a, b) {
|
|
146
|
+
if (a === b) return true;
|
|
147
|
+
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
148
|
+
return a.latitude === b.latitude && a.longitude === b.longitude;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
144
153
|
/**
|
|
145
154
|
* processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
|
|
146
155
|
* Applica un filtro passa-basso continuo a ogni pacchetto e storicizza a 1Hz con dati stabilizzati.
|
|
147
156
|
*/
|
|
148
157
|
function processIncomingDelta(path, val) {
|
|
149
158
|
if (val === null || val === undefined) return;
|
|
159
|
+
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
const lastVal = raw[path];
|
|
162
|
+
const lastTime = lastPathProcessTimes[path] || 0;
|
|
163
|
+
|
|
164
|
+
// FILTRO DEDUPLICAZIONE CON HEARTBEAT DI SICUREZZA A 10 SECONDI:
|
|
165
|
+
// Se il nuovo dato è identico al precedente e sono passati meno di 10 secondi dall'ultimo invio,
|
|
166
|
+
// scartiamo subito l'elaborazione risparmiando cicli di CPU sul server.
|
|
167
|
+
// La soglia dei 10s garantisce la compatibilità con il Gap Detection dei grafici client.
|
|
168
|
+
if (lastVal !== undefined && isValueEqual(val, lastVal) && (now - lastTime < 10000)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
150
171
|
|
|
151
172
|
// 1. Filtro anti-spike vento (> 100 nodi / 51.44 m/s)
|
|
152
173
|
if ((path === 'environment.wind.speedApparent' || path === 'environment.wind.speedTrue') && val > 51.44) {
|
|
@@ -252,15 +273,20 @@ module.exports = function (app) {
|
|
|
252
273
|
// 2. Calcolo combinato di FALLBACK (Si attiva solo se la centralina non invia TWS/TWD nativi)
|
|
253
274
|
const aws = raw["environment.wind.speedApparent"];
|
|
254
275
|
const awa = raw["environment.wind.angleApparent"];
|
|
255
|
-
const stw = raw["navigation.speedThroughWater"]
|
|
276
|
+
const stw = raw["navigation.speedThroughWater"];
|
|
256
277
|
const sog = raw["navigation.speedOverGround"] || 0;
|
|
257
278
|
const hdg = raw["navigation.headingTrue"];
|
|
258
279
|
const cog = raw["navigation.courseOverGroundTrue"] || 0;
|
|
259
280
|
|
|
260
281
|
if (aws !== undefined && awa !== undefined) {
|
|
261
282
|
const awsKts = aws * 1.94384;
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
|
|
284
|
+
// Verifica se lo STW fisicamente attivo ha trasmesso dati negli ultimi 15 secondi
|
|
285
|
+
const hasStw = lastPathProcessTimes["navigation.speedThroughWater"] && (now - lastPathProcessTimes["navigation.speedThroughWater"] < 15000);
|
|
286
|
+
// Se lo STW non è disponibile, usa automaticamente la velocità del GPS (SOG) come riferimento
|
|
287
|
+
const speedKtsRef = hasStw && stw !== undefined ? (stw * 1.94384) : (sog * 1.94384);
|
|
288
|
+
|
|
289
|
+
const tw_water_x = awsKts * Math.cos(awa) - speedKtsRef;
|
|
264
290
|
const tw_water_y = awsKts * Math.sin(awa);
|
|
265
291
|
|
|
266
292
|
// Calcoliamo il TWS di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
|
|
@@ -271,8 +297,8 @@ module.exports = function (app) {
|
|
|
271
297
|
|
|
272
298
|
const twa = Math.atan2(tw_water_y, tw_water_x);
|
|
273
299
|
|
|
274
|
-
// La VMG viene
|
|
275
|
-
const vmg = Math.abs(
|
|
300
|
+
// La VMG viene calcolata usando la velocità di riferimento (STW reale o SOG di fallback)
|
|
301
|
+
const vmg = Math.abs(speedKtsRef * Math.cos(twa));
|
|
276
302
|
manageHistory('vmg', vmg);
|
|
277
303
|
|
|
278
304
|
// Calcoliamo il TWD di fallback solo se non abbiamo visto dati nativi negli ultimi 5 secondi
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -385,7 +385,7 @@ body.night-mode .alarm-danger {
|
|
|
385
385
|
|
|
386
386
|
/* ==========================================================================
|
|
387
387
|
9. WIDGETS E OVERRIDE SVG (GIORNO)
|
|
388
|
-
|
|
388
|
+
========================================================================= */
|
|
389
389
|
#status {
|
|
390
390
|
position: absolute;
|
|
391
391
|
top: 15px; right: 15px;
|
|
@@ -467,27 +467,42 @@ body.night-mode {
|
|
|
467
467
|
fill: #000000 !important;
|
|
468
468
|
stroke: #220000 !important;
|
|
469
469
|
}
|
|
470
|
-
.night-mode #wind-gauge circle:nth-of-type(2), .night-mode #fullscreen-hotspot {
|
|
470
|
+
.night-mode #wind-gauge > circle:nth-of-type(2), .night-mode #fullscreen-hotspot {
|
|
471
471
|
fill: #050000 !important;
|
|
472
472
|
stroke: #330000 !important;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
/*
|
|
476
|
-
.night-mode #trend-gauge-cw,
|
|
477
|
-
.night-mode #trend-gauge-ccw
|
|
478
|
-
|
|
475
|
+
/* 1. STATO SPENTO/RIPOSO: Attivo solo se l'attributo fill originario è grigio (#bbb o #bbbbbb) */
|
|
476
|
+
.night-mode #trend-gauge-cw[fill="#bbb"],
|
|
477
|
+
.night-mode #trend-gauge-ccw[fill="#bbb"],
|
|
478
|
+
.night-mode #trend-gauge-cw[fill="#bbbbbb"],
|
|
479
|
+
.night-mode #trend-gauge-ccw[fill="#bbbbbb"] {
|
|
480
|
+
fill: #440000 !important; /* Rosso scurissimo soffuso */
|
|
479
481
|
stroke: none !important;
|
|
480
|
-
|
|
482
|
+
filter: none !important;
|
|
483
|
+
opacity: 0.3 !important;
|
|
481
484
|
}
|
|
482
485
|
|
|
483
|
-
/*
|
|
484
|
-
.night-mode #trend-gauge-cw
|
|
485
|
-
.night-mode #trend-gauge-ccw
|
|
486
|
+
/* 2. STATO ATTIVO VERDE (Buono/Sotto-vento): Attivo solo se JS imposta il colore di lift (#27ae60) */
|
|
487
|
+
.night-mode #trend-gauge-cw[fill="#27ae60"],
|
|
488
|
+
.night-mode #trend-gauge-ccw[fill="#27ae60"] {
|
|
489
|
+
fill: #00C851 !important; /* Verde smeraldo brillante */
|
|
490
|
+
stroke: none !important;
|
|
491
|
+
filter: drop-shadow(0 0 5px #00C851) !important;
|
|
492
|
+
opacity: 1 !important;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* 3. STATO ATTIVO ROSSO (Scarso/Sopra-vento o strambata): Attivo se JS imposta il colore di header (#c0392b) o se in allarme strambata */
|
|
496
|
+
.night-mode #trend-gauge-cw[fill="#c0392b"],
|
|
497
|
+
.night-mode #trend-gauge-ccw[fill="#c0392b"],
|
|
486
498
|
.night-mode #trend-gauge-cw.is-gybing,
|
|
487
499
|
.night-mode #trend-gauge-ccw.is-gybing {
|
|
488
|
-
fill: #
|
|
489
|
-
|
|
500
|
+
fill: #ff3b30 !important; /* Rosso brillante */
|
|
501
|
+
stroke: none !important;
|
|
502
|
+
filter: drop-shadow(0 0 5px #ff3b30) !important;
|
|
503
|
+
opacity: 1 !important;
|
|
490
504
|
}
|
|
505
|
+
|
|
491
506
|
.night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
|
|
492
507
|
.night-mode #boat-icon { fill: #220000 !important; stroke: #ff0000 !important; stroke-width: 0.8px !important; opacity: 1 !important; }
|
|
493
508
|
.night-mode #aws-display-group text { fill: #800000 !important; }
|
|
@@ -513,10 +528,16 @@ body.night-mode {
|
|
|
513
528
|
.night-mode #twd-boat-wrap path { fill: #ff3333 !important; opacity: 0.4 !important; }
|
|
514
529
|
.night-mode #twd-arrow #twd-wind-chevron { stroke: #ff3333 !important; }
|
|
515
530
|
|
|
516
|
-
/* --- 10.6 LOGICA RED-SHIFT TREND --- */
|
|
517
|
-
.night-mode circle[fill="#bbb"], .night-mode circle[fill="#bbbbbb"]
|
|
518
|
-
|
|
519
|
-
|
|
531
|
+
/* --- 10.6 LOGICA RED-SHIFT TREND (PULITA) --- */
|
|
532
|
+
.night-mode circle[fill="#bbb"]:not(#trend-gauge-cw):not(#trend-gauge-ccw), .night-mode circle[fill="#bbbbbb"]:not(#trend-gauge-cw):not(#trend-gauge-ccw) {
|
|
533
|
+
fill: #440000 !important;
|
|
534
|
+
}
|
|
535
|
+
.night-mode circle[fill="#27ae60"]:not(#trend-gauge-cw):not(#trend-gauge-ccw) {
|
|
536
|
+
fill: #ff0000 !important;
|
|
537
|
+
}
|
|
538
|
+
.night-mode circle[fill="#c0392b"]:not(#trend-gauge-cw):not(#trend-gauge-ccw) {
|
|
539
|
+
fill: #800000 !important;
|
|
540
|
+
}
|
|
520
541
|
|
|
521
542
|
/* ==========================================================================
|
|
522
543
|
11. TREND E ANIMAZIONI
|