@sailingrotevista/rotevista-dash 1.0.19 → 1.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.
Files changed (3) hide show
  1. package/app.js +74 -59
  2. package/index.js +54 -23
  3. package/package.json +1 -1
package/app.js CHANGED
@@ -1,33 +1,17 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE DI DEFAULT (Sovrascritta dal Server)
2
+ // 1. CONFIGURAZIONE E COSTANTI
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
- alarms: {
6
- depthDanger: 2.5,
7
- depthWarning: 5.0
8
- },
9
- averages: {
10
- smoothWindow: 2000,
11
- longWindow: 60000,
12
- stabilityTolerance: 2000,
13
- stabilityThreshold: 0.90,
14
- minSpeed: 0.5
15
- },
16
- graphs: {
17
- reef1: 15.0, // Soglia Arancio
18
- reef2: 20.0, // Soglia Rossa
19
- realInterval: 5000,
20
- samples: 60
21
- },
5
+ alarms: { depthDanger: 2.5, depthWarning: 5.0 },
6
+ averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
7
+ graphs: { reef1: 15.0, reef2: 20.0, realInterval: 5000, samples: 60 },
22
8
  scales: {
23
- stw: { stdMax: 12, hercSpan: 4, step: 2 },
24
- sog: { stdMax: 12, hercSpan: 4, step: 2 },
25
- tws: { stdMax: 25, hercSpan: 10, step: 5 },
9
+ stw: { stdMax: 12, hercSpan: 4, step: 2 },
10
+ sog: { stdMax: 12, hercSpan: 4, step: 2 },
11
+ tws: { stdMax: 25, hercSpan: 10, step: 5 },
26
12
  depth: { stdMax: 20, hercSpan: 10, step: 10 }
27
13
  },
28
- server: {
29
- fallbackIp: "192.168.111.240:3000"
30
- }
14
+ server: { fallbackIp: "192.168.111.240:3000" }
31
15
  };
32
16
 
33
17
  const RENDER_INTERVAL_MS = 1000;
@@ -50,7 +34,8 @@ const graphModes = {
50
34
  };
51
35
 
52
36
  const store = {
53
- raw: {}, timestamps: {},
37
+ raw: {},
38
+ timestamps: {},
54
39
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
55
40
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
56
41
  histories: { stw: [], sog: [], depth: [], tws: [] },
@@ -67,42 +52,54 @@ const ui = {
67
52
  twdAvg: document.getElementById('twd-avg'), twdArrow: document.getElementById('twd-arrow'),
68
53
  leewayMask: document.getElementById('leeway-mask-rect'), leewayVal: document.getElementById('leeway-val'),
69
54
  tackHdg: document.getElementById('tack-hdg'), tackCog: document.getElementById('tack-cog'),
70
- status: document.getElementById('status'),
71
- hotspot: document.getElementById('fullscreen-hotspot')
55
+ status: document.getElementById('status'), hotspot: document.getElementById('fullscreen-hotspot')
72
56
  };
73
57
 
74
58
  // ==========================================================================
75
- // 3. COMUNICAZIONE CON IL SERVER (FETCH CONFIG)
59
+ // 3. CARICAMENTO CONFIGURAZIONE (SK-SERVER CONFIG)
76
60
  // ==========================================================================
77
61
  async function fetchServerConfig() {
78
62
  if (!window.location.protocol.includes("http")) return;
79
63
  const pluginID = 'rotevista-dash';
80
- const possibleUrls = [
81
- `/skServer/plugins/${pluginID}/config`,
82
- `/plugins/${pluginID}/config`
83
- ];
64
+ const possibleUrls = [`/skServer/plugins/${pluginID}/config`, `/plugins/${pluginID}/config` ];
84
65
 
85
66
  for (let url of possibleUrls) {
86
67
  try {
87
68
  const response = await fetch(url);
88
69
  if (response.ok) {
89
70
  const data = await response.json();
90
- const actualConfig = data.configuration || data;
91
- if (actualConfig && typeof actualConfig === 'object') {
92
- if (actualConfig.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actualConfig.alarms };
93
- if (actualConfig.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actualConfig.graphs };
94
- if (actualConfig.averages) CONFIG.averages = { ...CONFIG.averages, ...actualConfig.averages };
95
- console.log("SUCCESS: Configurazione caricata da " + url);
71
+ const actual = data.configuration || data;
72
+ if (actual && typeof actual === 'object') {
73
+
74
+ // Funzione per pulire e convertire i numeri
75
+ const parseNumbers = (obj) => {
76
+ for (let k in obj) {
77
+ if (typeof obj[k] === 'object') parseNumbers(obj[k]);
78
+ else if (!isNaN(obj[k]) && obj[k] !== "") obj[k] = parseFloat(obj[k]);
79
+ }
80
+ };
81
+ parseNumbers(actual);
82
+
83
+ // Merge intelligente dei blocchi
84
+ if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
85
+ if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
86
+ if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging }; // Nota: mappiamo averaging -> averages
87
+ if (actual.scales) {
88
+ for (let key in actual.scales) {
89
+ CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] };
90
+ }
91
+ }
92
+
93
+ console.log("SUCCESS: Dashboard Config updated from Server:", CONFIG);
96
94
  return;
97
95
  }
98
96
  }
99
97
  } catch (e) { }
100
98
  }
