@sailingrotevista/rotevista-dash 7.0.45 → 8.0.2
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.
Potentially problematic release.
This version of @sailingrotevista/rotevista-dash might be problematic. Click here for more details.
- package/README.md +28 -70
- package/index.js +14 -0
- package/package.json +26 -1
- package/public/assets/dash-DmyxO1aO.png +0 -0
- package/public/assets/index-BFSy7dOW.css +1 -0
- package/public/assets/index-BHhAwNqq.js +9 -0
- package/public/assets/manifest-Ea_0WKQ8.json +17 -0
- package/public/icons/dash.png +0 -0
- package/public/index.html +23 -0
- package/public/manifest.json +17 -0
- package/app.js +0 -1505
- package/charts.js +0 -305
- package/debug.html +0 -1037
- package/gauge.js +0 -90
- package/index.html +0 -264
- package/style.css +0 -640
- package/utils.js +0 -148
- package/weather-radar.js +0 -305
- package/weather_debug.html +0 -181
package/app.js
DELETED
|
@@ -1,1505 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ==========================================================================
|
|
3
|
-
* Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
|
|
4
|
-
* ==========================================================================
|
|
5
|
-
* Autore: Sailing Rotevista
|
|
6
|
-
* Motore di calcolo tattico per navigazione e crociera.
|
|
7
|
-
* Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
|
|
8
|
-
* Memoria UI persistente, Modalità Hercules, Focus Split Screen e
|
|
9
|
-
* Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
|
|
10
|
-
* file app.js
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// ==========================================================================
|
|
14
|
-
// 1. CONFIGURAZIONE E DEFAULT
|
|
15
|
-
// ==========================================================================
|
|
16
|
-
let CONFIG = {
|
|
17
|
-
alarms: { depthDanger: 2.5, depthWarning: 3.5 },
|
|
18
|
-
averaging: {
|
|
19
|
-
smoothWindow: 2000,
|
|
20
|
-
longWindow: 30000,
|
|
21
|
-
stabilityTolerance: 2000,
|
|
22
|
-
stabilityThreshold: 0.99,
|
|
23
|
-
minSpeed: 0.5,
|
|
24
|
-
stabilityBreakout: 15
|
|
25
|
-
},
|
|
26
|
-
graphs: { reef1: 10, reef2: 15, historyMinutes: 5, samples: 60 },
|
|
27
|
-
scales: {
|
|
28
|
-
stw: { stdMax: 4, hercSpan: 2, step: 1 },
|
|
29
|
-
sog: { stdMax: 4, hercSpan: 2, step: 1 },
|
|
30
|
-
tws: { stdMax: 15, hercSpan: 2, step: 1 },
|
|
31
|
-
depth: { stdMax: 5, hercSpan: 2, step: 1 }
|
|
32
|
-
},
|
|
33
|
-
server: { fallbackIp: "venus.local:3000" }
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const RENDER_INTERVAL_MS = 1000;
|
|
37
|
-
const TIMEOUT_MS = 15000;
|
|
38
|
-
const SIM_SAMPLE_INTERVAL = 1000;
|
|
39
|
-
const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pro v6.0)
|
|
40
|
-
|
|
41
|
-
// ==========================================================================
|
|
42
|
-
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
43
|
-
// ==========================================================================
|
|
44
|
-
let simulationMode = false;
|
|
45
|
-
let displayModeSog = 'SOG';
|
|
46
|
-
let displayModeTws = 'TWS';
|
|
47
|
-
let activeInstrument = 'gauge'; // Modalità di default all'avvio: 'gauge' (analogico) o 'radar' (storico)
|
|
48
|
-
let socket, renderInterval, simInterval;
|
|
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
|
|
51
|
-
|
|
52
|
-
let rotationTrend = 0, meteoTrend = 0;
|
|
53
|
-
let lastShortAvgVal = null, lastInstantTwa = null;
|
|
54
|
-
let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
|
|
55
|
-
let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
56
|
-
let lastMessageReceivedTime = Date.now(); // Watchdog anti-zombie per monitorare il flusso dati reale
|
|
57
|
-
|
|
58
|
-
let pressTimer, isFocusActive = false;
|
|
59
|
-
|
|
60
|
-
let emaTwaSin = 0;
|
|
61
|
-
let emaTwaCos = 0;
|
|
62
|
-
let firstEmaRun = true;
|
|
63
|
-
// MACCHINA A STATI ALLARME STRAMBATA (Filtro Isteresi Anti-Brandeggio)
|
|
64
|
-
let lastGybeSide = null;
|
|
65
|
-
|
|
66
|
-
const graphModes = {
|
|
67
|
-
stw: 'standard',
|
|
68
|
-
sog: 'standard',
|
|
69
|
-
tws: 'standard',
|
|
70
|
-
depth: 'standard'
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
|
|
74
|
-
const sourceLocks = {};
|
|
75
|
-
|
|
76
|
-
// Database centrale dello store dati
|
|
77
|
-
const store = {
|
|
78
|
-
raw: {},
|
|
79
|
-
timestamps: {},
|
|
80
|
-
isNavigating: false, // Stato unificato di navigazione barca in movimento
|
|
81
|
-
depthProtectedActive: false, // Memoria per lo stato della protezione profondità
|
|
82
|
-
herculesScales: {}, // Memoria per i limiti attivi della modalità Hercules
|
|
83
|
-
smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
84
|
-
longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
|
|
85
|
-
histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [], twd: [] },
|
|
86
|
-
graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
|
|
87
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Riferimenti agli elementi DOM mappati all'avvio
|
|
91
|
-
const ui = {
|
|
92
|
-
stw: document.getElementById('stw'), sog: document.getElementById('sog'),
|
|
93
|
-
hdg: document.getElementById('hdg'), cog: document.getElementById('cog'),
|
|
94
|
-
awsSvg: document.getElementById('aws-val-svg'), awa: document.getElementById('awa-pointer'),
|
|
95
|
-
twa: document.getElementById('twa-pointer'), track: document.getElementById('track-pointer'),
|
|
96
|
-
tws: document.getElementById('tws'), depth: document.getElementById('depth'),
|
|
97
|
-
twaAvg: document.getElementById('twa-avg'), awaAvg: document.getElementById('awa-avg'),
|
|
98
|
-
twdAvg: document.getElementById('twd-avg'), twdArrow: document.getElementById('twd-arrow'),
|
|
99
|
-
twdBoat: document.getElementById('twd-boat-wrap'),
|
|
100
|
-
twdChevron: document.getElementById('twd-wind-chevron'),
|
|
101
|
-
leewayMask: document.getElementById('leeway-mask-rect'), leewayVal: document.getElementById('leeway-val'),
|
|
102
|
-
tackHdg: document.getElementById('tack-hdg'), tackCog: document.getElementById('tack-cog'),
|
|
103
|
-
status: document.getElementById('status'), hotspot: document.getElementById('fullscreen-hotspot'),
|
|
104
|
-
tackLabel: document.getElementById('tack-label')
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
let currentTackLabelMode = 'TACK'; // Stato dell'etichetta tattica ('TACK' o 'GYBE') con isteresi
|
|
108
|
-
|
|
109
|
-
// ==========================================================================
|
|
110
|
-
// 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA) - [Esportate in utils.js]
|
|
111
|
-
// ==========================================================================
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
115
|
-
* Mantiene sempre almeno 60 minuti di memoria locale per il calcolo della bussola meteo.
|
|
116
|
-
*/
|
|
117
|
-
function safePush(buffer, val, time) {
|
|
118
|
-
if (val === null || val === undefined || isNaN(val)) return;
|
|
119
|
-
|
|
120
|
-
buffer.push({
|
|
121
|
-
val: val,
|
|
122
|
-
time: time,
|
|
123
|
-
sin: Math.sin(val),
|
|
124
|
-
cos: Math.cos(val)
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Se stiamo spingendo nel buffer TWD, conserviamo sempre almeno 60 minuti in memoria locale
|
|
128
|
-
const isTwdBuffer = (buffer === store.longBuf.twd);
|
|
129
|
-
const limitMinutes = isTwdBuffer ? 60 : CONFIG.graphs.historyMinutes;
|
|
130
|
-
const maxHistoryMs = (limitMinutes * 60000 * 2) + 60000;
|
|
131
|
-
|
|
132
|
-
while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
|
|
133
|
-
buffer.shift();
|
|
134
|
-
}
|
|
135
|
-
if (buffer.length > 36000) buffer.shift();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function saveDashboardState() {
|
|
139
|
-
try {
|
|
140
|
-
const focusedBox = document.querySelector('.data-box.is-focused');
|
|
141
|
-
let focusedType = null;
|
|
142
|
-
if (focusedBox) {
|
|
143
|
-
const match = focusedBox.className.match(/box-([a-z]+)/);
|
|
144
|
-
if (match) focusedType = match[1];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const state = {
|
|
148
|
-
version: DASH_VERSION,
|
|
149
|
-
histories: store.histories,
|
|
150
|
-
longBuf: store.longBuf,
|
|
151
|
-
displayModeSog: displayModeSog,
|
|
152
|
-
displayModeTws: displayModeTws,
|
|
153
|
-
graphModes: graphModes,
|
|
154
|
-
isNightMode: document.body.classList.contains('night-mode'),
|
|
155
|
-
isFocusActive: isFocusActive,
|
|
156
|
-
focusedBoxType: focusedType,
|
|
157
|
-
timestamp: Date.now()
|
|
158
|
-
};
|
|
159
|
-
localStorage.setItem('rotevista_dash_state', JSON.stringify(state));
|
|
160
|
-
} catch (e) { console.error("Save error:", e); }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function loadDashboardState() {
|
|
164
|
-
const saved = localStorage.getItem('rotevista_dash_state');
|
|
165
|
-
if (!saved) return;
|
|
166
|
-
try {
|
|
167
|
-
const state = JSON.parse(saved);
|
|
168
|
-
if (state.version !== DASH_VERSION) { localStorage.removeItem('rotevista_dash_state'); return; }
|
|
169
|
-
|
|
170
|
-
if ((Date.now() - state.timestamp) / 60000 < 20) {
|
|
171
|
-
if (state.histories) Object.assign(store.histories, state.histories);
|
|
172
|
-
if (state.longBuf) Object.assign(store.longBuf, state.longBuf);
|
|
173
|
-
if (state.graphModes) Object.assign(graphModes, state.graphModes);
|
|
174
|
-
|
|
175
|
-
if (state.displayModeSog) {
|
|
176
|
-
displayModeSog = state.displayModeSog;
|
|
177
|
-
const labelEl = document.getElementById('sog-vmg-label');
|
|
178
|
-
if (labelEl) labelEl.textContent = displayModeSog;
|
|
179
|
-
}
|
|
180
|
-
if (state.displayModeTws) {
|
|
181
|
-
displayModeTws = state.displayModeTws;
|
|
182
|
-
const labelEl = document.getElementById('tws-aws-label');
|
|
183
|
-
if (labelEl) labelEl.textContent = displayModeTws;
|
|
184
|
-
}
|
|
185
|
-
if (state.isNightMode) document.body.classList.add('night-mode');
|
|
186
|
-
|
|
187
|
-
if (state.isFocusActive && state.focusedBoxType) {
|
|
188
|
-
setTimeout(() => {
|
|
189
|
-
const el = document.querySelector(`.box-${state.focusedBoxType}`);
|
|
190
|
-
if (el) { isFocusActive = false; toggleFocusMode(state.focusedBoxType, el); }
|
|
191
|
-
}, 200);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} catch (e) { localStorage.removeItem('rotevista_dash_state'); }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ==========================================================================
|
|
198
|
-
// 4. AUDIO E ALLARMI - [Esportati in utils.js]
|
|
199
|
-
// ==========================================================================
|
|
200
|
-
|
|
201
|
-
function checkDepthAlarm(m) {
|
|
202
|
-
ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
|
|
203
|
-
if (m < CONFIG.alarms.depthDanger) {
|
|
204
|
-
ui.depth.classList.add('alarm-danger', 'blink-alarm');
|
|
205
|
-
// Suona solo se siamo attivamente in navigazione (Harbor Silence acustico quando fermi)
|
|
206
|
-
if (isNavigating) {
|
|
207
|
-
playBingBing();
|
|
208
|
-
}
|
|
209
|
-
} else if (m < CONFIG.alarms.depthWarning) {
|
|
210
|
-
ui.depth.classList.add('alarm-warning');
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* computeTrueWind: Calcola TWS, TWA e TWD strategico.
|
|
216
|
-
* Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
|
|
217
|
-
*/
|
|
218
|
-
function computeTrueWind() {
|
|
219
|
-
const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
|
|
220
|
-
const stw = store.raw["navigation.speedThroughWater"], sog = store.raw["navigation.speedOverGround"] || 0;
|
|
221
|
-
const hdg = store.raw["navigation.headingTrue"]; // Rimosso il default a 0 fittizio all'avvio
|
|
222
|
-
if (aws === undefined || awa === undefined) return;
|
|
223
|
-
|
|
224
|
-
const now = store.timestamps["environment.wind.speedApparent"] || Date.now();
|
|
225
|
-
|
|
226
|
-
// RILEVAMENTO "LOG BLOCCATO" (Fouled Paddlewheel)
|
|
227
|
-
const hasFreshStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
228
|
-
let speedRef = 0;
|
|
229
|
-
|
|
230
|
-
if (hasFreshStw && stw !== undefined) {
|
|
231
|
-
// Se la barca naviga a GPS (> 1.5 nodi) ma l'elichetta legge quasi zero (< 0.5 nodi), il log è sporco/bloccato.
|
|
232
|
-
if (sog > 0.77 && stw < 0.25) {
|
|
233
|
-
speedRef = sog; // Forza il backup su SOG ignorando lo "0" fittizio del log
|
|
234
|
-
} else {
|
|
235
|
-
speedRef = stw;
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
speedRef = sog;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ==========================================================================
|
|
242
|
-
// 1. GESTIONE TWS (NATIVO vs FALLBACK)
|
|
243
|
-
// ==========================================================================
|
|
244
|
-
const hasNativeTws = store.timestamps["environment.wind.speedTrue"] && (Date.now() - store.timestamps["environment.wind.speedTrue"] < 5000);
|
|
245
|
-
let tws_water = 0;
|
|
246
|
-
|
|
247
|
-
if (hasNativeTws) {
|
|
248
|
-
tws_water = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
249
|
-
} else {
|
|
250
|
-
tws_water = Math.sqrt(aws * aws + speedRef * speedRef - 2 * aws * speedRef * Math.cos(awa));
|
|
251
|
-
store.raw["environment.wind.speedTrue"] = tws_water;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
let twa = 0;
|
|
255
|
-
// Verifica se riceviamo un TWA nativo valido ed efficiente negli ultimi 5 secondi
|
|
256
|
-
const hasNativeTwa = store.timestamps["environment.wind.angleTrueWater"] && (Date.now() - store.timestamps["environment.wind.angleTrueWater"] < 5000);
|
|
257
|
-
|
|
258
|
-
if (hasNativeTwa && store.raw["environment.wind.angleTrueWater"] !== undefined) {
|
|
259
|
-
// "Native First": diamo la precedenza assoluta al valore calcolato dalla centralina
|
|
260
|
-
twa = store.raw["environment.wind.angleTrueWater"];
|
|
261
|
-
} else {
|
|
262
|
-
// "Fallback Second": se manca il dato nativo, eseguiamo il calcolo vettoriale a mano
|
|
263
|
-
if (tws_water > 0.05) {
|
|
264
|
-
twa = Math.atan2(aws * Math.sin(awa), aws * Math.cos(awa) - speedRef);
|
|
265
|
-
store.raw["environment.wind.angleTrueWater"] = twa;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Salviamo e allineiamo i buffer delle medie se abbiamo un angolo valido (nativo o calcolato)
|
|
270
|
-
if (tws_water > 0.05 || hasNativeTwa) {
|
|
271
|
-
safePush(store.smoothBuf.twa, twa, now);
|
|
272
|
-
safePush(store.longBuf.twa, twa, now);
|
|
273
|
-
|
|
274
|
-
// Salviamo l'angolo apparente solo se il relativo sensore fisico è effettivamente attivo
|
|
275
|
-
if (awa !== undefined) {
|
|
276
|
-
safePush(store.smoothBuf.awa, awa, now);
|
|
277
|
-
safePush(store.longBuf.awa, awa, now);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ==========================================================================
|
|
282
|
-
// 2. GESTIONE TWD (NATIVO vs FALLBACK MATEMATICO STABILE)
|
|
283
|
-
// ==========================================================================
|
|
284
|
-
const hasNativeTwd = store.timestamps["environment.wind.directionTrue"] && (Date.now() - store.timestamps["environment.wind.directionTrue"] < 5000);
|
|
285
|
-
|
|
286
|
-
// Eseguiamo il calcolo del TWD di fallback solo se la Prua (hdg) è realmente disponibile in memoria,
|
|
287
|
-
// evitando di inquinare i buffer all'avvio con lo zero fittizio
|
|
288
|
-
if (!hasNativeTwd && tws_water > 0.05 && hdg !== undefined) {
|
|
289
|
-
// Calcolo TWD stabile e immune dal rollio: Prua + TWA
|
|
290
|
-
let twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
291
|
-
store.raw["environment.wind.directionTrue"] = twd;
|
|
292
|
-
safePush(store.smoothBuf.twd, twd, now);
|
|
293
|
-
safePush(store.longBuf.twd, twd, now);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// ==========================================================================
|
|
298
|
-
// GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
|
|
299
|
-
// ==========================================================================
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
|
|
303
|
-
* Più alto è il punteggio, maggiore è la priorità del sensore.
|
|
304
|
-
*/
|
|
305
|
-
function getSourcePriorityScore(sourceName) {
|
|
306
|
-
if (!sourceName) return 0;
|
|
307
|
-
const name = sourceName.toLowerCase();
|
|
308
|
-
|
|
309
|
-
// TIER 3: AIS / Trasmissioni lente (es. yacht_device.AI, VDO)
|
|
310
|
-
if (name.includes('.ai') || name.includes('ais') || name.includes('vdo')) {
|
|
311
|
-
return 10;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// TIER 2: GPS USB del Cerbo GX / Victron / Sistemi locali di servizio
|
|
315
|
-
if (name.includes('venus') || name.includes('victron') || name.includes('ttyacm') || name.includes('ttyusb') || name.includes('system')) {
|
|
316
|
-
return 50;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// TIER 1: Strumentazione ufficiale di navigazione (NMEA 2000, Yacht Devices GP/YD/AP, Gateway, ecc.)
|
|
320
|
-
if (name.includes('yacht_device') || name.includes('n2k') || name.includes('actisense') || name.includes('can') || name.includes('nmea') || name.includes('raymarine') || name.includes('garmin') || name.includes('simrad') || name.includes('b&g')) {
|
|
321
|
-
return 100;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return 30; // Punteggio standard per sorgenti sconosciute
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function processIncomingData(path, val, source, timeMs) {
|
|
328
|
-
// 0. Filtro di validità assoluta: ignora all'istante pacchetti vuoti, null o non numerici
|
|
329
|
-
if (val === null || val === undefined || (typeof val === 'number' && isNaN(val))) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// 1. Filtro anti-spike vento (> 100 nodi / 51.44 m/s)
|
|
334
|
-
if ((path === "environment.wind.speedApparent" || path === "environment.wind.speedTrue") && val > 51.44) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// 2. Filtro anti-spike velocità barca STW/SOG (> 50 nodi / 25.72 m/s)
|
|
339
|
-
if ((path === "navigation.speedThroughWater" || path === "navigation.speedOverGround") && val > 25.72) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// 3. Filtro validità profondità (ignora errori negativi e letture > 500m per lost-echo)
|
|
344
|
-
if (path === "environment.depth.belowTransducer" && (val < -2.0 || val > 500)) {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const now = timeMs || Date.now();
|
|
349
|
-
const score = getSourcePriorityScore(source);
|
|
350
|
-
|
|
351
|
-
// Gestione dello Smart Lock
|
|
352
|
-
if (!sourceLocks[path]) {
|
|
353
|
-
// Nessun blocco attivo: aggancia la sorgente corrente
|
|
354
|
-
sourceLocks[path] = { label: source, score: score, lastSeen: now };
|
|
355
|
-
} else {
|
|
356
|
-
const currentLock = sourceLocks[path];
|
|
357
|
-
const isSameSource = (currentLock.label === source);
|
|
358
|
-
// Tolleranza 5s: ignora il GPS Victron durante le micro-pause di Yacht Device per evitare imprecisioni
|
|
359
|
-
const isLockExpired = (now - currentLock.lastSeen > 5000);
|
|
360
|
-
const hasHigherPriority = (score > currentLock.score);
|
|
361
|
-
// Switch immediato tra apparati principali di pari priorità (es. YD e AP) se inattivi da > 1.5s
|
|
362
|
-
const isSamePriorityStale = (score === currentLock.score && (now - currentLock.lastSeen > 1500));
|
|
363
|
-
|
|
364
|
-
if (isSameSource) {
|
|
365
|
-
// Stessa sorgente: aggiorna il timestamp e mantiene il blocco
|
|
366
|
-
currentLock.lastSeen = now;
|
|
367
|
-
currentLock.score = score;
|
|
368
|
-
} else if (hasHigherPriority || isLockExpired || isSamePriorityStale) {
|
|
369
|
-
// Passa a sorgenti secondarie (Victron) solo se Yacht Device è spento da > 5s
|
|
370
|
-
sourceLocks[path] = { label: source, score: score, lastSeen: now };
|
|
371
|
-
console.log(`🔌 [Smart Lock] Path "${path}" switched to: ${source} (Score: ${score})`);
|
|
372
|
-
} else {
|
|
373
|
-
// Scarta sorgenti imprecise di servizio mentre Yacht Device è la sorgente principale attiva
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Aggiorna lo stato raw e sincronizza il timestamp del watchdog sull'orologio locale (Date.now()) per evitare falsi timeout
|
|
379
|
-
const localNow = Date.now();
|
|
380
|
-
store.timestamps[path] = localNow;
|
|
381
|
-
store.raw[path] = val;
|
|
382
|
-
|
|
383
|
-
// Converte e aggiorna istantaneamente store.raw["navigation.headingTrue"] per la UI prima del rate limiter
|
|
384
|
-
if (path === "navigation.headingMagnetic") {
|
|
385
|
-
const hasNativeTrueHdg = store.timestamps["navigation.headingTrueNative"] && (localNow - store.timestamps["navigation.headingTrueNative"] < 5000);
|
|
386
|
-
if (!hasNativeTrueHdg) {
|
|
387
|
-
const variation = store.raw["navigation.magneticVariation"] || 0;
|
|
388
|
-
store.raw["navigation.headingTrue"] = (val + variation + 2 * Math.PI) % (2 * Math.PI);
|
|
389
|
-
store.timestamps["navigation.headingTrue"] = localNow;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Le coordinate GPS e la posizione non sono soggette alla limitazione a 1Hz
|
|
394
|
-
if (path === "navigation.position") {
|
|
395
|
-
return; // Esce subito
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// LIMITATORE DI FREQUENZA (RATE LIMITER) CLIENT-SIDE A 1HZ PER PERCORSO ATTIVO:
|
|
399
|
-
if (!lastPathProcessTimes[path]) lastPathProcessTimes[path] = 0;
|
|
400
|
-
if (now - lastPathProcessTimes[path] < 800) {
|
|
401
|
-
return; // Esce subito risparmiando cicli di calcolo
|
|
402
|
-
}
|
|
403
|
-
lastPathProcessTimes[path] = now;
|
|
404
|
-
|
|
405
|
-
// Da qui in poi, l'inserimento nei buffer fisici avviene rigorosamente a 1Hz:
|
|
406
|
-
|
|
407
|
-
// Intercetta il TWD nativo e lo spinge nei buffer della bussola radar
|
|
408
|
-
if (path === "environment.wind.directionTrue") {
|
|
409
|
-
let directionVal = (val && typeof val === 'object' && val.val !== undefined) ? val.val : val;
|
|
410
|
-
safePush(store.smoothBuf.twd, directionVal, now);
|
|
411
|
-
safePush(store.longBuf.twd, directionVal, now);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Intercetta il TWS nativo e lo memorizza in tempo reale
|
|
415
|
-
if (path === "environment.wind.speedTrue") {
|
|
416
|
-
let speedVal = (val && typeof val === 'object' && val.val !== undefined) ? val.val : val;
|
|
417
|
-
store.raw["environment.wind.speedTrue"] = speedVal;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Intercetta TWA nativo (Angolo Vento Reale)
|
|
421
|
-
if (path === "environment.wind.angleTrueWater") {
|
|
422
|
-
let angleVal = (val && typeof val === 'object' && val.val !== undefined) ? val.val : val;
|
|
423
|
-
store.raw["environment.wind.angleTrueWater"] = angleVal;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// --- GESTIONE PRUA VERA / MAGNETICA CON AUTODIVIAZIONE ---
|
|
427
|
-
if (path === "navigation.headingTrue") {
|
|
428
|
-
store.timestamps["navigation.headingTrueNative"] = now; // Marca la presenza di un sensore nativo di prua vera
|
|
429
|
-
store.timestamps["navigation.headingTrue"] = localNow;
|
|
430
|
-
safePush(store.smoothBuf.hdg, val, now);
|
|
431
|
-
safePush(store.longBuf.hdg, val, now);
|
|
432
|
-
}
|
|
433
|
-
else if (path === "navigation.headingMagnetic") {
|
|
434
|
-
// Se non c'è una prua vera NATIVA negli ultimi 5 secondi, converte e registra ad ogni secondo la prua magnetica
|
|
435
|
-
const hasNativeTrueHdg = store.timestamps["navigation.headingTrueNative"] && (now - store.timestamps["navigation.headingTrueNative"] < 5000);
|
|
436
|
-
if (!hasNativeTrueHdg) {
|
|
437
|
-
const variation = store.raw["navigation.magneticVariation"] || 0; // Legge la declinazione magnetica del GPS
|
|
438
|
-
const calculatedTrueHdg = (val + variation + 2 * Math.PI) % (2 * Math.PI);
|
|
439
|
-
|
|
440
|
-
// Registra il valore calcolato mantenendo attivo il watchdog senza bloccare i successivi pacchetti magnetici
|
|
441
|
-
store.raw["navigation.headingTrue"] = calculatedTrueHdg;
|
|
442
|
-
store.timestamps["navigation.headingTrue"] = localNow;
|
|
443
|
-
safePush(store.smoothBuf.hdg, calculatedTrueHdg, now);
|
|
444
|
-
safePush(store.longBuf.hdg, calculatedTrueHdg, now);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (path === "navigation.courseOverGroundTrue") {
|
|
449
|
-
safePush(store.smoothBuf.cog, val, now);
|
|
450
|
-
safePush(store.longBuf.cog, val, now);
|
|
451
|
-
|
|
452
|
-
// Se non è installata alcuna bussola fisica sulla rete e la barca è in movimento stabile (> 1.5 nodi),
|
|
453
|
-
// emuliamo la Prua usando il COG per attivare il TWD, il mini-compass e la bussola radar.
|
|
454
|
-
const hasCompass = store.raw["navigation.headingTrue"] !== undefined || store.raw["navigation.headingMagnetic"] !== undefined;
|
|
455
|
-
const sog = store.raw["navigation.speedOverGround"] || 0;
|
|
456
|
-
if (!hasCompass && sog > 0.77) { // 0.77 m/s = 1.5 nodi
|
|
457
|
-
store.raw["navigation.headingTrue"] = val;
|
|
458
|
-
store.timestamps["navigation.headingTrue"] = localNow;
|
|
459
|
-
safePush(store.smoothBuf.hdg, val, now);
|
|
460
|
-
safePush(store.longBuf.hdg, val, now);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const twPaths = [
|
|
465
|
-
"environment.wind.speedApparent", "environment.wind.angleApparent",
|
|
466
|
-
"navigation.speedThroughWater", "navigation.speedOverGround",
|
|
467
|
-
"navigation.headingTrue", "navigation.courseOverGroundTrue"
|
|
468
|
-
];
|
|
469
|
-
|
|
470
|
-
if (twPaths.includes(path)) {
|
|
471
|
-
twDirty = true;
|
|
472
|
-
if (twDirty && (now - lastTWCompute > 100)) {
|
|
473
|
-
computeTrueWind();
|
|
474
|
-
lastTWCompute = now;
|
|
475
|
-
twDirty = false;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ==========================================================================
|
|
481
|
-
// 6. TREND VENTO (Tattico 2s vs 10s | Strategico 1m vs 10m)
|
|
482
|
-
// Sincronizzato sul tempo reale dei sensori per eliminare i conflitti di orologio.
|
|
483
|
-
// ==========================================================================
|
|
484
|
-
function updateWindTrend() {
|
|
485
|
-
// --- RISOLUZIONE CLOCK DRIFT SUI PALLINI METEO ---
|
|
486
|
-
// Usiamo il tempo del sensore (l'ultimo dato del TWD in memoria) invece dell'orologio del tablet
|
|
487
|
-
const latestTwdPoint = store.longBuf.twd.length > 0 ? store.longBuf.twd[store.longBuf.twd.length - 1] : null;
|
|
488
|
-
const now = latestTwdPoint ? latestTwdPoint.time : Date.now();
|
|
489
|
-
|
|
490
|
-
// --- 6.1 TREND TATTICO (AWA/TWA Sintonizzato) ---
|
|
491
|
-
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true, now);
|
|
492
|
-
const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true, now);
|
|
493
|
-
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
494
|
-
|
|
495
|
-
// --- 6.2 ALLARME STRAMBATA CON ISTERESI (MACCHINA A STATI ANTI-BRANDEGGIO Sintonizzato) ---
|
|
496
|
-
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
497
|
-
let smoothedTwaDeg = null;
|
|
498
|
-
|
|
499
|
-
if (instTwaRad !== undefined) {
|
|
500
|
-
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
501
|
-
const currentSin = Math.sin(instTwaRad);
|
|
502
|
-
const currentCos = Math.cos(instTwaRad);
|
|
503
|
-
|
|
504
|
-
if (firstEmaRun) {
|
|
505
|
-
emaTwaSin = currentSin; emaTwaCos = currentCos; firstEmaRun = false;
|
|
506
|
-
} else {
|
|
507
|
-
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
508
|
-
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
512
|
-
const absTwaDeg = Math.abs(smoothedTwaDeg);
|
|
513
|
-
|
|
514
|
-
// Allarme VISIVO: Se siamo in zona di pericolo poppa profonda (> 155°), accendi entrambi i LED di rosso pulsante
|
|
515
|
-
if (absTwaDeg > 155) {
|
|
516
|
-
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-gybing'); gaugeDots.cw.setAttribute('fill', '#ff3b30'); }
|
|
517
|
-
if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-gybing'); }
|
|
518
|
-
|
|
519
|
-
// LOGICA MACCHINA A STATI CON ISTERESI:
|
|
520
|
-
// Rileviamo le mure solo se siamo fuori dalla zona cieca di poppa secca (> 170°).
|
|
521
|
-
// Se oscilliamo tra -175° e +178° (brandeggio), lastGybeSide NON cambia e l'allarme acustico tace.
|
|
522
|
-
let currentTack = null;
|
|
523
|
-
if (smoothedTwaDeg > 155 && smoothedTwaDeg < 170) {
|
|
524
|
-
currentTack = 'starboard';
|
|
525
|
-
} else if (smoothedTwaDeg < -155 && smoothedTwaDeg > -170) {
|
|
526
|
-
currentTack = 'port';
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (currentTack !== null) {
|
|
530
|
-
if (lastGybeSide === null) {
|
|
531
|
-
// Inizializzazione al primo ingresso nella zona di controllo
|
|
532
|
-
lastGybeSide = currentTack;
|
|
533
|
-
} else if (lastGybeSide !== currentTack) {
|
|
534
|
-
// Abbiamo eseguito una vera strambata stabile e siamo usciti dalla zona di poppa secca!
|
|
535
|
-
lastGybeSide = currentTack;
|
|
536
|
-
|
|
537
|
-
// Estraggo i dati correnti di vento reale (TWS), velocità (SOG) e l'angolo TWA smorzato
|
|
538
|
-
const twsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
539
|
-
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
540
|
-
const twaDeg = Math.abs(smoothedTwaDeg);
|
|
541
|
-
|
|
542
|
-
// RILEVAMENTO AERODINAMICO MOTORE IN POPPA:
|
|
543
|
-
// Se siamo di poppa (TWA > 135°) e la velocità della barca supera il 75% della velocità del vento reale,
|
|
544
|
-
// è fisicamente impossibile essere a vela pura. Siamo a motore o in motorsailing.
|
|
545
|
-
const isMotoringDownwind = (twaDeg > 135) && (sogKts > (twsKts * 0.75));
|
|
546
|
-
|
|
547
|
-
// L'allarme strambata acustico è reale e attivo solo se:
|
|
548
|
-
// 1. C'è vento significativo (> 7.0 nodi)
|
|
549
|
-
// 2. La barca è in movimento (> 1.0 nodi)
|
|
550
|
-
// 3. NON siamo a motore/motorsailing (isMotoringDownwind è falso)
|
|
551
|
-
const isGybeDangerous = twsKts > 7.0 && sogKts > 1.0 && !isMotoringDownwind;
|
|
552
|
-
|
|
553
|
-
// Attivazione allarme acustico con blocco temporale di sicurezza (60 secondi)
|
|
554
|
-
if (isGybeDangerous && (now - lastGybeAlarmTime > 60000)) {
|
|
555
|
-
lastGybeAlarmTime = now;
|
|
556
|
-
playGybeAlarm();
|
|
557
|
-
console.log(`⚠️ GYBE ALARM TRIGGERED: Tack switched to ${currentTack} (TWA: ${twaDeg.toFixed(1)}°, TWS: ${twsKts.toFixed(1)}kts, SOG: ${sogKts.toFixed(1)}kts)`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
} else {
|
|
562
|
-
// Se usciamo dalla poppa profonda (< 155°), disattiva l'allarme visivo e resetta lo stato delle mure
|
|
563
|
-
if (gaugeDots.cw) gaugeDots.cw.classList.remove('is-gybing');
|
|
564
|
-
if (gaugeDots.ccw) gaugeDots.ccw.classList.remove('is-gybing');
|
|
565
|
-
lastGybeSide = null; // Reset per la prossima poppa
|
|
566
|
-
}
|
|
567
|
-
lastInstantTwa = smoothedTwaDeg;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Gestione normale dei Trend Tattici (Lifts/Headers) se NON siamo in allarme strambata
|
|
571
|
-
if (twaNow && twaRef && (smoothedTwaDeg === null || Math.abs(smoothedTwaDeg) <= 155)) {
|
|
572
|
-
let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
573
|
-
const curTwaDeg = radToDeg(twaNow.val);
|
|
574
|
-
if (Math.abs(deltaTac) > 3.0) {
|
|
575
|
-
let absTwa = Math.abs(curTwaDeg);
|
|
576
|
-
let tacticColor;
|
|
577
|
-
if (absTwa > 75 && absTwa < 105) {
|
|
578
|
-
tacticColor = "#bbb";
|
|
579
|
-
} else {
|
|
580
|
-
let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
|
|
581
|
-
if (absTwa >= 90) isLift = !isLift;
|
|
582
|
-
tacticColor = isLift ? "#27ae60" : "#c0392b";
|
|
583
|
-
}
|
|
584
|
-
if (deltaTac > 0) {
|
|
585
|
-
if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
|
|
586
|
-
if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); gaugeDots.ccw.setAttribute('fill', '#bbb'); } // Resetta l'attributo di riempimento del pallino inattivo
|
|
587
|
-
} else {
|
|
588
|
-
if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
|
|
589
|
-
if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); gaugeDots.cw.setAttribute('fill', '#bbb'); } // Resetta l'attributo di riempimento del pallino inattivo
|
|
590
|
-
}
|
|
591
|
-
} else {
|
|
592
|
-
[gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// --- 6.3 TREND METEO STRATEGICO (TWD PARAMETRIZZATO E SINTONIZZATO) ---
|
|
597
|
-
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false, now); // 1 minuto fisso
|
|
598
|
-
|
|
599
|
-
// Parametrizzazione rigida nel codice: 15 minuti in navigazione, 60 minuti all'ancora
|
|
600
|
-
const fixedTwdMinutes = isNavigating ? 15 : 60;
|
|
601
|
-
const strategicWindowMs = fixedTwdMinutes * 60000;
|
|
602
|
-
|
|
603
|
-
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false, now);
|
|
604
|
-
|
|
605
|
-
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
606
|
-
|
|
607
|
-
if (twdNow && twdRef) {
|
|
608
|
-
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
609
|
-
if (Math.abs(deltaMeteo) > 6.0) {
|
|
610
|
-
const isSouth = store.raw["navigation.position"]?.latitude < 0;
|
|
611
|
-
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
|
|
612
|
-
if (deltaMeteo > 0) {
|
|
613
|
-
if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
|
|
614
|
-
if (compassDots.ccw) { compassDots.ccw.classList.remove('is-trending'); }
|
|
615
|
-
} else {
|
|
616
|
-
if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
|
|
617
|
-
if (compassDots.cw) { compassDots.cw.classList.remove('is-trending'); }
|
|
618
|
-
}
|
|
619
|
-
} else {
|
|
620
|
-
[compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// ==========================================================================
|
|
626
|
-
// 7. RENDERING ENGINE E AGGIORNAMENTO UI
|
|
627
|
-
// ==========================================================================
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* upUI: Aggiornamento valori digitali con tolleranza d'emergenza sul dato istantaneo
|
|
631
|
-
*/
|
|
632
|
-
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
633
|
-
// Se la media obj non è momentaneamente disponibile, usa il dato istantaneo raw come paracadute
|
|
634
|
-
const displayRad = (obj && obj.val !== null && !isNaN(obj.val)) ? obj.val : instantRaw;
|
|
635
|
-
|
|
636
|
-
if (displayRad === undefined || displayRad === null || isNaN(displayRad)) {
|
|
637
|
-
el.innerHTML = "---°";
|
|
638
|
-
el.classList.remove('unstable-data');
|
|
639
|
-
} else {
|
|
640
|
-
let valDeg = Math.round(radToDeg(displayRad));
|
|
641
|
-
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
642
|
-
let dev = (obj && obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.8em; opacity: 0.4; margin-left: 6px;">±${obj.dev}</span>` : "";
|
|
643
|
-
el.innerHTML = mainVal + dev;
|
|
644
|
-
|
|
645
|
-
let diff = (obj && instantRaw !== undefined) ? Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180) : 0;
|
|
646
|
-
if (isNavigating && (obj && (!obj.stable || obj.dev > CONFIG.averaging.stabilityBreakout || diff > CONFIG.averaging.stabilityBreakout))) {
|
|
647
|
-
el.classList.add('unstable-data');
|
|
648
|
-
} else {
|
|
649
|
-
el.classList.remove('unstable-data');
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Loop principale di aggiornamento interfaccia (1Hz)
|
|
656
|
-
*/
|
|
657
|
-
function startDisplayLoop() {
|
|
658
|
-
renderInterval = setInterval(() => {
|
|
659
|
-
const now = Date.now();
|
|
660
|
-
const isNight = document.body.classList.contains('night-mode');
|
|
661
|
-
|
|
662
|
-
const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
|
|
663
|
-
const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
|
|
664
|
-
|
|
665
|
-
isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
|
|
666
|
-
store.isNavigating = isNavigating; // Sincronizzazione stato nello store condiviso
|
|
667
|
-
|
|
668
|
-
// --- CALCOLO TREND VENTO & ALLARME STRAMBATA ---
|
|
669
|
-
updateWindTrend();
|
|
670
|
-
|
|
671
|
-
// --- AGGIORNAMENTO STATUS CON WATCHDOG ANTI-ZOMBIE ---
|
|
672
|
-
const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
|
|
673
|
-
const isDataFlowing = (now - lastMessageReceivedTime) < 6000; // Flusso dati attivo negli ultimi 6s
|
|
674
|
-
|
|
675
|
-
// Se il socket risulta aperto ma non riceve pacchetti da > 6s (connessione zombie congelata da Android)
|
|
676
|
-
if (isSocketOpen && !isDataFlowing && !document.hidden && !simulationMode) {
|
|
677
|
-
console.warn("🔌 [Watchdog] Rilevato socket zombie (0 byte ricevuti da 6s). Forzo riconnessione...");
|
|
678
|
-
connect();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (isSocketOpen && isDataFlowing) {
|
|
682
|
-
ui.status.className = "online"; // Colore Verde
|
|
683
|
-
const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
684
|
-
const requiredMs = viewportMinutes * 60000;
|
|
685
|
-
const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
|
|
686
|
-
|
|
687
|
-
if (oldestStw) {
|
|
688
|
-
const availableMs = now - oldestStw.time;
|
|
689
|
-
if (availableMs >= requiredMs) {
|
|
690
|
-
ui.status.innerText = `ONLINE ${viewportMinutes}min`;
|
|
691
|
-
} else {
|
|
692
|
-
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
693
|
-
ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
|
|
694
|
-
}
|
|
695
|
-
} else {
|
|
696
|
-
ui.status.innerText = `ONLINE`;
|
|
697
|
-
}
|
|
698
|
-
} else {
|
|
699
|
-
ui.status.className = "offline"; // Colore Rosso
|
|
700
|
-
ui.status.innerText = isSocketOpen ? "STALL (RECONNECTING...)" : "OFFLINE";
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// --- WATCHDOG: CONTROLLO TIMEOUT ---
|
|
704
|
-
const watch = {
|
|
705
|
-
"navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
|
|
706
|
-
"navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
|
|
707
|
-
"environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
|
|
708
|
-
"environment.wind.speedTrue": ui.tws,
|
|
709
|
-
"environment.wind.angleApparent": ui.awaAvg,
|
|
710
|
-
"environment.wind.angleTrueWater": ui.twaAvg,
|
|
711
|
-
"environment.wind.directionTrue": ui.twdAvg
|
|
712
|
-
};
|
|
713
|
-
for (let p in watch) {
|
|
714
|
-
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
715
|
-
if (watch[p]) safeSetText(watch[p], "---");
|
|
716
|
-
delete store.raw[p];
|
|
717
|
-
|
|
718
|
-
// Forza lo scorrimento dei dati fuori dallo schermo per i sensori in timeout
|
|
719
|
-
if (p === "navigation.speedThroughWater") {
|
|
720
|
-
refreshGraph('stw');
|
|
721
|
-
} else if (p === "navigation.speedOverGround") {
|
|
722
|
-
refreshGraph('sog');
|
|
723
|
-
} else if (p === "environment.depth.belowTransducer") {
|
|
724
|
-
refreshGraph('depth');
|
|
725
|
-
} else if (p === "environment.wind.speedApparent" || p === "environment.wind.speedTrue") {
|
|
726
|
-
refreshGraph('tws');
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
732
|
-
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
733
|
-
safeSetText(ui.stw, stwKts.toFixed(1));
|
|
734
|
-
ui.stw.style.color = ""; // Neutro
|
|
735
|
-
manageHistory('stw', stwKts);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// --- LOGICA SOG / VMG ---
|
|
739
|
-
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
740
|
-
// RILEVAMENTO "LOG BLOCCATO" PER IL CALCOLO VMG
|
|
741
|
-
const hasFreshStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
742
|
-
let speedRefKts = sogKts; // Fallback predefinito su GPS
|
|
743
|
-
|
|
744
|
-
if (hasFreshStw) {
|
|
745
|
-
// Se viaggiamo a più di 1.5 nodi (GPS) ma il log segna meno di 0.5 nodi, usa il SOG come backup
|
|
746
|
-
speedRefKts = (sogKts > 1.5 && stwKts < 0.5) ? sogKts : stwKts;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
const vmgVal = Math.abs(speedRefKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
750
|
-
manageHistory('vmg', vmgVal);
|
|
751
|
-
manageHistory('sog', sogKts);
|
|
752
|
-
|
|
753
|
-
const labelSogVmg = document.getElementById('sog-vmg-label');
|
|
754
|
-
if (displayModeSog === 'VMG') {
|
|
755
|
-
safeSetText(ui.sog, vmgVal.toFixed(1));
|
|
756
|
-
ui.sog.style.setProperty('color', '#00b8d4', 'important'); // Cyan
|
|
757
|
-
if (labelSogVmg) labelSogVmg.textContent = 'VMG';
|
|
758
|
-
} else {
|
|
759
|
-
safeSetText(ui.sog, sogKts.toFixed(1));
|
|
760
|
-
if (labelSogVmg) labelSogVmg.textContent = 'SOG';
|
|
761
|
-
|
|
762
|
-
if (isNavigating) {
|
|
763
|
-
const hasFreshStw = store.timestamps["navigation.speedThroughWater"] && (now - store.timestamps["navigation.speedThroughWater"] < 15000);
|
|
764
|
-
// Rileviamo se il log è plausibilmente sporco o bloccato (SOG in movimento, STW a zero)
|
|
765
|
-
const isFouledLog = (sogKts > 1.5 && stwKts < 0.5);
|
|
766
|
-
|
|
767
|
-
// Se non abbiamo un log affidabile, è impossibile calcolare la corrente. Evitiamo falsi colori tattici.
|
|
768
|
-
if (!hasFreshStw || isFouledLog) {
|
|
769
|
-
ui.sog.style.removeProperty('color');
|
|
770
|
-
} else {
|
|
771
|
-
const lastSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1].val : sogKts;
|
|
772
|
-
const lastStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1].val : stwKts;
|
|
773
|
-
const drift = lastSog - lastStw;
|
|
774
|
-
|
|
775
|
-
if (drift < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Contro
|
|
776
|
-
else if (drift > 0.3) ui.sog.style.setProperty('color', '#00C851', 'important'); // Favore
|
|
777
|
-
else ui.sog.style.setProperty('color', '#ffbb33', 'important'); // Neutro SOG
|
|
778
|
-
}
|
|
779
|
-
} else {
|
|
780
|
-
ui.sog.style.removeProperty('color');
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
786
|
-
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
787
|
-
safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
|
|
788
|
-
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
789
|
-
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
|
|
793
|
-
|
|
794
|
-
// Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
|
|
795
|
-
const rawTws = store.raw["environment.wind.speedTrue"];
|
|
796
|
-
const rawAws = store.raw["environment.wind.speedApparent"];
|
|
797
|
-
|
|
798
|
-
const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
|
|
799
|
-
const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
|
|
800
|
-
|
|
801
|
-
if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
|
|
802
|
-
if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
|
|
803
|
-
|
|
804
|
-
// Disegno testo casella destra (TWS o AWS)
|
|
805
|
-
if (rawTws !== undefined || rawAws !== undefined) {
|
|
806
|
-
const labelWind = document.getElementById('tws-aws-label');
|
|
807
|
-
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
808
|
-
|
|
809
|
-
safeSetText(ui.tws, currentWind.toFixed(1));
|
|
810
|
-
if (labelWind) labelWind.textContent = displayModeTws;
|
|
811
|
-
|
|
812
|
-
if (currentWind >= CONFIG.graphs.reef2) {
|
|
813
|
-
ui.tws.style.setProperty('color', '#ff3b30', 'important');
|
|
814
|
-
} else if (currentWind >= CONFIG.graphs.reef1) {
|
|
815
|
-
ui.tws.style.setProperty('color', '#ff9800', 'important');
|
|
816
|
-
} else {
|
|
817
|
-
if (displayModeTws === 'AWS') {
|
|
818
|
-
ui.tws.style.setProperty('color', '#5c6bc0', 'important');
|
|
819
|
-
} else {
|
|
820
|
-
const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
|
|
821
|
-
ui.tws.style.setProperty('color', navyNight, 'important');
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
|
|
827
|
-
if (activeInstrument === 'gauge') {
|
|
828
|
-
updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
|
|
832
|
-
if (lastAvgUIUpdate++ % 10 === 0) {
|
|
833
|
-
saveDashboardState();
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// AGGIORNAMENTO FLUIDO MEDIE UI (Esecuzione a 1Hz costante ad ogni secondo)
|
|
837
|
-
const baseThreshold = CONFIG.averaging.stabilityThreshold || 0.95;
|
|
838
|
-
const breakout = CONFIG.averaging.stabilityBreakout || 15;
|
|
839
|
-
|
|
840
|
-
// CALCOLO DIFFERENZIALE: Tolleranza del vento proporzionale alla sensibilità di base
|
|
841
|
-
const windThreshold = Math.max(0.60, baseThreshold - 0.13);
|
|
842
|
-
|
|
843
|
-
let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false, now, baseThreshold, breakout);
|
|
844
|
-
let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false, now, baseThreshold, breakout);
|
|
845
|
-
|
|
846
|
-
// Applichiamo la soglia differenziale ottimizzata per i tre dati legati al vento
|
|
847
|
-
let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true, now, windThreshold, breakout);
|
|
848
|
-
let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true, now, windThreshold, breakout);
|
|
849
|
-
let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false, now, windThreshold, breakout);
|
|
850
|
-
|
|
851
|
-
// --- GESTIONE DINAMICA ETICHETTA TACK/GYBE CON ISTERESI DI 10 GRADI ---
|
|
852
|
-
if (ui.tackLabel) {
|
|
853
|
-
const absTwa = Math.abs(radToDeg(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
854
|
-
if (currentTackLabelMode === 'TACK' && absTwa > 95) {
|
|
855
|
-
currentTackLabelMode = 'GYBE';
|
|
856
|
-
} else if (currentTackLabelMode === 'GYBE' && absTwa < 85) {
|
|
857
|
-
currentTackLabelMode = 'TACK';
|
|
858
|
-
}
|
|
859
|
-
safeSetText(ui.tackLabel, currentTackLabelMode);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
|
|
863
|
-
upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
|
|
864
|
-
upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
|
|
865
|
-
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
866
|
-
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
867
|
-
|
|
868
|
-
if (hObj && twdObj) {
|
|
869
|
-
const reflectAngle = (targetRad, axisRad) => {
|
|
870
|
-
const dS = Math.sin(axisRad - targetRad);
|
|
871
|
-
const dC = Math.cos(axisRad - targetRad);
|
|
872
|
-
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
873
|
-
};
|
|
874
|
-
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
875
|
-
if (!isNavigating) ui.tackHdg.innerHTML = "---°";
|
|
876
|
-
else if (unstableH) { ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data'); }
|
|
877
|
-
else {
|
|
878
|
-
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
879
|
-
ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
880
|
-
ui.tackHdg.classList.remove('unstable-data');
|
|
881
|
-
}
|
|
882
|
-
if (cObj) {
|
|
883
|
-
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
884
|
-
if (!isNavigating) ui.tackCog.innerHTML = "---°";
|
|
885
|
-
else if (unstableC) { ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data'); }
|
|
886
|
-
else {
|
|
887
|
-
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
888
|
-
ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
889
|
-
ui.tackCog.classList.remove('unstable-data');
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}, RENDER_INTERVAL_MS);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// ==========================================================================
|
|
897
|
-
// 8. CONFIGURAZIONE, AGGIORNAMENTO LIVE E GRAFICI UTILS
|
|
898
|
-
// ==========================================================================
|
|
899
|
-
|
|
900
|
-
let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
|
|
904
|
-
*/
|
|
905
|
-
function getApiUrl(path) {
|
|
906
|
-
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
907
|
-
return `http://${CONFIG.server.fallbackIp}${path}`;
|
|
908
|
-
}
|
|
909
|
-
return path;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
|
|
914
|
-
*/
|
|
915
|
-
function applyConfigData(data) {
|
|
916
|
-
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
917
|
-
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
918
|
-
Object.assign(CONFIG.averaging, data.averaging || {});
|
|
919
|
-
|
|
920
|
-
// Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85).
|
|
921
|
-
// Aggiornato a 0.95 per ottimizzazione filtri.
|
|
922
|
-
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
923
|
-
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (data.scales) {
|
|
927
|
-
for (let key in data.scales) {
|
|
928
|
-
if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Recupera la configurazione iniziale al caricamento della pagina
|
|
935
|
-
*/
|
|
936
|
-
async function fetchServerConfig() {
|
|
937
|
-
try {
|
|
938
|
-
const response = await fetch(getApiUrl('/rotevista-config'));
|
|
939
|
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
940
|
-
const data = await response.json();
|
|
941
|
-
|
|
942
|
-
// Salviamo l'impronta digitale della configurazione per i confronti futuri
|
|
943
|
-
currentConfigString = JSON.stringify(data);
|
|
944
|
-
|
|
945
|
-
applyConfigData(data);
|
|
946
|
-
console.log("✅ Configurazione iniziale applicata con successo.");
|
|
947
|
-
} catch (err) {
|
|
948
|
-
console.warn("⚠️ Impossibile raggiungere le configurazioni dal server. Utilizzo default.");
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
* Watchdog: Controlla in background se le impostazioni sul server sono cambiate
|
|
954
|
-
*/
|
|
955
|
-
async function watchConfigChanges() {
|
|
956
|
-
try {
|
|
957
|
-
const response = await fetch(getApiUrl('/rotevista-config'));
|
|
958
|
-
if (!response.ok) return;
|
|
959
|
-
const data = await response.json();
|
|
960
|
-
|
|
961
|
-
const newConfigString = JSON.stringify(data);
|
|
962
|
-
|
|
963
|
-
// Se l'impronta digitale è diversa, l'utente ha salvato nuovi settings!
|
|
964
|
-
if (newConfigString !== currentConfigString) {
|
|
965
|
-
console.log("🔄 Rilevato cambio impostazioni su Signal K! Aggiornamento in tempo reale...");
|
|
966
|
-
|
|
967
|
-
// Applichiamo i nuovi settings al volo
|
|
968
|
-
applyConfigData(data);
|
|
969
|
-
|
|
970
|
-
// Aggiorniamo l'impronta
|
|
971
|
-
currentConfigString = newConfigString;
|
|
972
|
-
|
|
973
|
-
// FEEDBACK VISIVO: Avvisiamo l'utente dell'aggiornamento
|
|
974
|
-
ui.status.innerText = "CONFIG UPDATED!";
|
|
975
|
-
ui.status.style.color = "#00C851"; // Verde Neon
|
|
976
|
-
setTimeout(() => { ui.status.style.color = ""; }, 4000);
|
|
977
|
-
}
|
|
978
|
-
} catch (err) {
|
|
979
|
-
// Ignoriamo gli errori di rete nel watchdog per non intasare la console in caso di disconnessione
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
|
|
985
|
-
*/
|
|
986
|
-
async function fetchServerHistory() {
|
|
987
|
-
try {
|
|
988
|
-
const response = await fetch(getApiUrl('/rotevista-history'));
|
|
989
|
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
990
|
-
const data = await response.json();
|
|
991
|
-
|
|
992
|
-
if (data && typeof data === 'object') {
|
|
993
|
-
// 1. RILEVAMENTO DEL DELTA DI COERENZA TEMPORALE
|
|
994
|
-
// Troviamo il timestamp più recente in assoluto presente nei dati storici del server
|
|
995
|
-
let maxServerTime = 0;
|
|
996
|
-
for (let key in data) {
|
|
997
|
-
if (store.histories[key] !== undefined && Array.isArray(data[key]) && data[key].length > 0) {
|
|
998
|
-
const lastPoint = data[key][data[key].length - 1];
|
|
999
|
-
if (lastPoint && lastPoint.time > maxServerTime) {
|
|
1000
|
-
maxServerTime = lastPoint.time;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// Calcoliamo lo sfasamento in millisecondi rispetto all'orologio del client (MacBook/Tablet)
|
|
1006
|
-
const nowMac = Date.now();
|
|
1007
|
-
const timeDelta = (maxServerTime > 0) ? (nowMac - maxServerTime) : 0;
|
|
1008
|
-
|
|
1009
|
-
if (Math.abs(timeDelta) > 500) {
|
|
1010
|
-
console.log(`⏱️ [Clock Sync] Rilevato scostamento di ${(timeDelta/1000).toFixed(1)}s rispetto a einstein. Calibrazione attiva.`);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// 2. TRASLAZIONE DEI DATI STORICI
|
|
1014
|
-
// Spostiamo nel tempo tutti i punti passati per incollarli millimetricamente al "now" del client
|
|
1015
|
-
for (let key in data) {
|
|
1016
|
-
if (store.histories[key] !== undefined) {
|
|
1017
|
-
if (Array.isArray(data[key])) {
|
|
1018
|
-
store.histories[key] = data[key].map(p => ({
|
|
1019
|
-
val: p.val,
|
|
1020
|
-
time: p.time + timeDelta, // Sposta il punto nel tempo per allinearlo al Mac
|
|
1021
|
-
min: p.min !== undefined ? p.min : undefined,
|
|
1022
|
-
max: p.max !== undefined ? p.max : undefined
|
|
1023
|
-
}));
|
|
1024
|
-
} else {
|
|
1025
|
-
store.histories[key] = data[key];
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Sincronizza i dati specifici del radar storici mantendo i timestamp calendarizzati assoluti (0% sfasamento dei perni)
|
|
1031
|
-
if (data.windRadarSlots) {
|
|
1032
|
-
store.windRadarSlots = data.windRadarSlots; // Mantiene gli orari spaccati al minuto per l'uguaglianza del radar
|
|
1033
|
-
}
|
|
1034
|
-
if (data.futureForecast) {
|
|
1035
|
-
store.futureForecast = {
|
|
1036
|
-
...data.futureForecast,
|
|
1037
|
-
timestamp: data.futureForecast.timestamp + timeDelta
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
// data.twd e data.tws sono già mappati direttamente in store.histories.twd e store.histories.tws
|
|
1041
|
-
|
|
1042
|
-
// Sincronizziamo lo storico dei minuti per il radar, ma lasciamo che il buffer rapido 'store.longBuf.twd'
|
|
1043
|
-
// parta pulito all'avvio. Si popolerà istantaneamente a 3Hz con i soli dati in tempo reale,
|
|
1044
|
-
// garantendo una reattività della direzione del vento (TWD) immediata e priva di sbalzi all'avvio.
|
|
1045
|
-
console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
|
|
1046
|
-
}
|
|
1047
|
-
} catch (err) {
|
|
1048
|
-
console.warn("⚠️ Impossibile caricare lo storico dal server. Utilizzo dati vuoti/simulati.");
|
|
1049
|
-
throw err;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/**
|
|
1054
|
-
* manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
|
|
1055
|
-
* Integrazioni:
|
|
1056
|
-
* 1. Strict undefined check per lastUpdates.
|
|
1057
|
-
* 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
|
|
1058
|
-
* 3. Clamping di sicurezza (no negativi, no Infinity).
|
|
1059
|
-
*/
|
|
1060
|
-
function manageHistory(type, value) {
|
|
1061
|
-
// --- 1. VALIDAZIONE INPUT RIGOROSA ---
|
|
1062
|
-
if (value === undefined || value === null || !isFinite(value)) return;
|
|
1063
|
-
|
|
1064
|
-
const now = Date.now();
|
|
1065
|
-
const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
|
|
1066
|
-
const samples = Math.max(2, CONFIG.graphs.samples || 60);
|
|
1067
|
-
const bucketIntervalMs = (historyMinutes * 60000) / samples;
|
|
1068
|
-
|
|
1069
|
-
// --- 2. INIT SICURO (Sintonizzato all'epoca assoluta UTC) ---
|
|
1070
|
-
if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
|
|
1071
|
-
if (!store.histories[type]) store.histories[type] = [];
|
|
1072
|
-
|
|
1073
|
-
// SINTONIZZAZIONE DI FASE: Agganciamo il timer iniziale al multiplo UTC più vicino
|
|
1074
|
-
if (store.lastUpdates[type] === undefined || store.lastUpdates[type] === 0) {
|
|
1075
|
-
store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
const tempBuf = store.graphTempBuf[type];
|
|
1079
|
-
|
|
1080
|
-
// --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
|
|
1081
|
-
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
1082
|
-
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
1083
|
-
const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
|
|
1084
|
-
if (lastPoint && lastPoint.val > glitchThreshold) return;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// --- 4. STORAGE TEMPORANEO ---
|
|
1088
|
-
tempBuf.push({ val: value, time: now });
|
|
1089
|
-
|
|
1090
|
-
// Controllo finestra temporale
|
|
1091
|
-
const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
|
|
1092
|
-
if (!bucketReady) return;
|
|
1093
|
-
|
|
1094
|
-
// --- 5. AGGREGAZIONE SEMANTICA (Ottimizzata a zero-allocazioni di array intermedi) ---
|
|
1095
|
-
let finalValue = value;
|
|
1096
|
-
|
|
1097
|
-
if (tempBuf.length > 0) {
|
|
1098
|
-
// A. VENTO -> SUSTAINED PEAK (EMA Time-Aware) (0 allocazioni)
|
|
1099
|
-
if (type === 'tws' || type === 'aws') {
|
|
1100
|
-
const tauMs = 2500;
|
|
1101
|
-
let ema = tempBuf[0].val;
|
|
1102
|
-
let maxSustained = ema;
|
|
1103
|
-
|
|
1104
|
-
for (let i = 1; i < tempBuf.length; i++) {
|
|
1105
|
-
const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
|
|
1106
|
-
const alpha = 1 - Math.exp(-dt / tauMs);
|
|
1107
|
-
ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
|
|
1108
|
-
if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
|
|
1109
|
-
}
|
|
1110
|
-
finalValue = maxSustained;
|
|
1111
|
-
}
|
|
1112
|
-
// B. PROFONDITÀ -> MINIMO (In-place loop, 0 allocazioni)
|
|
1113
|
-
else if (type === 'depth') {
|
|
1114
|
-
let minVal = Infinity;
|
|
1115
|
-
for (let i = 0; i < tempBuf.length; i++) {
|
|
1116
|
-
const v = tempBuf[i].val;
|
|
1117
|
-
if (isFinite(v) && v < minVal) {
|
|
1118
|
-
minVal = v;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (minVal !== Infinity) {
|
|
1122
|
-
finalValue = minVal;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
// C. VELOCITÀ -> MEDIA (In-place loop, 0 allocazioni)
|
|
1126
|
-
else {
|
|
1127
|
-
let sum = 0;
|
|
1128
|
-
let count = 0;
|
|
1129
|
-
for (let i = 0; i < tempBuf.length; i++) {
|
|
1130
|
-
const v = tempBuf[i].val;
|
|
1131
|
-
if (isFinite(v)) {
|
|
1132
|
-
sum += v;
|
|
1133
|
-
count++;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
if (count > 0) {
|
|
1137
|
-
finalValue = sum / count;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// --- 6. CLAMPING E VALIDAZIONE FINALE ---
|
|
1143
|
-
if (!isFinite(finalValue)) return;
|
|
1144
|
-
finalValue = Math.max(0, finalValue);
|
|
1145
|
-
|
|
1146
|
-
// --- 7. STORAGE STORICO ---
|
|
1147
|
-
store.histories[type].push({ val: finalValue, time: now });
|
|
1148
|
-
|
|
1149
|
-
// --- TRIGGER STRATEGIA 1 (EVENT-DRIVEN) ---
|
|
1150
|
-
// Ridisegnamo lo strumento SOLO nell'esatto istante in cui viene generato un nuovo punto storico!
|
|
1151
|
-
refreshGraph(type);
|
|
1152
|
-
|
|
1153
|
-
// --- 8. PRUNING DINAMICO ---
|
|
1154
|
-
const maxViewportMinutes = historyMinutes * 2;
|
|
1155
|
-
const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
|
|
1156
|
-
|
|
1157
|
-
while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
|
|
1158
|
-
store.histories[type].shift();
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Reset per il prossimo bucket (sincronizzato)
|
|
1162
|
-
store.graphTempBuf[type] = [];
|
|
1163
|
-
store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// ==========================================================================
|
|
1167
|
-
// 9. INTERAZIONI E GESTI
|
|
1168
|
-
// ==========================================================================
|
|
1169
|
-
function toggleFocusMode(type, element) {
|
|
1170
|
-
const container = document.querySelector('.main-container');
|
|
1171
|
-
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
1172
|
-
isFocusActive = !isFocusActive;
|
|
1173
|
-
if (isFocusActive) {
|
|
1174
|
-
container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
1175
|
-
element.classList.add('is-focused');
|
|
1176
|
-
} else {
|
|
1177
|
-
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
1178
|
-
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// --- FORZATURA REDRAW IMMEDIATO DUAL SCREEN ---
|
|
1182
|
-
// Quando entriamo o usciamo dal Focus, ridisegnamo subito lo strumento
|
|
1183
|
-
// interessato per applicare istantaneamente il cambio di spessore della linea!
|
|
1184
|
-
if (['stw', 'sog', 'tws', 'depth'].includes(type)) {
|
|
1185
|
-
refreshGraph(type);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
1190
|
-
const el = document.getElementById(type + '-graph').closest('.data-box');
|
|
1191
|
-
let lastTapTime = 0, tapTimeout, isLongPressActive = false;
|
|
1192
|
-
|
|
1193
|
-
el.addEventListener('pointerdown', (e) => {
|
|
1194
|
-
isLongPressActive = false;
|
|
1195
|
-
pressTimer = setTimeout(() => {
|
|
1196
|
-
if (!isFocusActive) {
|
|
1197
|
-
isLongPressActive = true;
|
|
1198
|
-
toggleFocusMode(type, el);
|
|
1199
|
-
lastTapTime = 0;
|
|
1200
|
-
}
|
|
1201
|
-
}, 1000);
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
el.addEventListener('pointerup', (e) => {
|
|
1205
|
-
clearTimeout(pressTimer);
|
|
1206
|
-
if (isLongPressActive) return;
|
|
1207
|
-
|
|
1208
|
-
const currentTime = new Date().getTime();
|
|
1209
|
-
const tapDelay = currentTime - lastTapTime;
|
|
1210
|
-
|
|
1211
|
-
if (tapDelay < 300 && tapDelay > 0) {
|
|
1212
|
-
clearTimeout(tapTimeout);
|
|
1213
|
-
graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
|
|
1214
|
-
localStorage.setItem('mode_' + type, graphModes[type]);
|
|
1215
|
-
refreshGraph(type);
|
|
1216
|
-
lastTapTime = 0;
|
|
1217
|
-
} else {
|
|
1218
|
-
lastTapTime = currentTime;
|
|
1219
|
-
tapTimeout = setTimeout(() => {
|
|
1220
|
-
if (isFocusActive && el.classList.contains('is-focused')) {
|
|
1221
|
-
toggleFocusMode(type, el);
|
|
1222
|
-
} else if (!isFocusActive) {
|
|
1223
|
-
if (type === 'sog') {
|
|
1224
|
-
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
1225
|
-
refreshGraph('sog'); // --- FORZA RINFRESCO ISTANTANEO AL CAMBIO SOG/VMG ---
|
|
1226
|
-
} else if (type === 'tws') {
|
|
1227
|
-
displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
|
|
1228
|
-
refreshGraph('tws'); // --- FORZA RINFRESCO ISTANTANEO AL CAMBIO TWS/AWS ---
|
|
1229
|
-
}
|
|
1230
|
-
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
1231
|
-
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
1232
|
-
}
|
|
1233
|
-
}, 250);
|
|
1234
|
-
}
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
if (ui.hotspot) {
|
|
1241
|
-
ui.hotspot.addEventListener('pointerdown', (e) => { pressTimer = setTimeout(() => { document.body.classList.toggle('night-mode'); ui.hotspot.style.opacity = "0.5"; setTimeout(() => ui.hotspot.style.opacity = "1", 200); pressTimer = null; }, 1000); });
|
|
1242
|
-
ui.hotspot.addEventListener('pointerup', (e) => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; const doc = document.documentElement; if (document.fullscreenElement) document.exitFullscreen(); else doc.requestFullscreen(); } });
|
|
1243
|
-
ui.hotspot.addEventListener('pointerleave', () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } });
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// ==========================================================================
|
|
1247
|
-
// 10. SIMULAZIONE E RETE
|
|
1248
|
-
// ==========================================================================
|
|
1249
|
-
function startDynamicSimulation() {
|
|
1250
|
-
ui.status.innerText = "SIM ATTIVO";
|
|
1251
|
-
let sim = { hdg: 45, tws: 12, twd: 90, depth: 12, stw: 5, startTime: Date.now() };
|
|
1252
|
-
simInterval = setInterval(() => {
|
|
1253
|
-
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
1254
|
-
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
1255
|
-
const targetTws = 10 + Math.sin(elapsed / 10) * 2;
|
|
1256
|
-
sim.tws += (targetTws - sim.tws) * 0.05; sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
1257
|
-
let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
|
|
1258
|
-
let targetStw = 5; sim.stw += (targetStw - sim.stw) * 0.05;
|
|
1259
|
-
processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
1260
|
-
processIncomingData("environment.wind.speedApparent", ktsToMs(sim.stw + 2));
|
|
1261
|
-
processIncomingData("environment.wind.angleApparent", degToRad(twaRel + 5));
|
|
1262
|
-
processIncomingData("environment.depth.belowTransducer", sim.depth);
|
|
1263
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
|
|
1264
|
-
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
1265
|
-
processIncomingData("navigation.courseOverGroundTrue", degToRad(sim.hdg));
|
|
1266
|
-
}, 1000);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
function connect() {
|
|
1270
|
-
if (simulationMode) return;
|
|
1271
|
-
|
|
1272
|
-
// Pulizia preventiva del vecchio socket orfano (elimina memory leak ed eventi doppi)
|
|
1273
|
-
if (socket) {
|
|
1274
|
-
socket.onopen = null;
|
|
1275
|
-
socket.onmessage = null;
|
|
1276
|
-
socket.onerror = null;
|
|
1277
|
-
socket.onclose = null;
|
|
1278
|
-
try { socket.close(); } catch(e) {}
|
|
1279
|
-
socket = null;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
let addr = window.location.host || CONFIG.server.fallbackIp;
|
|
1283
|
-
try {
|
|
1284
|
-
// Connessione con subscribe=none per impedire a Signal K l'invio automatico di tutto lo stream di bordo
|
|
1285
|
-
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=none`);
|
|
1286
|
-
|
|
1287
|
-
socket.onopen = async () => {
|
|
1288
|
-
ui.status.className = "online";
|
|
1289
|
-
ui.status.innerText = "ONLINE";
|
|
1290
|
-
reconnectDelay = 1000;
|
|
1291
|
-
lastMessageReceivedTime = Date.now(); // Reset timestamp dati
|
|
1292
|
-
|
|
1293
|
-
// Sottoscrizione selettiva bilanciata: applichiamo un filtro a 3 Hz (333ms) per i dati rapidi,
|
|
1294
|
-
// garantendo fluidità matematica senza ritardi e ottimizzando profondità (1s) e GPS (10s)
|
|
1295
|
-
const subscriptionPayload = {
|
|
1296
|
-
context: "vessels.self",
|
|
1297
|
-
subscribe: [
|
|
1298
|
-
{ path: "navigation.position", minPeriod: 60000 }, // Posizione GPS lenta (60s)
|
|
1299
|
-
{ path: "navigation.magneticVariation", minPeriod: 60000 }, // Declinazione lenta (60s)
|
|
1300
|
-
{ path: "environment.depth.belowTransducer", minPeriod: 1000 }, // Profondità di sicurezza (1s)
|
|
1301
|
-
{ path: "navigation.speedThroughWater", minPeriod: 333 }, // Velocità barca a 3 Hz (333ms)
|
|
1302
|
-
{ path: "navigation.speedOverGround", minPeriod: 333 }, // Velocità GPS a 3 Hz
|
|
1303
|
-
{ path: "navigation.courseOverGroundTrue", minPeriod: 333 }, // COG a 3 Hz
|
|
1304
|
-
{ path: "navigation.headingTrue", minPeriod: 333 }, // Heading True a 3 Hz
|
|
1305
|
-
{ path: "navigation.headingMagnetic", minPeriod: 333 }, // Heading Fallback a 3 Hz
|
|
1306
|
-
{ path: "environment.wind.speedApparent", minPeriod: 333 }, // AWS a 3 Hz
|
|
1307
|
-
{ path: "environment.wind.angleApparent", minPeriod: 333 }, // AWA a 3 Hz
|
|
1308
|
-
{ path: "environment.wind.speedTrue", minPeriod: 333 }, // TWS (Nativo) a 3 Hz
|
|
1309
|
-
{ path: "environment.wind.angleTrueWater", minPeriod: 333 }, // TWA (Nativo) a 3 Hz (AGGIUNTO)
|
|
1310
|
-
{ path: "environment.wind.directionTrue", minPeriod: 333 } // TWD (Nativo) a 3 Hz
|
|
1311
|
-
]
|
|
1312
|
-
};
|
|
1313
|
-
|
|
1314
|
-
// Invio del payload per configurare lo streaming selettivo a 3 Hz
|
|
1315
|
-
try {
|
|
1316
|
-
socket.send(JSON.stringify(subscriptionPayload));
|
|
1317
|
-
console.log("🔌 [WebSocket] Sottoscrizione selettiva a 3 Hz inviata con successo (Latenza azzerata).");
|
|
1318
|
-
} catch (err) {
|
|
1319
|
-
console.error("❌ [WebSocket] Impossibile inviare il payload di sottoscrizione:", err);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
// SINCRONIZZAZIONE AUTOMATICA: Ogni volta che la connessione si apre o si riapre,
|
|
1323
|
-
// scarichiamo lo storico fresco dal server e ridisegnamo i grafici
|
|
1324
|
-
try {
|
|
1325
|
-
await fetchServerHistory();
|
|
1326
|
-
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
1327
|
-
} catch (err) {
|
|
1328
|
-
console.warn("⚠️ Sincronizzazione storico fallita alla connessione:", err);
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
|
-
|
|
1332
|
-
socket.onmessage = (e) => {
|
|
1333
|
-
lastMessageReceivedTime = Date.now(); // Aggiorna il battito cardiaco del flusso dati
|
|
1334
|
-
const d = JSON.parse(e.data);
|
|
1335
|
-
if (d.updates) {
|
|
1336
|
-
d.updates.forEach(u => {
|
|
1337
|
-
// Sincronizzazione dell'orologio sul tempo reale del server (NMEA/GPS)
|
|
1338
|
-
const timeMs = u.timestamp ? new Date(u.timestamp).getTime() : Date.now();
|
|
1339
|
-
let sourceLabel = "Unknown";
|
|
1340
|
-
if (u.$source) {
|
|
1341
|
-
sourceLabel = u.$source;
|
|
1342
|
-
} else if (u.source) {
|
|
1343
|
-
if (typeof u.source === 'object') {
|
|
1344
|
-
sourceLabel = u.source.label || u.source.talker || u.source.src || "Unknown";
|
|
1345
|
-
} else {
|
|
1346
|
-
sourceLabel = String(u.source);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
if (u.values) {
|
|
1351
|
-
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel, timeMs));
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1355
|
-
};
|
|
1356
|
-
|
|
1357
|
-
socket.onclose = () => {
|
|
1358
|
-
if (!simulationMode) {
|
|
1359
|
-
ui.status.className = "offline";
|
|
1360
|
-
ui.status.innerText = "OFFLINE";
|
|
1361
|
-
|
|
1362
|
-
// Tentiamo la riconnessione automatica solo se lo schermo è attivo e visibile,
|
|
1363
|
-
// evitando cicli di loop di rete infiniti in background mentre il tablet dorme
|
|
1364
|
-
if (document.visibilityState === "visible") {
|
|
1365
|
-
ui.status.innerText = "RECONNECTING...";
|
|
1366
|
-
setTimeout(connect, reconnectDelay);
|
|
1367
|
-
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
};
|
|
1371
|
-
} catch (e) {
|
|
1372
|
-
setTimeout(connect, reconnectDelay);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// ==========================================================================
|
|
1377
|
-
// 11. INIT E CICLO DI VITA
|
|
1378
|
-
// ==========================================================================
|
|
1379
|
-
window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
1380
|
-
|
|
1381
|
-
async function init() {
|
|
1382
|
-
loadDashboardState();
|
|
1383
|
-
|
|
1384
|
-
// Disegna la grafica statica delle tacche di calibrazione di entrambi gli strumenti
|
|
1385
|
-
initCompassTicks(); // Tacche del Wind Gauge analogico (gauge.js)
|
|
1386
|
-
initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
|
|
1387
|
-
|
|
1388
|
-
// 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
|
|
1389
|
-
const twdBox = document.querySelector('.box-twd');
|
|
1390
|
-
if (twdBox) {
|
|
1391
|
-
let twdPressTimer = null;
|
|
1392
|
-
let longPressTriggered = false; // Flag di controllo della pressione prolungata
|
|
1393
|
-
|
|
1394
|
-
twdBox.addEventListener('pointerdown', (e) => {
|
|
1395
|
-
longPressTriggered = false;
|
|
1396
|
-
if (activeInstrument === 'gauge') {
|
|
1397
|
-
twdPressTimer = setTimeout(() => {
|
|
1398
|
-
activeInstrument = 'radar';
|
|
1399
|
-
document.getElementById('wind-gauge').style.display = 'none';
|
|
1400
|
-
document.getElementById('wind-radar').style.display = 'block';
|
|
1401
|
-
renderRadar(); // Disegna immediatamente il radar all'attivazione
|
|
1402
|
-
twdPressTimer = null;
|
|
1403
|
-
longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
|
|
1404
|
-
}, 1000);
|
|
1405
|
-
}
|
|
1406
|
-
});
|
|
1407
|
-
twdBox.addEventListener('pointerup', () => {
|
|
1408
|
-
if (activeInstrument === 'gauge') {
|
|
1409
|
-
if (twdPressTimer) {
|
|
1410
|
-
clearTimeout(twdPressTimer);
|
|
1411
|
-
twdPressTimer = null;
|
|
1412
|
-
}
|
|
1413
|
-
} else if (activeInstrument === 'radar') {
|
|
1414
|
-
if (longPressTriggered) {
|
|
1415
|
-
// Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
|
|
1416
|
-
longPressTriggered = false;
|
|
1417
|
-
} else {
|
|
1418
|
-
// Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
|
|
1419
|
-
activeInstrument = 'gauge';
|
|
1420
|
-
document.getElementById('wind-radar').style.display = 'none';
|
|
1421
|
-
document.getElementById('wind-gauge').style.display = 'block';
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
});
|
|
1425
|
-
twdBox.addEventListener('pointerleave', () => {
|
|
1426
|
-
if (twdPressTimer) {
|
|
1427
|
-
clearTimeout(twdPressTimer);
|
|
1428
|
-
twdPressTimer = null;
|
|
1429
|
-
}
|
|
1430
|
-
longPressTriggered = false;
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
|
|
1435
|
-
const windRadarSvg = document.getElementById('wind-radar');
|
|
1436
|
-
if (windRadarSvg) {
|
|
1437
|
-
windRadarSvg.addEventListener('pointerup', () => {
|
|
1438
|
-
if (activeInstrument === 'radar') {
|
|
1439
|
-
activeInstrument = 'gauge';
|
|
1440
|
-
windRadarSvg.style.display = 'none';
|
|
1441
|
-
document.getElementById('wind-gauge').style.display = 'block';
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Rileviamo se siamo sul Mac tramite file:// (Ambiente di sviluppo locale)
|
|
1447
|
-
const isLocalFile = (window.location.protocol === 'file:');
|
|
1448
|
-
|
|
1449
|
-
// 3. CARICAMENTO STORICO GRAFICI E RADAR REALI DAL CERBO GX
|
|
1450
|
-
try {
|
|
1451
|
-
await fetchServerHistory();
|
|
1452
|
-
} catch (err) {
|
|
1453
|
-
console.warn("⚠️ Impossibile caricare lo storico reale dal server.");
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// 4. CARICAMENTO CONFIGURAZIONI REALI (Bypassato su Mac per preservare i tuoi test!)
|
|
1457
|
-
if (!isLocalFile) {
|
|
1458
|
-
await fetchServerConfig();
|
|
1459
|
-
} else {
|
|
1460
|
-
console.log("🎮 Esecuzione locale file://: utilizzo delle calibrazioni di CONFIG locali di debug.");
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
startDisplayLoop();
|
|
1464
|
-
connect(); // Si collegherà in tempo reale al WebSocket reale della barca
|
|
1465
|
-
|
|
1466
|
-
// 5. POLL LENTO (15 secondi): aggiorna i dati radar in background SOLO se lo strumento attivo è il radar,
|
|
1467
|
-
// evitando di sovraccaricare la rete ed eliminando il salto visivo di reset dei grafici a 1Hz
|
|
1468
|
-
setInterval(async () => {
|
|
1469
|
-
try {
|
|
1470
|
-
if (activeInstrument === 'radar') {
|
|
1471
|
-
await fetchServerHistory();
|
|
1472
|
-
renderRadar();
|
|
1473
|
-
}
|
|
1474
|
-
} catch (err) {
|
|
1475
|
-
console.warn("⚠️ Errore aggiornamento periodico storico:", err);
|
|
1476
|
-
}
|
|
1477
|
-
}, 15000);
|
|
1478
|
-
|
|
1479
|
-
// Controlla le modifiche di configurazione sul Cerbo solo se non siamo sul Mac via file://
|
|
1480
|
-
if (!isLocalFile) {
|
|
1481
|
-
setInterval(watchConfigChanges, 10000);
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
// Gestore unificato di riattivazione su Android / iOS (Visibility, Focus, Pageshow, Online)
|
|
1486
|
-
const handleWakeUp = () => {
|
|
1487
|
-
if (!document.hidden) {
|
|
1488
|
-
console.log("🔌 [Watchdog] Risveglio dispositivo / Tab attiva. Riconnessione immediata...");
|
|
1489
|
-
reconnectDelay = 1000;
|
|
1490
|
-
connect();
|
|
1491
|
-
} else {
|
|
1492
|
-
console.log("🔌 [Watchdog] Schermo bloccato / Tab in background. Chiusura preventiva WebSocket.");
|
|
1493
|
-
if (socket) {
|
|
1494
|
-
try { socket.close(); } catch(e) {}
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
};
|
|
1498
|
-
|
|
1499
|
-
document.addEventListener('visibilitychange', handleWakeUp);
|
|
1500
|
-
window.addEventListener('pageshow', handleWakeUp);
|
|
1501
|
-
window.addEventListener('focus', handleWakeUp);
|
|
1502
|
-
window.addEventListener('online', handleWakeUp);
|
|
1503
|
-
|
|
1504
|
-
window.addEventListener('load', init);
|
|
1505
|
-
window.addEventListener('pagehide', saveDashboardState);
|