@sailingrotevista/rotevista-dash 1.0.21 → 1.0.23

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 +33 -43
  2. package/index.js +78 -20
  3. package/package.json +1 -1
package/app.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // ==========================================================================
2
- // 1. CONFIGURAZIONE E COSTANTI
2
+ // 1. CONFIGURAZIONE E DEFAULT
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
5
  alarms: { depthDanger: 2.5, depthWarning: 5.0 },
6
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 },
7
+ graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
8
8
  scales: {
9
9
  stw: { stdMax: 12, hercSpan: 4, step: 2 },
10
10
  sog: { stdMax: 12, hercSpan: 4, step: 2 },
@@ -33,14 +33,7 @@ const graphModes = {
33
33
  depth: localStorage.getItem('mode_depth') || 'standard'
34
34
  };
35
35
 
36
- const store = {
37
- raw: {},
38
- timestamps: {},
39
- smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
40
- longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
41
- histories: { stw: [], sog: [], depth: [], tws: [] },
42
- lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 }
43
- };
36
+ const store = { raw: {}, timestamps: {}, smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] }, longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] }, histories: { stw: [], sog: [], depth: [], tws: [] }, lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0 } };
44
37
 
45
38
  const ui = {
46
39
  stw: document.getElementById('stw'), sog: document.getElementById('sog'),
@@ -56,7 +49,7 @@ const ui = {
56
49
  };
57
50
 
58
51
  // ==========================================================================
59
- // 3. CARICAMENTO CONFIGURAZIONE (SK-SERVER CONFIG)
52
+ // 3. CARICAMENTO CONFIGURAZIONE
60
53
  // ==========================================================================
61
54
  async function fetchServerConfig() {
62
55
  if (!window.location.protocol.includes("http")) return;
@@ -70,8 +63,6 @@ async function fetchServerConfig() {
70
63
  const data = await response.json();
71
64
  const actual = data.configuration || data;
72
65
  if (actual && typeof actual === 'object') {
73
-
74
- // Funzione per pulire e convertire i numeri
75
66
  const parseNumbers = (obj) => {
76
67
  for (let k in obj) {
77
68
  if (typeof obj[k] === 'object') parseNumbers(obj[k]);
@@ -79,18 +70,11 @@ async function fetchServerConfig() {
79
70
  }
80
71
  };
81
72
  parseNumbers(actual);
82
-
83
- // Merge intelligente dei blocchi
84
73
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
85
74
  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);
75
+ if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging }; // Mapping averaging -> averages
76
+ if (actual.scales) { for (let key in actual.scales) { CONFIG.scales[key] = { ...CONFIG.scales[key], ...actual.scales[key] }; } }
77
+ console.log("SUCCESS: Config loaded from Server:", CONFIG);
94
78
  return;
95
79
  }
96
80
  }
@@ -99,7 +83,7 @@ async function fetchServerConfig() {
99
83
  }
100
84
 
101
85
  // ==========================================================================
102
- // 4. MATEMATICA VETTORIALE (CIRCULAR AVERAGING)
86
+ // 4. MATEMATICA VETTORIALE
103
87
  // ==========================================================================
104
88
  function radToDeg(rad) { return rad * (180 / Math.PI); }
105
89
  function degToRad(deg) { return deg * (Math.PI / 180); }
@@ -107,7 +91,6 @@ function msToKts(ms) { return ms * 1.94384; }
107
91
  function ktsToMs(kts) { return kts / 1.94384; }
108
92
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
109
93
 
110
- // Calcolo media basato su componenti Seno/Coseno (Vettoriale)
111
94
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
112
95
  const now = Date.now();
113
96
  const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
@@ -131,7 +114,6 @@ function processIncomingData(path, val) {
131
114
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
132
115
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
133
116
 
134
- // Calcolo o ricezione TWD (True Wind Direction)
135
117
  if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
136
118
  let twdRad = 0;
137
119
  if (path === "environment.wind.directionTrue") twdRad = val;
@@ -139,14 +121,13 @@ function processIncomingData(path, val) {
139
121
  twdRad = (store.raw["navigation.headingTrue"] + store.raw["environment.wind.angleTrueWater"]) % (2 * Math.PI);
140
122
  if (twdRad < 0) twdRad += (2 * Math.PI);
141
123
  } else return;
142
-
143
124
  store.smoothBuf.twd.push({ val: twdRad, time: now });
144
125
  store.longBuf.twd.push({ val: twdRad, time: now });
145
126
  }
146
127
  }
147
128
 
148
129
  // ==========================================================================
149
- // 6. MOTORE DI RENDERING
130
+ // 6. RENDER LOOP
150
131
  // ==========================================================================
