@sailingrotevista/rotevista-dash 5.0.7 → 6.0.1
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 +28 -212
- package/index.js +214 -17
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -616,14 +616,11 @@ 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);
|
|
620
619
|
}
|
|
621
620
|
|
|
622
621
|
// --- LOGICA SOG / VMG ---
|
|
623
622
|
if (store.raw["navigation.speedOverGround"] !== undefined) {
|
|
624
623
|
const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
|
|
625
|
-
manageHistory('vmg', vmgVal);
|
|
626
|
-
manageHistory('sog', sogKts);
|
|
627
624
|
|
|
628
625
|
const labelSogVmg = document.getElementById('sog-vmg-label');
|
|
629
626
|
if (displayModeSog === 'VMG') {
|
|
@@ -652,16 +649,12 @@ function startDisplayLoop() {
|
|
|
652
649
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
653
650
|
ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
|
|
654
651
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
655
|
-
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
656
652
|
}
|
|
657
653
|
|
|
658
654
|
// --- GESTIONE VENTO (TWS / AWS SWITCH) ---
|
|
659
655
|
const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
|
|
660
656
|
const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
|
|
661
657
|
|
|
662
|
-
if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
|
|
663
|
-
if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
|
|
664
|
-
|
|
665
658
|
if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
|
|
666
659
|
const labelWind = document.getElementById('tws-aws-label');
|
|
667
660
|
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
@@ -801,7 +794,7 @@ async function fetchServerConfig() {
|
|
|
801
794
|
applyConfigData(data);
|
|
802
795
|
console.log("✅ Configurazione iniziale applicata con successo.");
|
|
803
796
|
} catch (err) {
|
|
804
|
-
console.warn("⚠️ Impossibile raggiungere
|
|
797
|
+
console.warn("⚠️ Impossibile raggiungere le configurazioni dal server. Utilizzo default.");
|
|
805
798
|
}
|
|
806
799
|
}
|
|
807
800
|
|
|
@@ -837,215 +830,26 @@ async function watchConfigChanges() {
|
|
|
837
830
|
}
|
|
838
831
|
|
|
839
832
|
/**
|
|
840
|
-
*
|
|
841
|
-
* Integrazioni:
|
|
842
|
-
* 1. Strict undefined check per lastUpdates.
|
|
843
|
-
* 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
|
|
844
|
-
* 3. Clamping di sicurezza (no negativi, no Infinity).
|
|
833
|
+
* Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
|
|
845
834
|
*/
|
|
846
|
-
function
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
|
|
852
|
-
const samples = Math.max(2, CONFIG.graphs.samples || 60);
|
|
853
|
-
const bucketIntervalMs = (historyMinutes * 60000) / samples;
|
|
854
|
-
|
|
855
|
-
// --- 2. INIT SICURO (Strict Check) ---
|
|
856
|
-
if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
|
|
857
|
-
if (!store.histories[type]) store.histories[type] = [];
|
|
858
|
-
if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
|
|
859
|
-
|
|
860
|
-
const tempBuf = store.graphTempBuf[type];
|
|
861
|
-
|
|
862
|
-
// --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
|
|
863
|
-
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
864
|
-
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
865
|
-
const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
|
|
866
|
-
if (lastPoint && lastPoint.val > glitchThreshold) return;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// --- 4. STORAGE TEMPORANEO ---
|
|
870
|
-
tempBuf.push({ val: value, time: now });
|
|
871
|
-
|
|
872
|
-
// Controllo finestra temporale
|
|
873
|
-
const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
|
|
874
|
-
if (!bucketReady) return;
|
|
875
|
-
|
|
876
|
-
// --- 5. AGGREGAZIONE SEMANTICA ---
|
|
877
|
-
let finalValue = value;
|
|
878
|
-
|
|
879
|
-
if (tempBuf.length > 0) {
|
|
880
|
-
// A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
|
|
881
|
-
if (type === 'tws' || type === 'aws') {
|
|
882
|
-
const tauMs = 2500;
|
|
883
|
-
let ema = tempBuf[0].val;
|
|
884
|
-
let maxSustained = ema;
|
|
885
|
-
|
|
886
|
-
for (let i = 1; i < tempBuf.length; i++) {
|
|
887
|
-
const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
|
|
888
|
-
const alpha = 1 - Math.exp(-dt / tauMs);
|
|
889
|
-
ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
|
|
890
|
-
if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
|
|
891
|
-
}
|
|
892
|
-
finalValue = maxSustained;
|
|
893
|
-
}
|
|
894
|
-
// B. PROFONDITÀ -> MINIMO
|
|
895
|
-
else if (type === 'depth') {
|
|
896
|
-
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
897
|
-
if (vals.length > 0) finalValue = Math.min(...vals);
|
|
898
|
-
}
|
|
899
|
-
// C. VELOCITÀ -> MEDIA
|
|
900
|
-
else {
|
|
901
|
-
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
902
|
-
if (vals.length > 0) {
|
|
903
|
-
const sum = vals.reduce((a, b) => a + b, 0);
|
|
904
|
-
finalValue = sum / tempBuf.length;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// --- 6. CLAMPING E VALIDAZIONE FINALE ---
|
|
910
|
-
if (!isFinite(finalValue)) return;
|
|
911
|
-
finalValue = Math.max(0, finalValue);
|
|
912
|
-
|
|
913
|
-
// --- 7. STORAGE STORICO ---
|
|
914
|
-
store.histories[type].push({ val: finalValue, time: now });
|
|
915
|
-
|
|
916
|
-
// --- 8. PRUNING DINAMICO ---
|
|
917
|
-
const maxViewportMinutes = historyMinutes * 2;
|
|
918
|
-
const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
|
|
919
|
-
|
|
920
|
-
while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
|
|
921
|
-
store.histories[type].shift();
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Reset per il prossimo bucket
|
|
925
|
-
store.graphTempBuf[type] = [];
|
|
926
|
-
store.lastUpdates[type] = now;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Gestione dinamica delle scale dei grafici (Involucro Elastico e Safety Zoom)
|
|
931
|
-
* Implementa la logica di sicurezza disaccoppiata a 2 minuti per la profondità.
|
|
932
|
-
*/
|
|
933
|
-
function calculateScale(type, data, mode) {
|
|
934
|
-
const s = CONFIG.scales[type];
|
|
935
|
-
const currentVal = data[data.length - 1];
|
|
936
|
-
|
|
937
|
-
// Fallback di emergenza se il buffer è momentaneamente vuoto
|
|
938
|
-
if (currentVal === undefined || currentVal === null) {
|
|
939
|
-
return { min: 0, max: s ? s.stdMax : 10 };
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Inizializzazione della memoria delle scale nello store se non esiste
|
|
943
|
-
if (!store.herculesScales) store.herculesScales = {};
|
|
944
|
-
if (!store.herculesScales[type]) {
|
|
945
|
-
store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
|
|
946
|
-
}
|
|
947
|
-
let currentScale = store.herculesScales[type];
|
|
948
|
-
|
|
949
|
-
// ==========================================================================
|
|
950
|
-
// SEZIONE PROFONDITÀ (REGOLA DI SICUREZZA DISACCOPPIATA A 2 MINUTI)
|
|
951
|
-
// ==========================================================================
|
|
952
|
-
if (type === 'depth') {
|
|
953
|
-
const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
|
|
954
|
-
if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
|
|
955
|
-
|
|
956
|
-
const now = Date.now();
|
|
957
|
-
const depthSafetyWindowMs = 120000; // 2 minuti di stabilizzazione fissi per la sicurezza
|
|
835
|
+
async function fetchServerHistory() {
|
|
836
|
+
try {
|
|
837
|
+
const response = await fetch('/rotevista-history');
|
|
838
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
839
|
+
const data = await response.json();
|
|
958
840
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
|
|
964
|
-
const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
|
|
965
|
-
|
|
966
|
-
// --- NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
|
|
967
|
-
if (mode !== 'hercules') {
|
|
968
|
-
// Entrata istantanea sotto lo Standard Max (sicurezza immediata)
|
|
969
|
-
if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
|
|
970
|
-
store.depthProtectedActive = true;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// Uscita ritardata: usciamo solo se il minimo degli ultimi 2 minuti è sopra soglia
|
|
974
|
-
if (store.depthProtectedActive) {
|
|
975
|
-
if (localMin > shallowThreshold) {
|
|
976
|
-
store.depthProtectedActive = false;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
if (store.depthProtectedActive) {
|
|
981
|
-
return { min: 0, max: shallowThreshold }; // Blocco a [0 - 20m]
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Se siamo fuori, scala dinamica normale basata sull'intero buffer passato
|
|
985
|
-
const maxHistorico = Math.max(...data);
|
|
986
|
-
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// --- HERCULES PROFONDITÀ (0 IN BASSO, ZOOM SUL MASSIMO DEI 2 MINUTI) ---
|
|
990
|
-
if (mode === 'hercules') {
|
|
991
|
-
const padding = 1.0; // 1 metro di margine sopra il fondo
|
|
992
|
-
|
|
993
|
-
// Calcoliamo i limiti ideali basati solo sugli ultimi 2 minuti
|
|
994
|
-
let targetMin = 0;
|
|
995
|
-
let targetMax = Math.ceil(localMax + padding);
|
|
996
|
-
|
|
997
|
-
// Impediamo una scala troppo stretta (minimo 4 metri di range totale per sicurezza)
|
|
998
|
-
const absoluteMinSpan = 4;
|
|
999
|
-
if (targetMax < absoluteMinSpan) {
|
|
1000
|
-
targetMax = absoluteMinSpan;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Regola asimmetrica di aggiornamento
|
|
1004
|
-
if (currentVal > currentScale.max) {
|
|
1005
|
-
// Se andiamo verso il fondo profondo, allarghiamo istantaneamente
|
|
1006
|
-
currentScale.max = targetMax;
|
|
1007
|
-
} else {
|
|
1008
|
-
// Stringiamo lo zoom solo se tutti i dati degli ultimi 2 minuti sono inferiori al target
|
|
1009
|
-
const allStableInTarget = recentVals.every(val => val <= targetMax);
|
|
1010
|
-
if (allStableInTarget) {
|
|
1011
|
-
currentScale.max = targetMax;
|
|
841
|
+
if (data && typeof data === 'object') {
|
|
842
|
+
for (let key in data) {
|
|
843
|
+
if (store.histories[key] !== undefined) {
|
|
844
|
+
store.histories[key] = data[key];
|
|
1012
845
|
}
|
|
1013
846
|
}
|
|
1014
|
-
|
|
1015
|
-
currentScale.min = 0;
|
|
1016
|
-
return { min: currentScale.min, max: currentScale.max };
|
|
847
|
+
console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
|
|
1017
848
|
}
|
|
849
|
+
} catch (err) {
|
|
850
|
+
console.warn("⚠️ Impossibile caricare lo storico dal server. Utilizzo dati vuoti/simulati.");
|
|
851
|
+
throw err; // Rilancia l'errore per far attivare il fallback nella funzione init()
|
|
1018
852
|
}
|
|
1019
|
-
|
|
1020
|
-
// ==========================================================================
|
|
1021
|
-
// ALTRI GRAFICI (STW, SOG, TWS): MANTENGONO IL COMPORTAMENTO ORIGINALE
|
|
1022
|
-
// ==========================================================================
|
|
1023
|
-
let aMin = Math.min(...data), aMax = Math.max(...data);
|
|
1024
|
-
|
|
1025
|
-
if (mode === 'hercules') {
|
|
1026
|
-
const padding = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
|
|
1027
|
-
|
|
1028
|
-
let min = Math.max(0, Math.floor(aMin - padding));
|
|
1029
|
-
let max = Math.ceil(aMax + padding);
|
|
1030
|
-
|
|
1031
|
-
// Garantisce lo span minimo
|
|
1032
|
-
const currentSpan = max - min;
|
|
1033
|
-
if (currentSpan < s.hercSpan) {
|
|
1034
|
-
const diff = s.hercSpan - currentSpan;
|
|
1035
|
-
min = Math.max(0, min - Math.floor(diff / 2));
|
|
1036
|
-
max = min + s.hercSpan;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// Arrotonda la griglia numerica a step prefissati
|
|
1040
|
-
const roundStep = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
|
|
1041
|
-
min = Math.floor(min / roundStep) * roundStep;
|
|
1042
|
-
max = Math.ceil(max / roundStep) * roundStep;
|
|
1043
|
-
|
|
1044
|
-
return { min, max };
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Scala Standard (Autocompressione a scatti)
|
|
1048
|
-
return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
|
|
1049
853
|
}
|
|
1050
854
|
|
|
1051
855
|
function updateScaleLabels(t, min, max) {
|
|
@@ -1120,7 +924,8 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1120
924
|
};
|
|
1121
925
|
|
|
1122
926
|
let grids = "";
|
|
1123
|
-
|
|
927
|
+
// Una sola linea di griglia centrale al 50%, matematicamente sempre intera e allineata all'etichetta
|
|
928
|
+
[0.5].forEach(p => grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`);
|
|
1124
929
|
|
|
1125
930
|
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
1126
931
|
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
@@ -1317,6 +1122,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
|
1317
1122
|
|
|
1318
1123
|
async function init() {
|
|
1319
1124
|
loadDashboardState();
|
|
1125
|
+
|
|
1126
|
+
// Prova a caricare lo storico dal server. Se fallisce (test offline), gestisce il fallback
|
|
1127
|
+
try {
|
|
1128
|
+
await fetchServerHistory();
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
if (simulationMode) {
|
|
1131
|
+
// Se sei in modalità simulazione locale, puoi generare dati finti qui (opzionale)
|
|
1132
|
+
console.log("🎮 Modalità Simulazione Locale attiva.");
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1320
1136
|
await fetchServerConfig();
|
|
1321
1137
|
startDisplayLoop();
|
|
1322
1138
|
connect();
|
package/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ==========================================================================
|
|
3
|
-
* Rotevista Dash Configuration Plugin
|
|
3
|
+
* Rotevista Dash Configuration & History Plugin (Pro v6.0)
|
|
4
4
|
* ==========================================================================
|
|
5
5
|
* Definisce l'interfaccia di configurazione in Signal K Admin e crea
|
|
6
|
-
*
|
|
6
|
+
* gli endpoint pubblici per la Dashboard, mantenendo lo storico in RAM.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
module.exports = function (app) {
|
|
@@ -14,40 +14,213 @@ module.exports = function (app) {
|
|
|
14
14
|
|
|
15
15
|
let currentConfig = {};
|
|
16
16
|
let routeRegistered = false;
|
|
17
|
+
let unsubscribes = [];
|
|
18
|
+
|
|
19
|
+
// Database dello storico in RAM sul server
|
|
20
|
+
let histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
|
|
21
|
+
let graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
|
|
22
|
+
let lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
|
|
23
|
+
let raw = {};
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* plugin.start: Inizializza il plugin.
|
|
20
27
|
* Viene chiamato all'avvio e OGNI VOLTA che clicchi "Save" nelle impostazioni.
|
|
21
28
|
*/
|
|
22
29
|
plugin.start = function (options) {
|
|
23
|
-
// 1. Aggiorna la configurazione in memoria
|
|
30
|
+
// 1. Aggiorna la configurazione in memoria
|
|
24
31
|
currentConfig = options;
|
|
25
|
-
|
|
26
|
-
// 2. Log di debug nel server Signal K
|
|
27
32
|
app.debug(`${plugin.name} started/updated with new options`);
|
|
28
33
|
|
|
29
|
-
//
|
|
34
|
+
// Reset dello storico al riavvio del plugin per evitare incoerenze
|
|
35
|
+
histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
|
|
36
|
+
graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
|
|
37
|
+
lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
|
|
38
|
+
raw = {};
|
|
39
|
+
|
|
40
|
+
// 2. Registra le rotte API solo la prima volta
|
|
30
41
|
if (!routeRegistered) {
|
|
31
42
|
app.get('/rotevista-config', (req, res) => {
|
|
32
43
|
res.json(currentConfig);
|
|
33
44
|
});
|
|
45
|
+
app.get('/rotevista-history', (req, res) => {
|
|
46
|
+
res.json(histories);
|
|
47
|
+
});
|
|
34
48
|
routeRegistered = true;
|
|
35
|
-
app.debug('Public API
|
|
49
|
+
app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
|
|
36
50
|
}
|
|
51
|
+
|
|
52
|
+
// 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
|
|
53
|
+
const localSubscription = {
|
|
54
|
+
context: 'vessels.self',
|
|
55
|
+
subscribe: [
|
|
56
|
+
{ path: 'navigation.speedThroughWater' },
|
|
57
|
+
{ path: 'navigation.speedOverGround' },
|
|
58
|
+
{ path: 'environment.depth.belowTransducer' },
|
|
59
|
+
{ path: 'environment.wind.speedApparent' },
|
|
60
|
+
{ path: 'environment.wind.angleApparent' },
|
|
61
|
+
{ path: 'navigation.headingTrue' },
|
|
62
|
+
{ path: 'navigation.courseOverGroundTrue' }
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
app.subscriptionmanager.subscribe(
|
|
67
|
+
localSubscription,
|
|
68
|
+
unsubscribes,
|
|
69
|
+
subscriptionError => {
|
|
70
|
+
app.error('Subscription error: ' + subscriptionError);
|
|
71
|
+
},
|
|
72
|
+
delta => {
|
|
73
|
+
if (delta.updates) {
|
|
74
|
+
delta.updates.forEach(update => {
|
|
75
|
+
if (update.values) {
|
|
76
|
+
update.values.forEach(v => {
|
|
77
|
+
processIncomingDelta(v.path, v.value);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
37
84
|
};
|
|
38
85
|
|
|
39
86
|
/**
|
|
40
|
-
* plugin.stop: Chiamato quando il plugin viene disattivato
|
|
87
|
+
* plugin.stop: Chiamato quando il plugin viene disattivato.
|
|
41
88
|
*/
|
|
42
89
|
plugin.stop = function () {
|
|
90
|
+
unsubscribes.forEach(f => f());
|
|
91
|
+
unsubscribes = [];
|
|
43
92
|
app.debug(`${plugin.name} stopped`);
|
|
44
93
|
};
|
|
45
94
|
|
|
46
|
-
// Se desideri avere una funzione plugin.debug personalizzata (opzionale)
|
|
47
95
|
plugin.debug = function(msg) {
|
|
48
96
|
app.debug(msg);
|
|
49
97
|
};
|
|
50
98
|
|
|
99
|
+
/**
|
|
100
|
+
* processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
|
|
101
|
+
*/
|
|
102
|
+
function processIncomingDelta(path, val) {
|
|
103
|
+
if (val === null || val === undefined) return;
|
|
104
|
+
raw[path] = val;
|
|
105
|
+
|
|
106
|
+
// Elaborazione e invio alla macchina a stati temporali dello storico
|
|
107
|
+
if (path === 'navigation.speedThroughWater') {
|
|
108
|
+
manageHistory('stw', val * 1.94384);
|
|
109
|
+
}
|
|
110
|
+
else if (path === 'navigation.speedOverGround') {
|
|
111
|
+
manageHistory('sog', val * 1.94384);
|
|
112
|
+
}
|
|
113
|
+
else if (path === 'environment.depth.belowTransducer') {
|
|
114
|
+
manageHistory('depth', val);
|
|
115
|
+
}
|
|
116
|
+
else if (path === 'environment.wind.speedApparent') {
|
|
117
|
+
manageHistory('aws', val * 1.94384);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Calcolo combinato del Vento Reale (TWS) e della VMG a livello Server
|
|
121
|
+
const aws = raw["environment.wind.speedApparent"];
|
|
122
|
+
const awa = raw["environment.wind.angleApparent"];
|
|
123
|
+
const stw = raw["navigation.speedThroughWater"] || 0;
|
|
124
|
+
const sog = raw["navigation.speedOverGround"] || 0;
|
|
125
|
+
const hdg = raw["navigation.headingTrue"] || 0;
|
|
126
|
+
const cog = raw["navigation.courseOverGroundTrue"] || 0;
|
|
127
|
+
|
|
128
|
+
if (aws !== undefined && awa !== undefined) {
|
|
129
|
+
const awsKts = aws * 1.94384;
|
|
130
|
+
const stwKts = stw * 1.94384;
|
|
131
|
+
const tw_water_x = awsKts * Math.cos(awa) - stwKts;
|
|
132
|
+
const tw_water_y = awsKts * Math.sin(awa);
|
|
133
|
+
const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
134
|
+
|
|
135
|
+
manageHistory('tws', tws);
|
|
136
|
+
|
|
137
|
+
const twa = Math.atan2(tw_water_y, tw_water_x);
|
|
138
|
+
const vmg = Math.abs(stwKts * Math.cos(twa));
|
|
139
|
+
manageHistory('vmg', vmg);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* manageHistory: Versione Server-side dell'aggregatore matematico tattico
|
|
145
|
+
*/
|
|
146
|
+
function manageHistory(type, value) {
|
|
147
|
+
if (value === undefined || value === null || !isFinite(value)) return;
|
|
148
|
+
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const historyMinutes = currentConfig.graphs ? currentConfig.graphs.historyMinutes : 5;
|
|
151
|
+
const samples = 60;
|
|
152
|
+
const bucketIntervalMs = (historyMinutes * 60000) / samples;
|
|
153
|
+
|
|
154
|
+
if (!graphTempBuf[type]) graphTempBuf[type] = [];
|
|
155
|
+
if (!histories[type]) histories[type] = [];
|
|
156
|
+
if (lastUpdates[type] === undefined) lastUpdates[type] = 0;
|
|
157
|
+
|
|
158
|
+
const tempBuf = graphTempBuf[type];
|
|
159
|
+
|
|
160
|
+
// Anti-dropout dinamico sul vento forte
|
|
161
|
+
if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
|
|
162
|
+
const lastPoint = tempBuf[tempBuf.length - 1];
|
|
163
|
+
const reef1 = currentConfig.graphs ? currentConfig.graphs.reef1 : 15;
|
|
164
|
+
const glitchThreshold = reef1 * 0.5;
|
|
165
|
+
if (lastPoint && lastPoint.val > glitchThreshold) return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
tempBuf.push({ val: value, time: now });
|
|
169
|
+
|
|
170
|
+
// Controllo avanzamento del secchiello temporale (Bucket)
|
|
171
|
+
const bucketReady = (now - lastUpdates[type] > bucketIntervalMs) || histories[type].length === 0;
|
|
172
|
+
if (!bucketReady) return;
|
|
173
|
+
|
|
174
|
+
let finalValue = value;
|
|
175
|
+
|
|
176
|
+
if (tempBuf.length > 0) {
|
|
177
|
+
// A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
|
|
178
|
+
if (type === 'tws' || type === 'aws') {
|
|
179
|
+
const tauMs = 2500;
|
|
180
|
+
let ema = tempBuf[0].val;
|
|
181
|
+
let maxSustained = ema;
|
|
182
|
+
|
|
183
|
+
for (let i = 1; i < tempBuf.length; i++) {
|
|
184
|
+
const dt = Math.max(1, tempBuf[i].time - tempBuf[i-1].time);
|
|
185
|
+
const alpha = 1 - Math.exp(-dt / tauMs);
|
|
186
|
+
ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
|
|
187
|
+
if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
|
|
188
|
+
}
|
|
189
|
+
finalValue = maxSustained;
|
|
190
|
+
}
|
|
191
|
+
// B. PROFONDITÀ -> MINIMO
|
|
192
|
+
else if (type === 'depth') {
|
|
193
|
+
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
194
|
+
if (vals.length > 0) finalValue = Math.min(...vals);
|
|
195
|
+
}
|
|
196
|
+
// C. VELOCITÀ -> MEDIA
|
|
197
|
+
else {
|
|
198
|
+
const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
|
|
199
|
+
if (vals.length > 0) {
|
|
200
|
+
const sum = vals.reduce((a, b) => a + b, 0);
|
|
201
|
+
finalValue = sum / tempBuf.length;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!isFinite(finalValue)) return;
|
|
207
|
+
finalValue = Math.max(0, finalValue);
|
|
208
|
+
|
|
209
|
+
// Salvataggio nel ring buffer dello storico principale
|
|
210
|
+
histories[type].push({ val: finalValue, time: now });
|
|
211
|
+
|
|
212
|
+
// Pruning automatico basato sulle impostazioni di timeline
|
|
213
|
+
const maxViewportMinutes = historyMinutes * 2;
|
|
214
|
+
const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
|
|
215
|
+
|
|
216
|
+
while (histories[type].length > 0 && (now - histories[type][0].time) > maxHistoryMs) {
|
|
217
|
+
histories[type].shift();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
graphTempBuf[type] = [];
|
|
221
|
+
lastUpdates[type] = now;
|
|
222
|
+
}
|
|
223
|
+
|
|
51
224
|
/**
|
|
52
225
|
* plugin.schema: Definisce l'interfaccia grafica in Signal K Admin.
|
|
53
226
|
*/
|
|
@@ -127,10 +300,10 @@ module.exports = function (app) {
|
|
|
127
300
|
default: 0.5
|
|
128
301
|
},
|
|
129
302
|
stabilityThreshold: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
303
|
+
type: 'number',
|
|
304
|
+
title: 'Steering Precision (Sensitivity)',
|
|
305
|
+
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.",
|
|
306
|
+
default: 0.95
|
|
134
307
|
},
|
|
135
308
|
stabilityBreakout: {
|
|
136
309
|
type: 'number',
|
|
@@ -152,7 +325,13 @@ module.exports = function (app) {
|
|
|
152
325
|
properties: {
|
|
153
326
|
stdMax: { type: 'number', title: 'Standard Max', description: "Default top limit of the graph.", default: 12 },
|
|
154
327
|
step: { type: 'number', title: 'Scale Jump', description: "Amount the scale increases when you exceed the limit.", default: 2 },
|
|
155
|
-
hercSpan: {
|
|
328
|
+
hercSpan: {
|
|
329
|
+
type: 'number',
|
|
330
|
+
title: 'Hercules Grid Step (Resolution)',
|
|
331
|
+
description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
|
|
332
|
+
enum: [0.5, 1.0, 2.0, 3.0],
|
|
333
|
+
default: 1.0
|
|
334
|
+
}
|
|
156
335
|
}
|
|
157
336
|
},
|
|
158
337
|
sog: {
|
|
@@ -161,7 +340,13 @@ module.exports = function (app) {
|
|
|
161
340
|
properties: {
|
|
162
341
|
stdMax: { type: 'number', title: 'Standard Max', default: 12 },
|
|
163
342
|
step: { type: 'number', title: 'Scale Jump', default: 2 },
|
|
164
|
-
hercSpan: {
|
|
343
|
+
hercSpan: {
|
|
344
|
+
type: 'number',
|
|
345
|
+
title: 'Hercules Grid Step (Resolution)',
|
|
346
|
+
description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
|
|
347
|
+
enum: [0.5, 1.0, 2.0, 3.0],
|
|
348
|
+
default: 1.0
|
|
349
|
+
}
|
|
165
350
|
}
|
|
166
351
|
},
|
|
167
352
|
tws: {
|
|
@@ -170,7 +355,13 @@ module.exports = function (app) {
|
|
|
170
355
|
properties: {
|
|
171
356
|
stdMax: { type: 'number', title: 'Standard Max', default: 25 },
|
|
172
357
|
step: { type: 'number', title: 'Scale Jump', default: 5 },
|
|
173
|
-
hercSpan: {
|
|
358
|
+
hercSpan: {
|
|
359
|
+
type: 'number',
|
|
360
|
+
title: 'Hercules Grid Step (Resolution)',
|
|
361
|
+
description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
|
|
362
|
+
enum: [1, 2, 3, 5, 10],
|
|
363
|
+
default: 2
|
|
364
|
+
}
|
|
174
365
|
}
|
|
175
366
|
},
|
|
176
367
|
depth: {
|
|
@@ -179,7 +370,13 @@ module.exports = function (app) {
|
|
|
179
370
|
properties: {
|
|
180
371
|
stdMax: { type: 'number', title: 'Standard Max', default: 20 },
|
|
181
372
|
step: { type: 'number', title: 'Scale Jump', default: 10 },
|
|
182
|
-
hercSpan: {
|
|
373
|
+
hercSpan: {
|
|
374
|
+
type: 'number',
|
|
375
|
+
title: 'Hercules Grid Step (Resolution)',
|
|
376
|
+
description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
|
|
377
|
+
enum: [1, 2, 3, 5, 10],
|
|
378
|
+
default: 2
|
|
379
|
+
}
|
|
183
380
|
}
|
|
184
381
|
}
|
|
185
382
|
}
|