101
- console.log("Info: Uso configurazione di default.");
102
99
  }
103
100
 
104
101
  // ==========================================================================
105
- // 4. MATEMATICA E GESTIONE DATI
102
+ // 4. MATEMATICA VETTORIALE (CIRCULAR AVERAGING)
106
103
  // ==========================================================================
107
104
  function radToDeg(rad) { return rad * (180 / Math.PI); }
108
105
  function degToRad(deg) { return deg * (Math.PI / 180); }
@@ -110,6 +107,7 @@ function msToKts(ms) { return ms * 1.94384; }
110
107
  function ktsToMs(kts) { return kts / 1.94384; }
111
108
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
112
109
 
110
+ // Calcolo media basato su componenti Seno/Coseno (Vettoriale)
113
111
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
114
112
  const now = Date.now();
115
113
  const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
@@ -123,17 +121,32 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
123
121
  return { val: signed ? avgDeg : (avgDeg + 360) % 360, stable: isStable };
124
122
  }
125
123
 
124
+ // ==========================================================================
125
+ // 5. GESTIONE DATI IN INGRESSO
126
+ // ==========================================================================
126
127
  function processIncomingData(path, val) {
127
128
  const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
128
129
  if (path === "navigation.headingTrue") { store.smoothBuf.hdg.push({ val: val, time: now }); store.longBuf.hdg.push({ val: val, time: now }); }
129
130
  if (path === "navigation.courseOverGroundTrue") { store.smoothBuf.cog.push({ val: val, time: now }); store.longBuf.cog.push({ val: val, time: now }); }
130
131
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
131
132
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
132
- if (path === "environment.wind.directionTrue") { store.smoothBuf.twd.push({ val: val, time: now }); store.longBuf.twd.push({ val: val, time: now }); }
133
+
134
+ // Calcolo o ricezione TWD (True Wind Direction)
135
+ if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
136
+ let twdRad = 0;
137
+ if (path === "environment.wind.directionTrue") twdRad = val;
138
+ else if (store.raw["navigation.headingTrue"] !== undefined && store.raw["environment.wind.angleTrueWater"] !== undefined) {
139
+ twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
140
+ if (twdRad < 0) twdRad += (2 * Math.PI);
141
+ } else return;
142
+
143
+ store.smoothBuf.twd.push({ val: twdRad, time: now });
144
+ store.longBuf.twd.push({ val: twdRad, time: now });
145
+ }
133
146
  }
134
147
 
135
148
  // ==========================================================================
136
- // 5. RENDERING ENGINE
149
+ // 6. MOTORE DI RENDERING
137
150
  // ==========================================================================
138
151
  function startDisplayLoop() {
139
152
  renderInterval = setInterval(() => {
@@ -190,7 +203,7 @@ function startDisplayLoop() {
190
203
  }
191
204
 
192
205
  // ==========================================================================
193
- // 6. CONNESSIONE SIGNALK (subscribe=self)
206
+ // 7. CONNESSIONE SIGNALK (subscribe=self)
194
207
  // ==========================================================================
195
208
  function connect() {
196
209
  if (simulationMode) return;
@@ -198,24 +211,19 @@ function connect() {
198
211
  try {
199
212
  socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
200
213
  socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; };
201
- socket.onmessage = (e) => {
202
- const d = JSON.parse(e.data);
203
- if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value)));
204
- };
214
+ socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
205
215
  socket.onclose = () => !simulationMode && setTimeout(connect, 5000);
206
216
  } catch (e) { setTimeout(connect, 5000); }
207
217
  }
208
218
 
209
219
  // ==========================================================================
210
- // 7. FUNZIONI GRAFICHE (Hercules Scalable)
220
+ // 8. FUNZIONI GRAFICHE (Hercules Scalable)
211
221
  // ==========================================================================
212
222
  function updateLeewayDisplay(deg) { const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125); ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w); ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`; }
213
223
 
214
224
  function manageHistory(t, v) {
215
225
  const n = Date.now(), i = simulationMode ? SIM_SAMPLE_INTERVAL : CONFIG.graphs.realInterval;
216
- if (n - store.lastUpdates[t] > i || store.histories[t].length === 0) {
217
- store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n;
218
- }
226
+ if (n - store.lastUpdates[t] > i || store.histories[t].length === 0) { store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n; }
219
227
  const mode = graphModes[t];
220
228
  const cfg = calculateScale(t, store.histories[t], mode);
221
229
  const box = document.getElementById(t + '-graph').closest('.data-box');
@@ -245,8 +253,9 @@ function updateScaleLabels(t, min, max) {
245
253
 
246
254
  function drawGraph(d, id, min, max, isTws, isHercules) {
247
255
  const svg = document.getElementById(id); if (!svg || d.length < 2) return;
248
- const w = 200, h = 40, range = max - min;
256
+ const w = 200, h = 40, range = max - min || 1;
249
257
  let gH = ""; [0.25, 0.5, 0.75].forEach(p => { gH += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
258
+
250
259
  let pD = "", cS = "";
251
260
  d.forEach((v, i) => {
252
261
  const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h); pD += `${i===0?'M':'L'} ${x} ${y} `;
@@ -256,18 +265,25 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
256
265
  cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" stroke-width="${isHercules?3:2}" class="${isHercules?'line-hercules':''}" />`;
257
266
  }
258
267
  });
268
+
269
+ const areaPath = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
259
270
  const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
260
- svg.innerHTML = isTws ? `${gH}<path d="${pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`}" fill="rgba(241,196,15,0.1)" />${cS}` : `${gH}<path d="${pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" stroke-width="1.5" />`;
271
+ const lClass = isHercules ? 'line-hercules' : '';
272
+
273
+ if (isTws) {
274
+ svg.innerHTML = `${gH}<path d="${areaPath}" fill="rgba(241,196,15,0.1)" stroke="none" />${cS}`;
275
+ } else {
276
+ svg.innerHTML = `${gH}<path d="${areaPath}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${lClass}" fill="none" stroke="${clrs[id]}" stroke-width="1.5" />`;
277
+ }
261
278
  }
262
279
 
263
280
  // ==========================================================================
264
- // 8. EVENTI E INIZIALIZZAZIONE
281
+ // 9. EVENTI E INIZIALIZZAZIONE
265
282
  // ==========================================================================
266
283
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
267
284
  const el = document.getElementById(type + '-graph').closest('.data-box');
268
285
  el.addEventListener('dblclick', (e) => {
269
- e.preventDefault();
270
- graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
286
+ e.preventDefault(); graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
271
287
  localStorage.setItem('mode_' + type, graphModes[type]);
272
288
  el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
273
289
  });
@@ -286,7 +302,7 @@ if (ui.hotspot) {
286
302
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
287
303
  window.addEventListener('load', init);
288
304
 
289
- function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
305
+ function checkDepthAlarm(d) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (d < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (d < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
290
306
  function playBingBing() { if (!audioCtx) return; const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
291
307
 
292
308
  // Simulatore (Triple click su Depth)
@@ -294,7 +310,6 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
294
310
  let dC = 0, lC = 0;
295
311
  return function() {
296
312
  const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n;
297
- if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); ui.status.innerText = "SIM ATTIVO"; simInterval = setInterval(() => { const h = Math.random()*360; processIncomingData("navigation.headingTrue", degToRad(h)); processIncomingData("navigation.speedOverGround", ktsToMs(8)); }, 200); } else location.reload(); dC = 0; }
313
+ if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); ui.status.innerText = "SIM ATTIVO"; simInterval = setInterval(() => { processIncomingData("navigation.headingTrue", degToRad(Math.random()*360)); processIncomingData("navigation.speedOverGround", ktsToMs(8)); }, 200); } else location.reload(); dC = 0; }
298
314
  };
299
315
  })());
