@sailingrotevista/rotevista-dash 4.0.19 → 4.0.21
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 +248 -65
- package/index.js +4 -4
- package/package.json +1 -1
- package/settings.json +3 -3
- package/style.css +9 -2
package/app.js
CHANGED
|
@@ -17,7 +17,7 @@ let CONFIG = {
|
|
|
17
17
|
smoothWindow: 2000,
|
|
18
18
|
longWindow: 30000,
|
|
19
19
|
stabilityTolerance: 2000,
|
|
20
|
-
stabilityThreshold: 0.
|
|
20
|
+
stabilityThreshold: 0.95,
|
|
21
21
|
minSpeed: 0,
|
|
22
22
|
stabilityBreakout: 15
|
|
23
23
|
},
|
|
@@ -35,6 +35,8 @@ const RENDER_INTERVAL_MS = 1000;
|
|
|
35
35
|
const TIMEOUT_MS = 5000;
|
|
36
36
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
37
37
|
const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
|
|
38
|
+
const sourceLocks = {};
|
|
39
|
+
|
|
38
40
|
|
|
39
41
|
// ==========================================================================
|
|
40
42
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
@@ -53,6 +55,10 @@ let twDirty = false, isNavigating = false, reconnectDelay = 1000;
|
|
|
53
55
|
|
|
54
56
|
let pressTimer, isFocusActive = false;
|
|
55
57
|
|
|
58
|
+
let emaTwaSin = 0;
|
|
59
|
+
let emaTwaCos = 0;
|
|
60
|
+
let firstEmaRun = true;
|
|
61
|
+
|
|
56
62
|
// Stato dei singoli grafici (Standard vs Hercules Zoom)
|
|
57
63
|
const graphModes = {
|
|
58
64
|
stw: 'standard',
|
|
@@ -110,39 +116,106 @@ function getShortestRotation(curr, target) {
|
|
|
110
116
|
/**
|
|
111
117
|
* Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
|
|
112
118
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
121
|
+
*/
|
|
122
|
+
function safePush(buffer, val, time) {
|
|
123
|
+
// --- PROTEZIONE ANTI-NaN (FONDAMENTALE ALL'AVVIO) ---
|
|
124
|
+
// Se il valore è nullo, non definito o non è un numero, ignoriamo l'inserimento
|
|
125
|
+
if (val === null || val === undefined || isNaN(val)) return;
|
|
126
|
+
|
|
127
|
+
buffer.push({
|
|
128
|
+
val: val,
|
|
129
|
+
time: time,
|
|
130
|
+
sin: Math.sin(val),
|
|
131
|
+
cos: Math.cos(val)
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
|
|
135
|
+
// + 1 minuto di margine
|
|
136
|
+
const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
|
|
137
|
+
|
|
138
|
+
while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
|
|
139
|
+
buffer.shift();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
|
|
143
|
+
if (buffer.length > 36000) buffer.shift();
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
/**
|
|
119
|
-
* Media Circolare Vettoriale
|
|
147
|
+
* Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
|
|
148
|
+
* Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
|
|
120
149
|
*/
|
|
121
|
-
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
let sSin = 0, sCos = 0;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
151
|
+
now = now || Date.now();
|
|
152
|
+
const len = bufferArray.length;
|
|
153
|
+
if (len === 0) return null;
|
|
154
|
+
|
|
155
|
+
let sSin = 0, sCos = 0, count = 0;
|
|
156
|
+
let newestTime = 0, oldestTime = 0;
|
|
157
|
+
|
|
158
|
+
// 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
|
|
159
|
+
let pilotSin = 0, pilotCos = 0;
|
|
160
|
+
const pilotSamples = Math.min(len, 15);
|
|
161
|
+
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
162
|
+
pilotSin += bufferArray[i].sin;
|
|
163
|
+
pilotCos += bufferArray[i].cos;
|
|
164
|
+
}
|
|
165
|
+
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
166
|
+
|
|
167
|
+
// Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
|
|
168
|
+
const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
|
|
169
|
+
|
|
170
|
+
// 2. CALCOLO AMMORTIZZATO
|
|
171
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
172
|
+
const item = bufferArray[i];
|
|
173
|
+
if ((now - item.time) > windowMs) break;
|
|
174
|
+
|
|
175
|
+
// Troviamo la differenza angolare (da -Pi a +Pi) tra il dato e la Media Pilota
|
|
176
|
+
let diffRad = Math.atan2(
|
|
177
|
+
Math.sin(item.val - pilotRad),
|
|
178
|
+
Math.cos(item.val - pilotRad)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
let finalSin, finalCos;
|
|
182
|
+
|
|
183
|
+
// Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
|
|
184
|
+
if (Math.abs(diffRad) > limitRad) {
|
|
185
|
+
// Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
|
|
186
|
+
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
187
|
+
// Ricalcoliamo l'angolo ammortizzato
|
|
188
|
+
const clampedRad = pilotRad + clampedDiff;
|
|
189
|
+
|
|
190
|
+
finalSin = Math.sin(clampedRad);
|
|
191
|
+
finalCos = Math.cos(clampedRad);
|
|
192
|
+
} else {
|
|
193
|
+
// Il dato è buono, usiamo i valori precalcolati
|
|
194
|
+
finalSin = item.sin;
|
|
195
|
+
finalCos = item.cos;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
sSin += finalSin;
|
|
199
|
+
sCos += finalCos;
|
|
200
|
+
|
|
201
|
+
if (count === 0) newestTime = item.time;
|
|
202
|
+
oldestTime = item.time;
|
|
203
|
+
count++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (count === 0) return null;
|
|
138
207
|
|
|
139
|
-
|
|
140
|
-
|
|
208
|
+
const R = Math.hypot(sSin, sCos) / count;
|
|
209
|
+
const avgRad = Math.atan2(sSin, sCos);
|
|
210
|
+
const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
211
|
+
const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
|
|
212
|
+
const safeR = Math.max(R, 1e-9);
|
|
141
213
|
|
|
142
214
|
return {
|
|
143
|
-
val:
|
|
144
|
-
stable:
|
|
145
|
-
dev:
|
|
215
|
+
val: finalVal,
|
|
216
|
+
stable: historyDuration > 10000 && R > CONFIG.averaging.stabilityThreshold,
|
|
217
|
+
dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
|
|
218
|
+
samples: count
|
|
146
219
|
};
|
|
147
220
|
}
|
|
148
221
|
|
|
@@ -291,14 +364,58 @@ function computeTrueWind() {
|
|
|
291
364
|
}
|
|
292
365
|
}
|
|
293
366
|
|
|
294
|
-
function processIncomingData(path, val) {
|
|
295
|
-
const now = Date.now();
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
367
|
+
function processIncomingData(path, val, source) {
|
|
368
|
+
const now = Date.now();
|
|
369
|
+
|
|
370
|
+
// --- 1. FILTRO SORGENTE STICKY (Elimina conflitti yacht_device vs Unknown) ---
|
|
371
|
+
if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
|
|
372
|
+
sourceLocks[path] = { label: source, lastSeen: now };
|
|
373
|
+
} else {
|
|
374
|
+
// Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
|
|
379
|
+
store.timestamps[path] = now;
|
|
380
|
+
store.raw[path] = val;
|
|
381
|
+
|
|
382
|
+
// Buffer per AWA (Vento Apparente)
|
|
383
|
+
if (path === "environment.wind.angleApparent") {
|
|
384
|
+
safePush(store.smoothBuf.awa, val, now);
|
|
385
|
+
safePush(store.longBuf.awa, val, now);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Buffer per HDG (Prua)
|
|
389
|
+
if (path === "navigation.headingTrue") {
|
|
390
|
+
safePush(store.smoothBuf.hdg, val, now);
|
|
391
|
+
safePush(store.longBuf.hdg, val, now);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Buffer per COG (Rotta Fondo)
|
|
395
|
+
if (path === "navigation.courseOverGroundTrue") {
|
|
396
|
+
safePush(store.smoothBuf.cog, val, now);
|
|
397
|
+
safePush(store.longBuf.cog, val, now);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
|
|
401
|
+
const twPaths = [
|
|
402
|
+
"environment.wind.speedApparent",
|
|
403
|
+
"environment.wind.angleApparent",
|
|
404
|
+
"navigation.speedThroughWater",
|
|
405
|
+
"navigation.speedOverGround",
|
|
406
|
+
"navigation.headingTrue",
|
|
407
|
+
"navigation.courseOverGroundTrue"
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
if (twPaths.includes(path)) {
|
|
411
|
+
twDirty = true;
|
|
412
|
+
// Calcolo limitato a 10Hz per non pesare sulla CPU
|
|
413
|
+
if (twDirty && (now - lastTWCompute > 100)) {
|
|
414
|
+
computeTrueWind();
|
|
415
|
+
lastTWCompute = now;
|
|
416
|
+
twDirty = false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
302
419
|
}
|
|
303
420
|
|
|
304
421
|
// ==========================================================================
|
|
@@ -312,8 +429,9 @@ function updateWindTrend() {
|
|
|
312
429
|
|
|
313
430
|
// STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
|
|
314
431
|
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
315
|
-
const
|
|
316
|
-
|
|
432
|
+
const multiplier = isNavigating ? 1 : 2;
|
|
433
|
+
const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
|
|
434
|
+
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
|
|
317
435
|
if (!twaNow || !twaRef || !twdNow || !twdRef) return;
|
|
318
436
|
const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
|
|
319
437
|
const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
|
|
@@ -357,12 +475,41 @@ function updateWindTrend() {
|
|
|
357
475
|
}
|
|
358
476
|
} else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
|
|
359
477
|
|
|
360
|
-
// ALLARME STRAMBATA (GYBE)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
478
|
+
// --- LOGICA ALLARME STRAMBATA (GYBE) FILTRATA ---
|
|
479
|
+
const instTwaRad = store.raw["environment.wind.angleTrueWater"];
|
|
480
|
+
|
|
481
|
+
if (instTwaRad !== undefined) {
|
|
482
|
+
// Calcolo Alfa basato sulla Steering Precision (più precisione = filtro più lento e stabile)
|
|
483
|
+
const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
|
|
484
|
+
|
|
485
|
+
const currentSin = Math.sin(instTwaRad);
|
|
486
|
+
const currentCos = Math.cos(instTwaRad);
|
|
487
|
+
|
|
488
|
+
if (firstEmaRun) {
|
|
489
|
+
emaTwaSin = currentSin;
|
|
490
|
+
emaTwaCos = currentCos;
|
|
491
|
+
firstEmaRun = false;
|
|
492
|
+
} else {
|
|
493
|
+
// Media Esponenziale Vettoriale
|
|
494
|
+
emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
|
|
495
|
+
emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Angolo risultante filtrato
|
|
499
|
+
const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
|
|
500
|
+
|
|
501
|
+
// Verifichiamo il cambio di mure (> 155° e inversione di segno confermata dal filtro)
|
|
502
|
+
if (Math.abs(smoothedTwaDeg) > 155) {
|
|
503
|
+
if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
|
|
504
|
+
if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
|
|
505
|
+
lastGybeAlarmTime = now;
|
|
506
|
+
playGybeAlarm();
|
|
507
|
+
console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
lastInstantTwa = smoothedTwaDeg;
|
|
512
|
+
}
|
|
366
513
|
}
|
|
367
514
|
|
|
368
515
|
// ==========================================================================
|
|
@@ -385,12 +532,13 @@ function refreshGraph(t) {
|
|
|
385
532
|
* upUI: Aggiornamento valori digitali con logica anti-ritardo (Istantaneo vs Media)
|
|
386
533
|
*/
|
|
387
534
|
const upUI = (el, obj, instantRaw, isCompass = false) => {
|
|
388
|
-
if (!obj || obj.val === null || instantRaw === undefined) {
|
|
389
|
-
|
|
535
|
+
if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
|
|
536
|
+
el.innerHTML = "---°";
|
|
537
|
+
el.classList.remove('unstable-data');
|
|
390
538
|
} else {
|
|
391
539
|
let valDeg = Math.round(radToDeg(obj.val));
|
|
392
540
|
let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "°";
|
|
393
|
-
let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.
|
|
541
|
+
let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.8em; opacity: 0.4; margin-left: 6px;">±${obj.dev}</span>` : "";
|
|
394
542
|
el.innerHTML = mainVal + dev;
|
|
395
543
|
|
|
396
544
|
let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
|
|
@@ -512,30 +660,39 @@ function startDisplayLoop() {
|
|
|
512
660
|
upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
|
|
513
661
|
upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
|
|
514
662
|
|
|
515
|
-
// --- LOGICA TACK STRATEGICA (
|
|
663
|
+
// --- LOGICA TACK STRATEGICA (VETTORIALE) ---
|
|
516
664
|
if (hObj && twdObj) {
|
|
517
|
-
|
|
518
|
-
const
|
|
665
|
+
// Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
|
|
666
|
+
const reflectAngle = (targetRad, axisRad) => {
|
|
667
|
+
const diffSin = Math.sin(axisRad - targetRad);
|
|
668
|
+
const diffCos = Math.cos(axisRad - targetRad);
|
|
669
|
+
return Math.atan2(Math.sin(axisRad) * diffCos + Math.cos(axisRad) * diffSin,
|
|
670
|
+
Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
519
674
|
|
|
520
675
|
if (!isNavigating) {
|
|
521
|
-
ui.tackHdg.innerHTML = "---°";
|
|
676
|
+
ui.tackHdg.innerHTML = "---°";
|
|
522
677
|
} else if (unstableH) {
|
|
523
678
|
ui.tackHdg.innerHTML = "---°"; ui.tackHdg.classList.add('unstable-data');
|
|
524
679
|
} else {
|
|
525
|
-
|
|
680
|
+
const reflectedH = reflectAngle(hObj.val, twdObj.val);
|
|
681
|
+
const outH = (radToDeg(reflectedH) + 360) % 360;
|
|
682
|
+
ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}°`;
|
|
526
683
|
ui.tackHdg.classList.remove('unstable-data');
|
|
527
684
|
}
|
|
528
685
|
|
|
529
686
|
if (cObj) {
|
|
530
|
-
const
|
|
531
|
-
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout || twdObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
532
|
-
|
|
687
|
+
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
|
|
533
688
|
if (!isNavigating) {
|
|
534
|
-
ui.tackCog.innerHTML = "---°";
|
|
689
|
+
ui.tackCog.innerHTML = "---°";
|
|
535
690
|
} else if (unstableC) {
|
|
536
691
|
ui.tackCog.innerHTML = "---°"; ui.tackCog.classList.add('unstable-data');
|
|
537
692
|
} else {
|
|
538
|
-
|
|
693
|
+
const reflectedC = reflectAngle(cObj.val, twdObj.val);
|
|
694
|
+
const outC = (radToDeg(reflectedC) + 360) % 360;
|
|
695
|
+
ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}°`;
|
|
539
696
|
ui.tackCog.classList.remove('unstable-data');
|
|
540
697
|
}
|
|
541
698
|
}
|
|
@@ -556,7 +713,7 @@ function startDisplayLoop() {
|
|
|
556
713
|
// 8. CONFIGURAZIONE E GRAFICI UTILS
|
|
557
714
|
// ==========================================================================
|
|
558
715
|
/**
|
|
559
|
-
* Recupera la configurazione e
|
|
716
|
+
* Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
|
|
560
717
|
*/
|
|
561
718
|
async function fetchServerConfig() {
|
|
562
719
|
try {
|
|
@@ -567,19 +724,27 @@ async function fetchServerConfig() {
|
|
|
567
724
|
// Stampa di debug per verificare cosa riceve il client
|
|
568
725
|
console.log("🔍 Configurazione ricevuta dal Server:", data);
|
|
569
726
|
|
|
570
|
-
// Merge intelligente dei dati ricevuti
|
|
727
|
+
// Merge intelligente dei dati ricevuti
|
|
571
728
|
Object.assign(CONFIG.alarms, data.alarms || {});
|
|
572
729
|
Object.assign(CONFIG.graphs, data.graphs || {});
|
|
573
730
|
Object.assign(CONFIG.averaging, data.averaging || {});
|
|
574
731
|
|
|
575
|
-
//
|
|
732
|
+
// --- LOGICA DI MIGRAZIONE SILENZIOSA ---
|
|
733
|
+
// Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
|
|
734
|
+
// Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
|
|
735
|
+
if (CONFIG.averaging.stabilityThreshold <= 0.85) {
|
|
736
|
+
CONFIG.averaging.stabilityThreshold = 0.95;
|
|
737
|
+
console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
|
|
576
741
|
if (data.scales) {
|
|
577
742
|
for (let key in data.scales) {
|
|
578
743
|
if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
|
|
579
744
|
}
|
|
580
745
|
}
|
|
581
746
|
|
|
582
|
-
console.log("✅ Configurazione applicata.
|
|
747
|
+
console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
|
|
583
748
|
} catch (err) {
|
|
584
749
|
console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
|
|
585
750
|
}
|
|
@@ -691,10 +856,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
691
856
|
style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
|
|
692
857
|
|
|
693
858
|
if (i === 1) areaPath += `L ${x1} ${y1} `;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
859
|
+
areaPath += `L ${x2} ${y2} `;
|
|
860
|
+
|
|
861
|
+
// Salviamo l'ultima coordinata X calcolata per chiudere correttamente il path
|
|
862
|
+
if (i === d.length - 1) {
|
|
863
|
+
areaPath += `L ${x2} ${h} Z`;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
698
866
|
|
|
699
867
|
// 4. Iniezione del Gradiente
|
|
700
868
|
const gradId = `grad-${id}`;
|
|
@@ -775,7 +943,22 @@ function connect() {
|
|
|
775
943
|
try {
|
|
776
944
|
socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
|
|
777
945
|
socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
|
|
778
|
-
|
|
946
|
+
|
|
947
|
+
socket.onmessage = (e) => {
|
|
948
|
+
const d = JSON.parse(e.data);
|
|
949
|
+
if (d.updates) {
|
|
950
|
+
d.updates.forEach(u => {
|
|
951
|
+
// 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
|
|
952
|
+
const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
|
|
953
|
+
|
|
954
|
+
// 2. Passiamo il nome della sorgente come TERZO parametro
|
|
955
|
+
if (u.values) {
|
|
956
|
+
u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
|
|
779
962
|
socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
|
|
780
963
|
} catch (e) { setTimeout(connect, reconnectDelay); }
|
|
781
964
|
}
|
package/index.js
CHANGED
|
@@ -127,10 +127,10 @@ module.exports = function (app) {
|
|
|
127
127
|
default: 0.5
|
|
128
128
|
},
|
|
129
129
|
stabilityThreshold: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
type: 'number',
|
|
131
|
+
title: 'Steering Precision (Sensitivity)',
|
|
132
|
+
description: "How strictly the system judges data coherence (0.0 to 1.0). Due to internal smoothing, 0.97-0.98 requires racing precision; 0.93-0.95 is ideal for cruising in waves. Below this, the display rarely alerts for instability.",
|
|
133
|
+
default: 0.95
|
|
134
134
|
},
|
|
135
135
|
stabilityBreakout: {
|
|
136
136
|
type: 'number',
|
package/package.json
CHANGED
package/settings.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"alarms": {
|
|
3
3
|
"depthDanger": 2.5,
|
|
4
|
-
"depthWarning": 5
|
|
4
|
+
"depthWarning": 3.5
|
|
5
5
|
},
|
|
6
6
|
"graphs": {
|
|
7
7
|
"reef1": 15.0,
|
|
8
8
|
"reef2": 20.0,
|
|
9
|
-
"historyMinutes":
|
|
9
|
+
"historyMinutes": 30
|
|
10
10
|
},
|
|
11
11
|
"averaging": {
|
|
12
12
|
"longWindow": 30000,
|
|
13
13
|
"smoothWindow": 2000,
|
|
14
14
|
"minSpeed": 0.5,
|
|
15
|
-
"stabilityThreshold": 0.
|
|
15
|
+
"stabilityThreshold": 0.95,
|
|
16
16
|
"stabilityBreakout": 15
|
|
17
17
|
},
|
|
18
18
|
"scales": {
|
package/style.css
CHANGED
|
@@ -122,6 +122,12 @@ body {
|
|
|
122
122
|
.box-twd #twd-avg { font-size: clamp(1.4rem, 45cqh, 20cqw) !important; }
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/* --- OTTIMIZZAZIONE SPECIFICA TWD PER EVITARE ANDATA A CAPO in landscape--- */
|
|
126
|
+
.box-twd .value-large {
|
|
127
|
+
/* Riduciamo il limite di larghezza (18cqw) per far spazio al simbolo ± rimpicciolendo il testo se serve */
|
|
128
|
+
font-size: clamp(1.2rem, 55cqh, 18cqw) !important;
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
/* --- BOX TACK (Mure Opposte) --- */
|
|
126
132
|
.box-tack .dual-value-container { display: flex; flex-direction: row; justify-content: space-between; align-items: center; width: 100%; height: 100%; }
|
|
127
133
|
.box-tack .dual-value-col { display: flex; flex-direction: column; justify-content: center; }
|
|
@@ -169,7 +175,7 @@ body.night-mode .alarm-danger {
|
|
|
169
175
|
|
|
170
176
|
.focus-active .is-focused .sparkline path,
|
|
171
177
|
.focus-active .is-focused .sparkline line {
|
|
172
|
-
stroke-width: 1.5px !important;
|
|
178
|
+
stroke-width: 1.5px !important; /* Un briciolo più spessa per la stabilità visiva su schermi grandi */
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
.focus-active.focus-side-left {
|
|
@@ -435,10 +441,11 @@ body.night-mode {
|
|
|
435
441
|
|
|
436
442
|
/* --- 10.3 GRAFICI (STILE DINAMICO NIGHT MODE) --- */
|
|
437
443
|
.night-mode .graph-wrapper {
|
|
438
|
-
background: rgba(10, 0, 0, 0.8) !important;
|
|
444
|
+
background: rgba(10, 0, 0, 0.8) !important; /* Sfondo scurissimo per alto contrasto */
|
|
439
445
|
border: 1px solid #330000;
|
|
440
446
|
}
|
|
441
447
|
|
|
448
|
+
/* Griglia temporale (minuti e livelli): quasi impercettibile */
|
|
442
449
|
.night-mode .sparkline line[stroke="rgba(0,0,0,0.12)"],
|
|
443
450
|
.night-mode .sparkline line[stroke="rgba(0,0,0,0.08)"] {
|
|
444
451
|
stroke: rgba(255, 0, 0, 0.1) !important; /* Griglia temporale rossa soffusa */
|