151
132
  function startDisplayLoop() {
152
133
  renderInterval = setInterval(() => {
@@ -192,7 +173,9 @@ function startDisplayLoop() {
192
173
  let tA = twObj.val * 2;
193
174
  ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
194
175
  if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
195
- if (hObj.stable && twObj.stable || curSog < CONFIG.averages.minSpeed) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); } else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
176
+ let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
177
+ if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
178
+ else { ui.tackHdg.classList.add('unstable-data'); ui.tackCog.classList.add('unstable-data'); }
196
179
  }
197
180
  if (twdObj) { curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val); ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`); }
198
181
  lastAvgUIUpdate = now;
@@ -217,13 +200,20 @@ function connect() {
217
200
  }
218
201
 
219
202
  // ==========================================================================
220
- // 8. FUNZIONI GRAFICHE (Hercules Scalable)
203
+ // 8. FUNZIONI GRAFICHE (Hercules Mode)
221
204
  // ==========================================================================
222
205
  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)}°`; }
223
206
 
224
207
  function manageHistory(t, v) {
225
- const n = Date.now(), i = simulationMode ? SIM_SAMPLE_INTERVAL : CONFIG.graphs.realInterval;
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; }
208
+ const n = Date.now();
209
+ const dynamicInterval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
210
+ const interval = simulationMode ? SIM_SAMPLE_INTERVAL : dynamicInterval;
211
+
212
+ if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
213
+ store.histories[t].push(v);
214
+ if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
215
+ store.lastUpdates[t] = n;
216
+ }
227
217
  const mode = graphModes[t];
228
218
  const cfg = calculateScale(t, store.histories[t], mode);
229
219
  const box = document.getElementById(t + '-graph').closest('.data-box');