300
- ---Fine Codice ---
package/index.js CHANGED
@@ -1,45 +1,76 @@
1
1
  module.exports = function (app) {
2
2
  const plugin = {};
3
+ plugin.id = 'rotevista-dash';
4
+ plugin.name = 'Rotevista Dash Configuration';
3
5
 
4
- plugin.id = 'rotevista-dash'; // ID univoco per il plugin
5
- plugin.name = 'Rotevista Dash Config';
6
- plugin.description = 'Impostazioni centralizzate per la Dashboard Rotevista';
6
+ plugin.start = function (options) { };
7
+ plugin.stop = function () { };
7
8
 
8
- plugin.start = function (options, restartServer) {
9
- app.debug('Plugin Rotevista Dash avviato con opzioni:', options);
10
- };
11
-
12
- plugin.stop = function () {
13
- app.debug('Plugin Rotevista Dash fermato');
14
- };
15
-
16
- // Schema conforme alla doc di SignalK per generare la UI
17
9
  plugin.schema = {
18
10
  type: 'object',
19
- title: 'Configurazione Barca',
11
+ title: 'Rotevista Dashboard Settings',
20
12
  properties: {
21
13
  alarms: {
22
14
  type: 'object',
23
- title: 'Allarmi Profondità (metri)',
15
+ title: 'Depth Alarms (meters)',
24
16
  properties: {
25
- depthDanger: { type: 'number', title: 'Pericolo (Rosso)', default: 2.5 },
26
- depthWarning: { type: 'number', title: 'Pre-allarme (Giallo)', default: 5.0 }
17
+ depthDanger: { type: 'number', title: 'Danger Threshold (Red + Sound)', default: 2.5 },
18
+ depthWarning: { type: 'number', title: 'Warning Threshold (Yellow)', default: 5.0 }
27
19
  }
28
20
  },
29
21
  graphs: {
30
22
  type: 'object',
31
- title: 'Soglie Vento (Nodi TWS)',
23
+ title: 'Wind Reef Alerts (TWS Knots)',
24
+ properties: {
25
+ reef1: { type: 'number', title: '1st Reef Threshold (Orange Line)', default: 15.0 },
26
+ reef2: { type: 'number', title: '2nd Reef Threshold (Red Line)', default: 20.0 }
27
+ }
28
+ },
29
+ averaging: {
30
+ type: 'object',
31
+ title: 'Averaging & Stability',
32
32
  properties: {
33
- reef1: { type: 'number', title: 'Soglia Arancio (1° Mano)', default: 15.0 },
34
- reef2: { type: 'number', title: 'Soglia Rossa (2° Mano)', default: 20.0 }
33
+ longWindow: { type: 'number', title: 'Long Average Window (ms)', default: 60000 },
34
+ smoothWindow: { type: 'number', title: 'Pointer Smoothing Window (ms)', default: 2000 },
35
+ minSpeed: { type: 'number', title: 'Min Speed for Stability (knots)', default: 0.5 }
35
36
  }
36
37
  },
37
- averages: {
38
+ scales: {
38
39
  type: 'object',
39
- title: 'Parametri Medie',
40
+ title: 'Graph Scale Configurations',
40
41
  properties: {
41
- longWindow: { type: 'number', title: 'Finestra Medie LUNGHE (ms)', default: 60000 },
42
- minSpeed: { type: 'number', title: 'Velocità Minima Stabilità (kts)', default: 0.5 }
42
+ stw: {
43
+ type: 'object', title: 'STW (Speed Through Water)',
44
+ properties: {
45
+ stdMax: { type: 'number', title: 'Standard Mode Max Value', default: 12 },
46
+ hercSpan: { type: 'number', title: 'Hercules Mode Zoom Span', default: 4 },
47
+ step: { type: 'number', title: 'Rounding Step', default: 2 }
48
+ }
49
+ },
50
+ sog: {
51
+ type: 'object', title: 'SOG (Speed Over Ground)',
52
+ properties: {
53
+ stdMax: { type: 'number', title: 'Standard Mode Max Value', default: 12 },
54
+ hercSpan: { type: 'number', title: 'Hercules Mode Zoom Span', default: 4 },
55
+ step: { type: 'number', title: 'Rounding Step', default: 2 }
56
+ }
57
+ },
58
+ tws: {
59
+ type: 'object', title: 'TWS (True Wind Speed)',
60
+ properties: {
61
+ stdMax: { type: 'number', title: 'Standard Mode Max Value', default: 25 },
62
+ hercSpan: { type: 'number', title: 'Hercules Mode Zoom Span', default: 10 },
63
+ step: { type: 'number', title: 'Rounding Step', default: 5 }
64
+ }
65
+ },
66
+ depth: {
67
+ type: 'object', title: 'Depth',
68
+ properties: {
69
+ stdMax: { type: 'number', title: 'Standard Mode Max Value', default: 20 },
70
+ hercSpan: { type: 'number', title: 'Hercules Mode Zoom Span', default: 10 },
71
+ step: { type: 'number', title: 'Rounding Step', default: 10 }
72
+ }
73
+ }
43
74
  }
44
75
  }
45
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {