@sailingrotevista/rotevista-dash 6.0.3 → 6.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +151 -14
- package/index.js +13 -11
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ==========================================================================
|
|
3
|
-
* Signal K Wind Dashboard - Pro Version
|
|
3
|
+
* Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Autore: Sailing Rotevista
|
|
6
6
|
* Motore di calcolo tattico per navigazione e crociera.
|
|
@@ -35,7 +35,7 @@ let CONFIG = {
|
|
|
35
35
|
const RENDER_INTERVAL_MS = 1000;
|
|
36
36
|
const TIMEOUT_MS = 15000;
|
|
37
37
|
const SIM_SAMPLE_INTERVAL = 1000;
|
|
38
|
-
const DASH_VERSION = "6.0"; // Major Update:
|
|
38
|
+
const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pro v6.0)
|
|
39
39
|
|
|
40
40
|
// ==========================================================================
|
|
41
41
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
@@ -616,11 +616,14 @@ function startDisplayLoop() {
|
|
|
616
616
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
617
617
|
ui.stw.innerText = stwKts.toFixed(1);
|
|
618
618
|
ui.stw.style.color = ""; // Neutro
|
|
619
|
+
manageHistory('stw', stwKts);
|
|
619
620
|
}
|
|
620
621
|
|
|
621
622
|
// --- LOGICA SOG / VMG ---
|
|
622
623
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
623
624
|
const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
625
|
+
manageHistory('vmg', vmgVal);
|
|
626
|
+
manageHistory('sog', sogKts);
|
|
624
627
|
|
|
625
628
|
const labelSogVmg = document.getElementById('sog-vmg-label');
|
|
626
629
|
if (displayModeSog === 'VMG') {
|
|
@@ -649,12 +652,16 @@ function startDisplayLoop() {
|
|
|
649
652
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
650
653
|
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
651
654
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
655
|
+
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
652
656
|
}
|
|
653
657
|
|
|
654
658
|
// --- GESTIONE VENTO (TWS / AWS SWITCH) ---
|
|
655
659
|
const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
656
660
|
const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
|
|
657
661
|
|
|
662
|
+
if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
|
|
663
|
+
if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
|
|
664
|
+
|
|
658
665
|
if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
659
666
|
const labelWind = document.getElementById('tws-aws-label');
|
|
660
667
|
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
@@ -687,7 +694,7 @@ function startDisplayLoop() {
|
|
|
687
694
|
if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
|
|
688
695
|
|
|
689
696
|
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
690
|
-
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI
|
|
697
|
+
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
691
698
|
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
692
699
|
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
693
700
|
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
@@ -758,6 +765,16 @@ function startDisplayLoop() {
|
|
|
758
765
|
|
|
759
766
|
let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
|
|
760
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
|
|
770
|
+
*/
|
|
771
|
+
function getApiUrl(path) {
|
|
772
|
+
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
773
|
+
return `http://${CONFIG.server.fallbackIp}${path}`;
|
|
774
|
+
}
|
|
775
|
+
return path;
|
|
776
|
+
}
|
|
777
|
+
|
|
761
778
|
/**
|
|
762
779
|
* Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
|
|
763
780
|
*/
|
|
@@ -784,7 +801,7 @@ function applyConfigData(data) {
|
|
|
784
801
|
*/
|
|
785
802
|
async function fetchServerConfig() {
|
|
786
803
|
try {
|
|
787
|
-
const response = await fetch('/rotevista-config');
|
|
804
|
+
const response = await fetch(getApiUrl('/rotevista-config'));
|
|
788
805
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
789
806
|
const data = await response.json();
|
|
790
807
|
|
|
@@ -803,7 +820,7 @@ async function fetchServerConfig() {
|
|
|
803
820
|
*/
|
|
804
821
|
async function watchConfigChanges() {
|
|
805
822
|
try {
|
|
806
|
-
const response = await fetch('/rotevista-config');
|
|
823
|
+
const response = await fetch(getApiUrl('/rotevista-config'));
|
|
807
824
|
if (!response.ok) return;
|
|
808
825
|
const data = await response.json();
|
|
809
826
|
|
|
@@ -834,7 +851,7 @@ async function watchConfigChanges() {
|
|
|
834
851
|
*/
|
|
835
852
|
async function fetchServerHistory() {
|
|
836
853
|
try {
|
|
837
|
-
const response = await fetch('/rotevista-history');
|
|
854
|
+
const response = await fetch(getApiUrl('/rotevista-history'));
|
|
838
855
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
839
856
|
const data = await response.json();
|
|
840
857
|
|
|
@@ -852,6 +869,96 @@ async function fetchServerHistory() {
|
|
|
852
869
|
}
|
|
853
870
|
}
|
|
854
871
|
|
|
872
|
+
/**
|
|
873
|
+
* manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
|
|
874
|
+
* Integrazioni:
|
|
875
|
+
* 1. Strict undefined check per lastUpdates.
|
|
876
|
+
* 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
|
|
877
|
+
* 3. Clamping di sicurezza (no negativi, no Infinity).
|
|
878
|
+
*/
|
|
879
|
+
function manageHistory(type, value) {
|
|
880
|
+
// --- 1. VALIDAZIONE INPUT RIGOROSA ---
|
|
881
|
+
if (value === undefined || value === null || !isFinite(value)) return;
|
|
882
|
+
|
|
883
|
+
const now = Date.now();
|
|
884
|
+
const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
|
|
885
|
+
const samples = Math.max(2, CONFIG.graphs.samples || 60);
|
|
886
|
+
const bucketIntervalMs = (historyMinutes * 60000) / samples;
|
|
887
|
+
|
|
888
|
+
// --- 2. INIT SICURO (Strict Check) ---
|
|
889
|
+
if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
|
|
890
|
+
if (!store.histories[type]) store.histories[type] = [];
|
|
891
|
+
if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
|
|
892
|
+
|
|
893
|
+
const tempBuf = store.graphTempBuf[type];
|
|
894
|
+
|
|
895
|
+
// --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
|
|
896
|
+
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
897
|
+
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
898
|
+
const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
|
|
899
|
+
if (lastPoint && lastPoint.val > glitchThreshold) return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// --- 4. STORAGE TEMPORANEO ---
|
|
903
|
+
tempBuf.push({ val: value, time: now });
|
|
904
|
+
|
|
905
|
+
// Controllo finestra temporale
|
|
906
|
+
const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
|
|
907
|
+
if (!bucketReady) return;
|
|
908
|
+
|
|
909
|
+
// --- 5. AGGREGAZIONE SEMANTICA ---
|
|
910
|
+
let finalValue = value;
|
|
911
|
+
|
|
912
|
+
if (tempBuf.length > 0) {
|
|
913
|
+
// A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
|
|
914
|
+
if (type === 'tws' || type === 'aws') {
|
|
915
|
+
const tauMs = 2500;
|
|
916
|
+
let ema = tempBuf[0].val;
|
|
917
|
+
let maxSustained = ema;
|
|
918
|
+
|
|
919
|
+
for (let i = 1; i < tempBuf.length; i++) {
|
|
920
|
+
const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
|
|
921
|
+
const alpha = 1 - Math.exp(-dt / tauMs);
|
|
922
|
+
ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
|
|
923
|
+
if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
|
|
924
|
+
}
|
|
925
|
+
finalValue = maxSustained;
|
|
926
|
+
}
|
|
927
|
+
// B. PROFONDITÀ -> MINIMO
|
|
928
|
+
else if (type === 'depth') {
|
|
929
|
+
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
930
|
+
if (vals.length > 0) finalValue = Math.min(...vals);
|
|
931
|
+
}
|
|
932
|
+
// C. VELOCITÀ -> MEDIA
|
|
933
|
+
else {
|
|
934
|
+
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
935
|
+
if (vals.length > 0) {
|
|
936
|
+
const sum = vals.reduce((a, b) => a + b, 0);
|
|
937
|
+
finalValue = sum / tempBuf.length;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// --- 6. CLAMPING E VALIDAZIONE FINALE ---
|
|
943
|
+
if (!isFinite(finalValue)) return;
|
|
944
|
+
finalValue = Math.max(0, finalValue);
|
|
945
|
+
|
|
946
|
+
// --- 7. STORAGE STORICO ---
|
|
947
|
+
store.histories[type].push({ val: finalValue, time: now });
|
|
948
|
+
|
|
949
|
+
// --- 8. PRUNING DINAMICO ---
|
|
950
|
+
const maxViewportMinutes = historyMinutes * 2;
|
|
951
|
+
const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
|
|
952
|
+
|
|
953
|
+
while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
|
|
954
|
+
store.histories[type].shift();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Reset per il prossimo bucket
|
|
958
|
+
store.graphTempBuf[type] = [];
|
|
959
|
+
store.lastUpdates[type] = now;
|
|
960
|
+
}
|
|
961
|
+
|
|
855
962
|
/**
|
|
856
963
|
* Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
|
|
857
964
|
* Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
|
|
@@ -880,7 +987,7 @@ function calculateScale(type, data, mode) {
|
|
|
880
987
|
if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
|
|
881
988
|
|
|
882
989
|
const now = Date.now();
|
|
883
|
-
const depthSafetyWindowMs = 120000; // 2 minuti
|
|
990
|
+
const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
|
|
884
991
|
|
|
885
992
|
// Estrazione dati reali degli ultimi 2 minuti con timestamp
|
|
886
993
|
const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
|
|
@@ -1015,6 +1122,7 @@ function refreshGraph(t) {
|
|
|
1015
1122
|
|
|
1016
1123
|
/**
|
|
1017
1124
|
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
1125
|
+
* Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
|
|
1018
1126
|
*/
|
|
1019
1127
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
1020
1128
|
const svg = document.getElementById(id);
|
|
@@ -1023,7 +1131,11 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1023
1131
|
const w = 200, h = 40;
|
|
1024
1132
|
const range = max - min || 1;
|
|
1025
1133
|
const isDepth = (id === 'depth-graph');
|
|
1026
|
-
|
|
1134
|
+
|
|
1135
|
+
// --- RISOLUZIONE DISALLINEAMENTO ORARIO ---
|
|
1136
|
+
// Usiamo il tempo del sensore (l'ultimo dato ricevuto) invece dell'orologio del tablet
|
|
1137
|
+
const latestPoint = d[d.length - 1];
|
|
1138
|
+
const now = latestPoint ? latestPoint.time : Date.now();
|
|
1027
1139
|
|
|
1028
1140
|
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
1029
1141
|
const viewportMs = visibleMinutes * 60000;
|
|
@@ -1253,21 +1365,46 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
|
1253
1365
|
async function init() {
|
|
1254
1366
|
loadDashboardState();
|
|
1255
1367
|
|
|
1256
|
-
// Prova a caricare lo storico dal
|
|
1368
|
+
// Prova a caricare lo storico reale dal Cerbo GX tramite l'API deviata
|
|
1257
1369
|
try {
|
|
1258
1370
|
await fetchServerHistory();
|
|
1259
1371
|
} catch (err) {
|
|
1260
|
-
|
|
1261
|
-
// Se sei in modalità simulazione locale, puoi generare dati finti qui (opzionale)
|
|
1262
|
-
console.log("🎮 Modalità Simulazione Locale attiva.");
|
|
1263
|
-
}
|
|
1372
|
+
console.warn("⚠️ Impossibile caricare lo storico dal server.");
|
|
1264
1373
|
}
|
|
1265
1374
|
|
|
1266
1375
|
await fetchServerConfig();
|
|
1267
1376
|
startDisplayLoop();
|
|
1268
|
-
connect();
|
|
1377
|
+
connect(); // Si collegherà in tempo reale al WebSocket del Cerbo (usando l'IP di fallback se sei su Mac)
|
|
1378
|
+
|
|
1269
1379
|
setInterval(watchConfigChanges, 10000);
|
|
1380
|
+
|
|
1381
|
+
// ==========================================================================
|
|
1382
|
+
// WATCHDOG DI RISVEGLIO (SLEEP/WAKE DETECTOR)
|
|
1383
|
+
// ==========================================================================
|
|
1384
|
+
|
|
1385
|
+
// Rileva quando la scheda del browser torna in primo piano (es. sblocco iPad o Mac aperto)
|
|
1386
|
+
document.addEventListener('visibilitychange', () => {
|
|
1387
|
+
if (document.visibilityState === 'visible') {
|
|
1388
|
+
handleWakeUp();
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// Rileva se il computer è andato in sospensione misurando il ritardo dei secondi
|
|
1393
|
+
let lastHeartbeat = Date.now();
|
|
1394
|
+
setInterval(() => {
|
|
1395
|
+
const now = Date.now();
|
|
1396
|
+
const diff = now - lastHeartbeat;
|
|
1397
|
+
lastHeartbeat = now;
|
|
1398
|
+
|
|
1399
|
+
// Se passano più di 6 secondi tra un ciclo e l'altro (invece di 1 secondo),
|
|
1400
|
+
// significa che il PC era in sospensione. Avviamo il risveglio.
|
|
1401
|
+
if (diff > 6000) {
|
|
1402
|
+
handleWakeUp();
|
|
1403
|
+
}
|
|
1404
|
+
}, 1000);
|
|
1270
1405
|
}
|
|
1271
1406
|
|
|
1407
|
+
|
|
1408
|
+
|
|
1272
1409
|
window.addEventListener('load', init);
|
|
1273
1410
|
window.addEventListener('pagehide', saveDashboardState);
|
package/index.js
CHANGED
|
@@ -37,17 +37,19 @@ module.exports = function (app) {
|
|
|
37
37
|
lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
|
|
38
38
|
raw = {};
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
// 2. Registra le rotte API solo la prima volta (Abilitate per CORS remoto)
|
|
41
|
+
if (!routeRegistered) {
|
|
42
|
+
app.get('/rotevista-config', (req, res) => {
|
|
43
|
+
res.header("Access-Control-Allow-Origin", "*"); // Sblocca la Dashboard locale su Mac
|
|
44
|
+
res.json(currentConfig);
|
|
45
|
+
});
|
|
46
|
+
app.get('/rotevista-history', (req, res) => {
|
|
47
|
+
res.header("Access-Control-Allow-Origin", "*"); // Sblocca la Dashboard locale su Mac
|
|
48
|
+
res.json(histories);
|
|
49
|
+
});
|
|
50
|
+
routeRegistered = true;
|
|
51
|
+
app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
|
|
52
|
+
}
|
|
51
53
|
|
|
52
54
|
// 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
|
|
53
55
|
const localSubscription = {
|