@sailingrotevista/rotevista-dash 5.0.8 → 6.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.
- package/app.js +29 -221
- package/index.js +182 -9
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -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 = "
|
|
38
|
+
const DASH_VERSION = "6.0"; // Major Update: Smart Source Locking & Breathing Hercules Scale
|
|
39
39
|
|
|
40
40
|
// ==========================================================================
|
|
41
41
|
// 2. STATO GLOBALE E RIFERIMENTI UI
|
|
@@ -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,222 +830,25 @@ 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).
|
|
845
|
-
*/
|
|
846
|
-
function manageHistory(type, value) {
|
|
847
|
-
// --- 1. VALIDAZIONE INPUT RIGOROSA ---
|
|
848
|
-
if (value === undefined || value === null || !isFinite(value)) return;
|
|
849
|
-
|
|
850
|
-
const now = Date.now();
|
|
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 Snapped e Safety Zoom)
|
|
931
|
-
* Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
|
|
833
|
+
* Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
|
|
932
834
|
*/
|
|
933
|
-
function
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
// 1. SEZIONE PROFONDITÀ (DEDICATA A 2 MINUTI DI SICUREZZA, MINIMO FISSO A 0)
|
|
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 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
|
-
// --- 1.1 NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
|
|
967
|
-
if (mode !== 'hercules') {
|
|
968
|
-
if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
|
|
969
|
-
store.depthProtectedActive = true;
|
|
970
|
-
}
|
|
971
|
-
if (store.depthProtectedActive) {
|
|
972
|
-
if (localMin > shallowThreshold) {
|
|
973
|
-
store.depthProtectedActive = false;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (store.depthProtectedActive) {
|
|
977
|
-
return { min: 0, max: shallowThreshold };
|
|
978
|
-
}
|
|
979
|
-
const maxHistorico = Math.max(...data);
|
|
980
|
-
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// --- 1.2 HERCULES PROFONDITÀ (0 IN BASSO, SNAP SUL MAX SENZA PADDING) ---
|
|
984
|
-
if (mode === 'hercules') {
|
|
985
|
-
const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
|
|
986
|
-
|
|
987
|
-
let targetMin = 0;
|
|
988
|
-
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
989
|
-
|
|
990
|
-
// Impediamo una scala inferiore a 4 metri per sicurezza visiva
|
|
991
|
-
const absoluteMinSpan = 4;
|
|
992
|
-
if (targetMax < absoluteMinSpan) {
|
|
993
|
-
targetMax = absoluteMinSpan;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Regola asimmetrica per il MAX (Espansione istantanea, contrazione a 2 minuti)
|
|
997
|
-
if (currentVal > currentScale.max) {
|
|
998
|
-
currentScale.max = targetMax;
|
|
999
|
-
} else {
|
|
1000
|
-
const allStableInTarget = recentVals.every(val => val <= targetMax);
|
|
1001
|
-
if (allStableInTarget) {
|
|
1002
|
-
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];
|
|
1003
845
|
}
|
|
1004
846
|
}
|
|
1005
|
-
|
|
1006
|
-
currentScale.min = 0;
|
|
1007
|
-
return { min: currentScale.min, max: currentScale.max };
|
|
847
|
+
console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
|
|
1008
848
|
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
// 2. ALTRI GRAFICI (STW, SOG, TWS): COMPORTAMENTO ADATTIVO SU TERZO DEL GRAFICO
|
|
1013
|
-
// ==========================================================================
|
|
1014
|
-
|
|
1015
|
-
// --- 2.1 MODALITÀ NORMALE ---
|
|
1016
|
-
if (mode !== 'hercules') {
|
|
1017
|
-
const maxHistorico = Math.max(...data);
|
|
1018
|
-
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// --- 2.2 MODALITÀ HERCULES AD ALTO CONTRASTO (SNAP SENZA PADDING) ---
|
|
1022
|
-
if (mode === 'hercules') {
|
|
1023
|
-
const roundStep = s.hercSpan; // Passo di griglia (ex hercSpan)
|
|
1024
|
-
|
|
1025
|
-
const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
|
|
1026
|
-
const recentThirdData = data.slice(-oneThirdCount);
|
|
1027
|
-
|
|
1028
|
-
const localMin = Math.min(...recentThirdData);
|
|
1029
|
-
const localMax = Math.max(...recentThirdData);
|
|
1030
|
-
|
|
1031
|
-
// Applichiamo lo snap diretto ai multipli della griglia
|
|
1032
|
-
let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
1033
|
-
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
1034
|
-
|
|
1035
|
-
// Se lo span calcolato è nullo (es. velocità costante), forziamo lo span minimo
|
|
1036
|
-
if (targetMax - targetMin === 0) {
|
|
1037
|
-
targetMax = targetMin + roundStep;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// APPLICAZIONE REGOLE ASIMMETRICHE ANTI-CLIPPING
|
|
1041
|
-
// A. Espansione ISTANTANEA in alto o in basso (sicurezza)
|
|
1042
|
-
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
1043
|
-
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
1044
|
-
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
1045
|
-
}
|
|
1046
|
-
// B. Contrazione RITARDATA (solo se tutto il terzo recente si è assestato nel target)
|
|
1047
|
-
else {
|
|
1048
|
-
const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
|
|
1049
|
-
if (allStableInTarget) {
|
|
1050
|
-
currentScale.min = targetMin;
|
|
1051
|
-
currentScale.max = targetMax;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
return { min: currentScale.min, max: currentScale.max };
|
|
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()
|
|
1056
852
|
}
|
|
1057
853
|
}
|
|
1058
854
|
|
|
@@ -1128,7 +924,8 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1128
924
|
};
|
|
1129
925
|
|
|
1130
926
|
let grids = "";
|
|
1131
|
-
|
|
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" />`);
|
|
1132
929
|
|
|
1133
930
|
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
1134
931
|
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
@@ -1325,6 +1122,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
|
|
|
1325
1122
|
|
|
1326
1123
|
async function init() {
|
|
1327
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
|
+
|
|
1328
1136
|
await fetchServerConfig();
|
|
1329
1137
|
startDisplayLoop();
|
|
1330
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
|
*/
|