@sailingrotevista/rotevista-dash 6.0.2 → 6.0.4
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 +253 -13
- package/index.html +1 -1
- 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,226 @@ 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
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
|
|
964
|
+
* Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
|
|
965
|
+
*/
|
|
966
|
+
function calculateScale(type, data, mode) {
|
|
967
|
+
const s = CONFIG.scales[type];
|
|
968
|
+
const currentVal = data[data.length - 1];
|
|
969
|
+
|
|
970
|
+
// Fallback di emergenza se il buffer è momentaneamente vuoto
|
|
971
|
+
if (currentVal === undefined || currentVal === null) {
|
|
972
|
+
return { min: 0, max: s ? s.stdMax : 10 };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Inizializzazione della memoria delle scale nello store se non esiste
|
|
976
|
+
if (!store.herculesScales) store.herculesScales = {};
|
|
977
|
+
if (!store.herculesScales[type]) {
|
|
978
|
+
store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
|
|
979
|
+
}
|
|
980
|
+
let currentScale = store.herculesScales[type];
|
|
981
|
+
|
|
982
|
+
// ==========================================================================
|
|
983
|
+
// 1. SEZIONE PROFONDITÀ (DEDICATA A 2 MINUTI DI SICUREZZA, MINIMO FISSO A 0)
|
|
984
|
+
// ==========================================================================
|
|
985
|
+
if (type === 'depth') {
|
|
986
|
+
const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
|
|
987
|
+
if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
|
|
988
|
+
|
|
989
|
+
const now = Date.now();
|
|
990
|
+
const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
|
|
991
|
+
|
|
992
|
+
// Estrazione dati reali degli ultimi 2 minuti con timestamp
|
|
993
|
+
const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
|
|
994
|
+
const recentVals = recentPoints.map(p => p.val);
|
|
995
|
+
|
|
996
|
+
const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
|
|
997
|
+
const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
|
|
998
|
+
|
|
999
|
+
// --- 1.1 NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
|
|
1000
|
+
if (mode !== 'hercules') {
|
|
1001
|
+
if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
|
|
1002
|
+
store.depthProtectedActive = true;
|
|
1003
|
+
}
|
|
1004
|
+
if (store.depthProtectedActive) {
|
|
1005
|
+
if (localMin > shallowThreshold) {
|
|
1006
|
+
store.depthProtectedActive = false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (store.depthProtectedActive) {
|
|
1010
|
+
return { min: 0, max: shallowThreshold };
|
|
1011
|
+
}
|
|
1012
|
+
const maxHistorico = Math.max(...data);
|
|
1013
|
+
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// --- 1.2 HERCULES PROFONDITÀ (0 IN BASSO, SNAP SUL MAX SENZA PADDING) ---
|
|
1017
|
+
if (mode === 'hercules') {
|
|
1018
|
+
const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
|
|
1019
|
+
|
|
1020
|
+
let targetMin = 0;
|
|
1021
|
+
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
1022
|
+
|
|
1023
|
+
// Impediamo una scala inferiore a 4 metri per sicurezza visiva
|
|
1024
|
+
const absoluteMinSpan = 4;
|
|
1025
|
+
if (targetMax < absoluteMinSpan) {
|
|
1026
|
+
targetMax = absoluteMinSpan;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Regola asimmetrica per il MAX (Espansione istantanea, contrazione a 2 minuti)
|
|
1030
|
+
if (currentVal > currentScale.max) {
|
|
1031
|
+
currentScale.max = targetMax;
|
|
1032
|
+
} else {
|
|
1033
|
+
const allStableInTarget = recentVals.every(val => val <= targetMax);
|
|
1034
|
+
if (allStableInTarget) {
|
|
1035
|
+
currentScale.max = targetMax;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
currentScale.min = 0;
|
|
1040
|
+
return { min: currentScale.min, max: currentScale.max };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ==========================================================================
|
|
1045
|
+
// 2. ALTRI GRAFICI (STW, SOG, TWS): COMPORTAMENTO ADATTIVO SU TERZO DEL GRAFICO
|
|
1046
|
+
// ==========================================================================
|
|
1047
|
+
|
|
1048
|
+
// --- 2.1 MODALITÀ NORMALE ---
|
|
1049
|
+
if (mode !== 'hercules') {
|
|
1050
|
+
const maxHistorico = Math.max(...data);
|
|
1051
|
+
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// --- 2.2 MODALITÀ HERCULES AD ALTO CONTRASTO (SNAP SENZA PADDING) ---
|
|
1055
|
+
if (mode === 'hercules') {
|
|
1056
|
+
const roundStep = s.hercSpan; // Passo di griglia (ex hercSpan)
|
|
1057
|
+
|
|
1058
|
+
const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
|
|
1059
|
+
const recentThirdData = data.slice(-oneThirdCount);
|
|
1060
|
+
|
|
1061
|
+
const localMin = Math.min(...recentThirdData);
|
|
1062
|
+
const localMax = Math.max(...recentThirdData);
|
|
1063
|
+
|
|
1064
|
+
// Applichiamo lo snap diretto ai multipli della griglia
|
|
1065
|
+
let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
1066
|
+
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
1067
|
+
|
|
1068
|
+
// Se lo span calcolato è nullo (es. velocità costante), forziamo lo span minimo
|
|
1069
|
+
if (targetMax - targetMin === 0) {
|
|
1070
|
+
targetMax = targetMin + roundStep;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// APPLICAZIONE REGOLE ASIMMETRICHE ANTI-CLIPPING
|
|
1074
|
+
// A. Espansione ISTANTANEA in alto o in basso (sicurezza)
|
|
1075
|
+
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
1076
|
+
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
1077
|
+
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
1078
|
+
}
|
|
1079
|
+
// B. Contrazione RITARDATA (solo se tutto il terzo recente si è assestato nel target)
|
|
1080
|
+
else {
|
|
1081
|
+
const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
|
|
1082
|
+
if (allStableInTarget) {
|
|
1083
|
+
currentScale.min = targetMin;
|
|
1084
|
+
currentScale.max = targetMax;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return { min: currentScale.min, max: currentScale.max };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
855
1092
|
function updateScaleLabels(t, min, max) {
|
|
856
1093
|
const el = document.getElementById(t + '-scale');
|
|
857
1094
|
if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
|
|
@@ -885,6 +1122,7 @@ function refreshGraph(t) {
|
|
|
885
1122
|
|
|
886
1123
|
/**
|
|
887
1124
|
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
1125
|
+
* Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
|
|
888
1126
|
*/
|
|
889
1127
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
890
1128
|
const svg = document.getElementById(id);
|
|
@@ -893,7 +1131,11 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
893
1131
|
const w = 200, h = 40;
|
|
894
1132
|
const range = max - min || 1;
|
|
895
1133
|
const isDepth = (id === 'depth-graph');
|
|
896
|
-
|
|
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();
|
|
897
1139
|
|
|
898
1140
|
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
899
1141
|
const viewportMs = visibleMinutes * 60000;
|
|
@@ -1123,19 +1365,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
|
1123
1365
|
async function init() {
|
|
1124
1366
|
loadDashboardState();
|
|
1125
1367
|
|
|
1126
|
-
// Prova a caricare lo storico dal
|
|
1368
|
+
// Prova a caricare lo storico reale dal Cerbo GX tramite l'API deviata
|
|
1127
1369
|
try {
|
|
1128
1370
|
await fetchServerHistory();
|
|
1129
1371
|
} catch (err) {
|
|
1130
|
-
|
|
1131
|
-
// Se sei in modalità simulazione locale, puoi generare dati finti qui (opzionale)
|
|
1132
|
-
console.log("🎮 Modalità Simulazione Locale attiva.");
|
|
1133
|
-
}
|
|
1372
|
+
console.warn("⚠️ Impossibile caricare lo storico dal server.");
|
|
1134
1373
|
}
|
|
1135
1374
|
|
|
1136
1375
|
await fetchServerConfig();
|
|
1137
1376
|
startDisplayLoop();
|
|
1138
|
-
connect();
|
|
1377
|
+
connect(); // Si collegherà in tempo reale al WebSocket del Cerbo (usando l'IP di fallback se sei su Mac)
|
|
1378
|
+
|
|
1139
1379
|
setInterval(watchConfigChanges, 10000);
|
|
1140
1380
|
}
|
|
1141
1381
|
|
package/index.html
CHANGED
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 = {
|