@@ -254,7 +244,14 @@ function updateScaleLabels(t, min, max) {
254
244
  function drawGraph(d, id, min, max, isTws, isHercules) {
255
245
  const svg = document.getElementById(id); if (!svg || d.length < 2) return;
256
246
  const w = 200, h = 40, range = max - min || 1;
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" />`; });
247
+ let grids = "";
248
+ [0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
249
+
250
+ const minutes = CONFIG.graphs.historyMinutes;
251
+ for (let m = 1; m < minutes; m++) {
252
+ const x = w - (m / minutes) * w;
253
+ grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`;
254
+ }
258
255
 
259
256
  let pD = "", cS = "";
260
257
  d.forEach((v, i) => {
@@ -266,15 +263,9 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
266
263
  }
267
264
  });
268
265
 
269
- const areaPath = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
270
266
  const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
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
- }
267
+ const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`;
268
+ svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}` : `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" stroke-width="1.5" />`;
278
269
  }
279
270
 
280
271
  // ==========================================================================
@@ -302,10 +293,9 @@ if (ui.hotspot) {
302
293
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
303
294
  window.addEventListener('load', init);
304
295
 
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'); }
296
+ 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'); }
306
297
  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); }
307
298
 
308
- // Simulatore (Triple click su Depth)
309
299
  ui.depth.closest('.data-box').addEventListener('click', (function() {
310
300
  let dC = 0, lC = 0;
311
301
  return function() {
package/index.js CHANGED
@@ -2,6 +2,7 @@ module.exports = function (app) {
2
2
  const plugin = {};
3
3
  plugin.id = 'rotevista-dash';
4
4
  plugin.name = 'Rotevista Dash Configuration';
5
+ plugin.description = 'Configure boat-specific parameters for the Dashboard';
5
6
 
6
7
  plugin.start = function (options) { };
7
8
  plugin.stop = function () { };
@@ -13,62 +14,119 @@ module.exports = function (app) {
13
14
  alarms: {
14
15
  type: 'object',
15
16
  title: 'Depth Alarms (meters)',
17
+ description: "Configure safety thresholds for depth monitoring.",
16
18
  properties: {
17
- depthDanger: { type: 'number', title: 'Danger Threshold (Red + Sound)', default: 2.5 },
18
- depthWarning: { type: 'number', title: 'Warning Threshold (Yellow)', default: 5.0 }
19
+ depthDanger: {
20
+ type: 'number',
21
+ title: 'Danger Threshold (Red + Sound)',
22
+ description: "Critical depth level. When depth is below this value, the display turns red and an audible alarm is triggered.",
23
+ default: 2.5
24
+ },
25
+ depthWarning: {
26
+ type: 'number',
27
+ title: 'Warning Threshold (Yellow)',
28
+ description: "Shallow water margin. When depth is below this value, the display turns yellow as a safety warning.",
29
+ default: 5.0
30
+ }
19
31
  }
20
32
  },
21
33
  graphs: {
22
34
  type: 'object',
23
- title: 'Wind Reef Alerts (TWS Knots)',
35
+ title: 'Graph & Reef Alerts',
36
+ description: "General settings for graph sampling and tactical wind alerts.",
24
37
  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 }
38
+ reef1: {
39
+ type: 'number',
40
+ title: '1st Reef Threshold (Orange)',
41
+ description: "TWS knots at which the wind graph turns orange, indicating it's time to prepare for the first reef.",
42
+ default: 15.0
43
+ },
44
+ reef2: {
45
+ type: 'number',
46
+ title: '2nd Reef Threshold (Red)',
47
+ description: "TWS knots at which the wind graph turns red, indicating urgent reefing is required.",
48
+ default: 20.0
49
+ },
50
+ historyMinutes: {
51
+ type: 'number',
52
+ title: 'Graph History Duration (minutes)',
53
+ description: "Total timeframe shown in the sparklines. Vertical grid lines will automatically mark each elapsed minute.",
54
+ default: 5
55
+ }
27
56
  }
28
57
  },
29
58
  averaging: {
30
59
  type: 'object',
31
60
  title: 'Averaging & Stability',
61
+ description: "Fine-tune data smoothing and maneuver detection.",
32
62
  properties: {
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 }
63
+ longWindow: {
64
+ type: 'number',
65
+ title: 'Long Average Window (ms)',
66
+ description: "Time buffer used for 'MEAN' values (e.g., 60000ms = 1 min). Higher values provide more stability but slower reaction.",
67
+ default: 60000
68
+ },
69
+ smoothWindow: {
70
+ type: 'number',
71
+ title: 'Pointer Smoothing Window (ms)',
72
+ description: "Buffer for gauge needles and pointers. Removes sensor jitter while maintaining real-time responsiveness.",
73
+ default: 2000
74
+ },
75
+ minSpeed: {
76
+ type: 'number',
77
+ title: 'Min Speed for Stability (knots)',
78
+ description: "SOG threshold below which stability alerts (blinking orange) are suppressed to avoid GPS noise while docked.",
79
+ default: 0.5
80
+ }
36
81
  }
37
82
  },
38
83
  scales: {
39
84
  type: 'object',
40
85
  title: 'Graph Scale Configurations',
86
+ description: "Customize how scales adapt to your boat's performance in both Standard and Hercules modes.",
41
87
  properties: {
42
88
  stw: {
43
89
  type: 'object', title: 'STW (Speed Through Water)',
44
90
  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 }
91
+ stdMax: {
92
+ type: 'number', title: 'Standard Mode Max',
93
+ description: "The initial top limit of the graph (base 0) during normal navigation.",
94
+ default: 12
95
+ },
96
+ step: {
97
+ type: 'number', title: 'Rounding Step',
98
+ description: "The fixed increment used when speed exceeds the Max (e.g., scale jumps from 0-12 to 0-14, 0-16).",
99
+ default: 2
100
+ },
101
+ hercSpan: {
102
+ type: 'number', title: 'Hercules Zoom Span',
103
+ description: "The minimum knots window centered on current speed. Smaller values increase 'zoom' on small variations.",
104
+ default: 4
105
+ }
48
106
  }
49
107
  },
50
108
  sog: {
51
109
  type: 'object', title: 'SOG (Speed Over Ground)',
52
110
  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 }
111
+ stdMax: { type: 'number', title: 'Standard Mode Max', description: "Initial top limit for the base-0 scale.", default: 12 },
112
+ step: { type: 'number', title: 'Rounding Step', description: "Scale jump interval to keep labels tidy.", default: 2 },
113
+ hercSpan: { type: 'number', title: 'Hercules Zoom Span', description: "Minimum window amplitude during active zoom.", default: 4 }
56
114
  }
57
115
  },
58
116
  tws: {
59
117
  type: 'object', title: 'TWS (True Wind Speed)',
60
118
  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 }
119
+ stdMax: { type: 'number', title: 'Standard Mode Max', description: "Maximum wind speed shown in standard view (base 0).", default: 25 },
120
+ step: { type: 'number', title: 'Rounding Step', description: "Incremental jump for wind scales (usually 5 or 10 knots).", default: 5 },
121
+ hercSpan: { type: 'number', title: 'Hercules Zoom Span', description: "Minimum knots window for high-detail gust monitoring.", default: 10 }
64
122
  }
65
123
  },
66
124
  depth: {
67
125
  type: 'object', title: 'Depth',
68
126
  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 }
127
+ stdMax: { type: 'number', title: 'Standard Mode Max', description: "Default maximum depth for the graph scale.", default: 20 },
128
+ step: { type: 'number', title: 'Rounding Step', description: "Gradual increment steps for deep water navigation.", default: 10 },
129
+ hercSpan: { type: 'number', title: 'Hercules Zoom Span', description: "Minimum meters window to highlight bottom profile changes.", default: 10 }
72
130
  }
73
131
  }
74
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {