speedandcurrent 2.1.2 → 2.2.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/README.md +2 -0
- package/index.js +121 -58
- package/package.json +2 -2
- package/public/app.js +48 -15
- package/public/index.html +1 -0
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Most paddle wheel logs carry a systematic error that varies with boat speed and
|
|
|
6
6
|
|
|
7
7
|
This plugin builds and maintains a **2-dimensional correction table** — indexed by speed and heel — that is populated automatically from real sailing observations. No calibration runs, no spreadsheets, no manual entry. Once the table has enough coverage the plugin applies per-point corrections to `navigation.speedThroughWater`, derives leeway from the lateral component of the correction, and estimates water current by subtracting the corrected boat speed vector from the GPS ground speed vector.
|
|
8
8
|
|
|
9
|
+
In the current implementation the plugin is also explicit about runtime state: if an input goes idle or stale, the webapp shows a warning and the handler resubscribes; when estimation is disabled or the plugin stops, the active output paths are cleared with `null` so downstream consumers do not keep stale values.
|
|
10
|
+
|
|
9
11
|
In broad terms the plugin:
|
|
10
12
|
|
|
11
13
|
- corrects paddle wheel speed using a learned, heel- and speed-aware correction table
|
package/index.js
CHANGED
|
@@ -38,8 +38,7 @@ module.exports = function (app) {
|
|
|
38
38
|
smootherTau: 3,
|
|
39
39
|
smootherTimeSpan: 5,
|
|
40
40
|
smootherSteadyState: 0.2,
|
|
41
|
-
showStatistics: false
|
|
42
|
-
stalenessDetection: true
|
|
41
|
+
showStatistics: false
|
|
43
42
|
};
|
|
44
43
|
|
|
45
44
|
|
|
@@ -140,6 +139,56 @@ module.exports = function (app) {
|
|
|
140
139
|
let started = null;
|
|
141
140
|
let minSpeed = 0;
|
|
142
141
|
let lastSave = 0;
|
|
142
|
+
let lifecycleWarningMap = new Map();
|
|
143
|
+
let lifecycleWarnings = [];
|
|
144
|
+
|
|
145
|
+
function setLifecycleWarning(id, status, path) {
|
|
146
|
+
const safePath = path || 'unknown path';
|
|
147
|
+
const message = status === 'idle'
|
|
148
|
+
? `Input ${id} is idle on ${safePath}; resubscribing`
|
|
149
|
+
: `Input ${id} is stale on ${safePath}`;
|
|
150
|
+
lifecycleWarningMap.set(id, {
|
|
151
|
+
id,
|
|
152
|
+
status,
|
|
153
|
+
path: safePath,
|
|
154
|
+
message,
|
|
155
|
+
updatedAt: Date.now()
|
|
156
|
+
});
|
|
157
|
+
lifecycleWarnings = Array.from(lifecycleWarningMap.values())
|
|
158
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function clearLifecycleWarning(id) {
|
|
162
|
+
if (!lifecycleWarningMap.has(id)) return;
|
|
163
|
+
lifecycleWarningMap.delete(id);
|
|
164
|
+
lifecycleWarnings = Array.from(lifecycleWarningMap.values())
|
|
165
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildLifecycleCallbacks(id, getPath, resubscribe) {
|
|
169
|
+
return {
|
|
170
|
+
onDelta: () => {
|
|
171
|
+
clearLifecycleWarning(id);
|
|
172
|
+
},
|
|
173
|
+
onStale: () => {
|
|
174
|
+
if (!isRunning) return;
|
|
175
|
+
const path = getPath();
|
|
176
|
+
app.debug(`[${plugin.id}] stale input ${id} on ${path}`);
|
|
177
|
+
setLifecycleWarning(id, 'stale', path);
|
|
178
|
+
},
|
|
179
|
+
onIdle: () => {
|
|
180
|
+
if (!isRunning) return;
|
|
181
|
+
const path = getPath();
|
|
182
|
+
app.debug(`[${plugin.id}] idle input ${id} on ${path}; resubscribing`);
|
|
183
|
+
setLifecycleWarning(id, 'idle', path);
|
|
184
|
+
try {
|
|
185
|
+
resubscribe();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
app.debug(`[${plugin.id}] resubscribe failed for ${id}: ${e.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
143
192
|
|
|
144
193
|
const plugin = {};
|
|
145
194
|
plugin.id = "SpeedAndCurrent";
|
|
@@ -161,7 +210,9 @@ module.exports = function (app) {
|
|
|
161
210
|
if (!isRunning) {
|
|
162
211
|
res.status(503).json({ error: "Plugin is not running" });
|
|
163
212
|
} else {
|
|
164
|
-
|
|
213
|
+
const payload = reportFull.report();
|
|
214
|
+
payload.lifecycleWarnings = lifecycleWarnings;
|
|
215
|
+
res.json(payload);
|
|
165
216
|
}
|
|
166
217
|
});
|
|
167
218
|
|
|
@@ -176,7 +227,7 @@ module.exports = function (app) {
|
|
|
176
227
|
|
|
177
228
|
|
|
178
229
|
router.get('/api/status', (req, res) => {
|
|
179
|
-
res.json({ status: pluginStatus, isRunning });
|
|
230
|
+
res.json({ status: pluginStatus, isRunning, lifecycleWarnings });
|
|
180
231
|
});
|
|
181
232
|
|
|
182
233
|
// --- Settings API ---
|
|
@@ -305,6 +356,8 @@ module.exports = function (app) {
|
|
|
305
356
|
plugin.start = (settings) => {
|
|
306
357
|
setStatus('Starting');
|
|
307
358
|
app.debug("Starting");
|
|
359
|
+
lifecycleWarningMap = new Map();
|
|
360
|
+
lifecycleWarnings = [];
|
|
308
361
|
readOptions(); // pick up any saves since registerWithRouter ran
|
|
309
362
|
migrateConfig(); // strip obsolete fields from persisted config
|
|
310
363
|
const tableName = options.tableName || 'correctionTable';
|
|
@@ -320,12 +373,13 @@ module.exports = function (app) {
|
|
|
320
373
|
angleRange: '0to2pi',
|
|
321
374
|
meta: { displayName: 'Heading', plane: 'Ground' },
|
|
322
375
|
SmootherClass,
|
|
323
|
-
smootherOptions
|
|
376
|
+
smootherOptions,
|
|
377
|
+
...buildLifecycleCallbacks(
|
|
378
|
+
'heading.angle',
|
|
379
|
+
() => smoothedHeading?.handler?.path || 'navigation.headingTrue',
|
|
380
|
+
() => { smoothedHeading?.unsubscribe(); smoothedHeading?.subscribe(false, true); }
|
|
381
|
+
)
|
|
324
382
|
});
|
|
325
|
-
// The magnitude handler has a fixed value of 1 and is never subscribed, so its timestamp
|
|
326
|
-
// stays null forever. Staleness detection on it would always force _stale=true, blocking
|
|
327
|
-
// processChanges() from setting polar._ready=true on every heading delta.
|
|
328
|
-
smoothedHeading.polar.magnitudeHandler.stalenessDetection = false;
|
|
329
383
|
rawHeading = smoothedHeading.handler;
|
|
330
384
|
|
|
331
385
|
//attitude
|
|
@@ -335,7 +389,12 @@ module.exports = function (app) {
|
|
|
335
389
|
path: 'navigation.attitude',
|
|
336
390
|
subscribe: true,
|
|
337
391
|
SmootherClass,
|
|
338
|
-
smootherOptions
|
|
392
|
+
smootherOptions,
|
|
393
|
+
...buildLifecycleCallbacks(
|
|
394
|
+
'attitude.smoothed',
|
|
395
|
+
() => smoothedAttitude?.handler?.path || 'navigation.attitude',
|
|
396
|
+
() => { smoothedAttitude?.unsubscribe(); smoothedAttitude?.subscribe(); }
|
|
397
|
+
)
|
|
339
398
|
});
|
|
340
399
|
rawAttitude = smoothedAttitude.handler;
|
|
341
400
|
|
|
@@ -370,7 +429,6 @@ module.exports = function (app) {
|
|
|
370
429
|
});
|
|
371
430
|
noCurrent.xSmoother.reset(0,0);
|
|
372
431
|
noCurrent.ySmoother.reset(0,0);
|
|
373
|
-
noCurrent.stalenessDetection = false; // noCurrent is a fixed zero-vector, never receives live data
|
|
374
432
|
PolarSmoother.send(app, plugin.id, [noCurrent]);
|
|
375
433
|
|
|
376
434
|
MessageHandler.setMeta(app, plugin.id, 'navigation.leewayAngle', {
|
|
@@ -388,7 +446,49 @@ module.exports = function (app) {
|
|
|
388
446
|
path: 'navigation.speedThroughWater',
|
|
389
447
|
subscribe: true,
|
|
390
448
|
SmootherClass,
|
|
391
|
-
smootherOptions
|
|
449
|
+
smootherOptions,
|
|
450
|
+
onDelta: () => {
|
|
451
|
+
clearLifecycleWarning('boatSpeed.smoothed');
|
|
452
|
+
// Drain any pending option changes before calculating
|
|
453
|
+
if (Object.keys(changedOptions).length) applyOptionChanges();
|
|
454
|
+
|
|
455
|
+
const wellUnderway = started < new Date() - 60 * 1000;
|
|
456
|
+
|
|
457
|
+
setStatus(wellUnderway ? 'Running' : 'Stabilizing');
|
|
458
|
+
if (options.estimateBoatSpeed) correct(wellUnderway);
|
|
459
|
+
if (options.updateCorrectionTable) {
|
|
460
|
+
if (wellUnderway) {
|
|
461
|
+
updateTable();
|
|
462
|
+
// Save correction table periodically (skip if table is empty to avoid overwriting good data on disk)
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
if (now - lastSave > 60_000) {
|
|
465
|
+
if (!isTableEmpty(table)) {
|
|
466
|
+
saveTable(table, path.join(app.getDataDirPath(), table.id + '.json'));
|
|
467
|
+
}
|
|
468
|
+
lastSave = now;
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
table.lastUpdateResult = 'stabilizing';
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
table.lastUpdateResult = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
,
|
|
478
|
+
onIdle: () => {
|
|
479
|
+
if (!isRunning) return;
|
|
480
|
+
const path = smoothedBoatSpeed?.handler?.path || 'navigation.speedThroughWater';
|
|
481
|
+
app.debug(`[${plugin.id}] idle input boatSpeed.smoothed on ${path}; resubscribing`);
|
|
482
|
+
setLifecycleWarning('boatSpeed.smoothed', 'idle', path);
|
|
483
|
+
smoothedBoatSpeed?.unsubscribe();
|
|
484
|
+
smoothedBoatSpeed?.subscribe();
|
|
485
|
+
},
|
|
486
|
+
onStale: () => {
|
|
487
|
+
if (!isRunning) return;
|
|
488
|
+
const path = smoothedBoatSpeed?.handler?.path || 'navigation.speedThroughWater';
|
|
489
|
+
app.debug(`[${plugin.id}] stale input boatSpeed.smoothed on ${path}`);
|
|
490
|
+
setLifecycleWarning('boatSpeed.smoothed', 'stale', path);
|
|
491
|
+
}
|
|
392
492
|
});
|
|
393
493
|
rawBoatSpeed = smoothedBoatSpeed.handler;
|
|
394
494
|
|
|
@@ -419,7 +519,12 @@ module.exports = function (app) {
|
|
|
419
519
|
angleRange: '0to2pi',
|
|
420
520
|
meta: { displayName: 'Groundspeed', plane: 'Ground' },
|
|
421
521
|
SmootherClass,
|
|
422
|
-
smootherOptions
|
|
522
|
+
smootherOptions,
|
|
523
|
+
...buildLifecycleCallbacks(
|
|
524
|
+
'groundSpeed.smoothed',
|
|
525
|
+
() => `${smoothedGroundSpeed?.polar?.pathMagnitude || 'navigation.speedOverGround'}, ${smoothedGroundSpeed?.polar?.pathAngle || 'navigation.courseOverGroundTrue'}`,
|
|
526
|
+
() => { smoothedGroundSpeed?.unsubscribe(); smoothedGroundSpeed?.subscribe(true, true); }
|
|
527
|
+
)
|
|
423
528
|
});
|
|
424
529
|
rawGroundSpeed = smoothedGroundSpeed.polar;
|
|
425
530
|
|
|
@@ -466,50 +571,12 @@ module.exports = function (app) {
|
|
|
466
571
|
|
|
467
572
|
//#endregion
|
|
468
573
|
|
|
469
|
-
// Apply staleness detection to all subscribed smoother instances.
|
|
470
|
-
// noCurrent is intentionally excluded — it is a fixed zero-vector and never receives live data.
|
|
471
|
-
// Only apply when sd = false: all smoothers already default to stalenessDetection = true.
|
|
472
|
-
// Explicitly calling the setter with true at startup forces _stale = true on handlers that have
|
|
473
|
-
// never received data, causing the first sample to be dropped (library side-effect).
|
|
474
|
-
const sd = options.stalenessDetection ?? true;
|
|
475
|
-
if (!sd) {
|
|
476
|
-
for (const s of [smoothedHeading, smoothedAttitude, smoothedBoatSpeed, smoothedGroundSpeed, smoothedCurrent, smoothedResidual]) {
|
|
477
|
-
if (s) s.stalenessDetection = false;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
574
|
isRunning = true;
|
|
482
575
|
started = Date.now();
|
|
483
576
|
lastSave = 0;
|
|
484
577
|
setStatus('Running');
|
|
485
578
|
app.debug("Running");
|
|
486
579
|
|
|
487
|
-
smoothedBoatSpeed.onChange = () => {
|
|
488
|
-
// Drain any pending option changes before calculating
|
|
489
|
-
if (Object.keys(changedOptions).length) applyOptionChanges();
|
|
490
|
-
|
|
491
|
-
const wellUnderway = started < new Date() - 60 * 1000;
|
|
492
|
-
|
|
493
|
-
setStatus(wellUnderway ? 'Running' : 'Stabilizing');
|
|
494
|
-
if (options.estimateBoatSpeed) correct(wellUnderway);
|
|
495
|
-
if (options.updateCorrectionTable) {
|
|
496
|
-
if (wellUnderway) {
|
|
497
|
-
updateTable();
|
|
498
|
-
// Save correction table periodically (skip if table is empty to avoid overwriting good data on disk)
|
|
499
|
-
const now = Date.now();
|
|
500
|
-
if (now - lastSave > 60_000) {
|
|
501
|
-
if (!isTableEmpty(table)) {
|
|
502
|
-
saveTable(table, path.join(app.getDataDirPath(), table.id + '.json'));
|
|
503
|
-
}
|
|
504
|
-
lastSave = now;
|
|
505
|
-
}
|
|
506
|
-
} else {
|
|
507
|
-
table.lastUpdateResult = 'stabilizing';
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
table.lastUpdateResult = null;
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
580
|
}
|
|
514
581
|
|
|
515
582
|
plugin.stop = () => {
|
|
@@ -546,6 +613,8 @@ module.exports = function (app) {
|
|
|
546
613
|
|
|
547
614
|
pluginStatus = 'Stopped';
|
|
548
615
|
isRunning = false;
|
|
616
|
+
lifecycleWarningMap = new Map();
|
|
617
|
+
lifecycleWarnings = [];
|
|
549
618
|
resolve();
|
|
550
619
|
} catch (error) {
|
|
551
620
|
reject(error);
|
|
@@ -723,13 +792,7 @@ module.exports = function (app) {
|
|
|
723
792
|
const value = changedOptions[key];
|
|
724
793
|
options[key] = value;
|
|
725
794
|
|
|
726
|
-
if (key === '
|
|
727
|
-
const sdVal = Boolean(value);
|
|
728
|
-
for (const s of [smoothedHeading, smoothedAttitude, smoothedBoatSpeed, smoothedGroundSpeed, smoothedCurrent, smoothedResidual]) {
|
|
729
|
-
if (s) s.stalenessDetection = sdVal;
|
|
730
|
-
}
|
|
731
|
-
// noCurrent is always excluded from staleness detection
|
|
732
|
-
} else if (key === 'updateCorrectionTable') {
|
|
795
|
+
if (key === 'updateCorrectionTable') {
|
|
733
796
|
if (!value && table && !isTableEmpty(table)) {
|
|
734
797
|
// Learning switched off — persist current state before periodic saves stop
|
|
735
798
|
saveTableSync(table, path.join(app.getDataDirPath(), table.id + '.json'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speedandcurrent",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"kalman-filter": "^2.3.0",
|
|
41
|
-
"signalkutilities": "^3.0
|
|
41
|
+
"signalkutilities": "^3.1.0"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"icon.png",
|
package/public/app.js
CHANGED
|
@@ -97,6 +97,7 @@ function normaliseState(data) {
|
|
|
97
97
|
|
|
98
98
|
// ─── Static meta (fetched once at startup from /api/meta) ─────────────────────
|
|
99
99
|
let metaById = {}; // keyed by item id
|
|
100
|
+
let lifecycleWarnings = [];
|
|
100
101
|
|
|
101
102
|
async function loadMeta() {
|
|
102
103
|
const data = await apiGet('/api/meta');
|
|
@@ -182,6 +183,29 @@ function renderWarnings(elId, ids) {
|
|
|
182
183
|
el.appendChild(ul);
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
function renderInputWarnings(elId, ids) {
|
|
187
|
+
const el = document.getElementById(elId);
|
|
188
|
+
if (!el) return;
|
|
189
|
+
el.innerHTML = '';
|
|
190
|
+
const lines = [];
|
|
191
|
+
lifecycleWarnings.forEach(w => {
|
|
192
|
+
if (w && typeof w.message === 'string') lines.push(w.message);
|
|
193
|
+
});
|
|
194
|
+
if (lines.length === 0) return;
|
|
195
|
+
const h = document.createElement('h6');
|
|
196
|
+
h.className = 'text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small';
|
|
197
|
+
h.textContent = 'Warnings';
|
|
198
|
+
el.appendChild(h);
|
|
199
|
+
const ul = document.createElement('ul');
|
|
200
|
+
ul.className = 'list-unstyled text-danger small ps-3';
|
|
201
|
+
lines.forEach(line => {
|
|
202
|
+
const li = document.createElement('li');
|
|
203
|
+
li.textContent = line;
|
|
204
|
+
ul.appendChild(li);
|
|
205
|
+
});
|
|
206
|
+
el.appendChild(ul);
|
|
207
|
+
}
|
|
208
|
+
|
|
185
209
|
// ─── Config (live settings mirror) ───────────────────────────────────────────
|
|
186
210
|
let config = null;
|
|
187
211
|
|
|
@@ -296,15 +320,15 @@ const paramMeta = {
|
|
|
296
320
|
min: 0.01, max: 0.99, step: 0.01, default: 0.2,
|
|
297
321
|
description: 'Steady-state Kalman gain (0 ≈ slow/smooth, 1 ≈ fast/raw). Clamped to 0.01–0.99.'
|
|
298
322
|
},
|
|
299
|
-
stalenessDetection: {
|
|
300
|
-
label: 'Staleness detection',
|
|
301
|
-
type: 'boolean',
|
|
302
|
-
description: 'Mark inputs as unavailable when no data has been received for a period. '
|
|
303
|
-
},
|
|
304
323
|
};
|
|
305
324
|
|
|
306
325
|
// Settings groups for each UI section
|
|
307
|
-
const INPUTS_SETTING_KEYS = [
|
|
326
|
+
const INPUTS_SETTING_KEYS = [
|
|
327
|
+
'smootherClass',
|
|
328
|
+
{ key: 'smootherTau', showIf: cfg => cfg.smootherClass === 'ExponentialSmoother' },
|
|
329
|
+
{ key: 'smootherTimeSpan', showIf: cfg => (cfg.smootherClass || 'MovingAverageSmoother') === 'MovingAverageSmoother' },
|
|
330
|
+
{ key: 'smootherSteadyState', showIf: cfg => cfg.smootherClass === 'KalmanSmoother' },
|
|
331
|
+
];
|
|
308
332
|
const ESTIMATION_SETTING_KEYS = ['sogFallback'];
|
|
309
333
|
const LEARNING_SETTING_KEYS = ['stability','assumeCurrent','showStatistics'];
|
|
310
334
|
const SMOOTHER_SETTING_KEYS = [
|
|
@@ -511,12 +535,22 @@ function renderGroupInto(elId, polars, deltas, attitudes) {
|
|
|
511
535
|
}
|
|
512
536
|
|
|
513
537
|
function renderLiveSections() {
|
|
538
|
+
const inputPolars = filterById(state.polarsAll, ['groundSpeed']);
|
|
539
|
+
const inputDeltas = filterById(state.deltasAll, ['heading.angle', 'boatSpeed']);
|
|
540
|
+
const inputAttitudes = filterById(state.attitudesAll, ['attitude']);
|
|
541
|
+
const fallbackInputPolars = inputPolars.length ? inputPolars : filterById(state.polarsAll, ['groundSpeed.smoothed']);
|
|
542
|
+
const fallbackInputDeltas = inputDeltas.length ? inputDeltas : filterById(state.deltasAll, ['heading.smoothed', 'boatSpeed.smoothed']);
|
|
543
|
+
const fallbackInputAttitudes = inputAttitudes.length ? inputAttitudes : filterById(state.attitudesAll, ['attitude.smoothed']);
|
|
544
|
+
|
|
514
545
|
// Inputs section — raw sensor readings only (smoothing is internal to the plugin)
|
|
515
546
|
renderGroupInto('inputs-values',
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
547
|
+
fallbackInputPolars,
|
|
548
|
+
fallbackInputDeltas,
|
|
549
|
+
fallbackInputAttitudes
|
|
519
550
|
);
|
|
551
|
+
const inputWarningIds = ['boatSpeed', 'attitude', 'heading.angle', 'groundSpeed', 'boatSpeed.smoothed', 'groundSpeed.smoothed', 'heading.smoothed', 'attitude.smoothed'];
|
|
552
|
+
if (config && config.assumeCurrent) inputWarningIds.push('current.smoothed');
|
|
553
|
+
renderInputWarnings('inputs-warnings', inputWarningIds);
|
|
520
554
|
|
|
521
555
|
// Estimation — inputs (raw sensor data used for boat speed estimation)
|
|
522
556
|
renderGroupInto('estimation-inputs',
|
|
@@ -535,9 +569,8 @@ function renderLiveSections() {
|
|
|
535
569
|
[], []
|
|
536
570
|
);
|
|
537
571
|
// Estimation — warnings
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
);
|
|
572
|
+
const estimationWarnings = document.getElementById('estimation-warnings');
|
|
573
|
+
if (estimationWarnings) estimationWarnings.innerHTML = '';
|
|
541
574
|
|
|
542
575
|
// Learning — inputs: smoothed sensors + current if assumeCurrent
|
|
543
576
|
const learningCurrentPolars = (config && config.assumeCurrent)
|
|
@@ -549,9 +582,8 @@ function renderLiveSections() {
|
|
|
549
582
|
filterById(state.attitudesAll, ['attitude.smoothed'])
|
|
550
583
|
);
|
|
551
584
|
// Learning — warnings
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
renderWarnings('learning-warnings', learningWarningIds);
|
|
585
|
+
const learningWarnings = document.getElementById('learning-warnings');
|
|
586
|
+
if (learningWarnings) learningWarnings.innerHTML = '';
|
|
555
587
|
// Learning status section
|
|
556
588
|
const statusTbody = document.querySelector('#learning-status-table tbody');
|
|
557
589
|
if (statusTbody) {
|
|
@@ -620,6 +652,7 @@ async function tick() {
|
|
|
620
652
|
.then(r => r.ok ? r.json() : null)
|
|
621
653
|
.catch(() => null);
|
|
622
654
|
_pluginStatus = statusData?.status ?? '';
|
|
655
|
+
lifecycleWarnings = Array.isArray(statusData?.lifecycleWarnings) ? statusData.lifecycleWarnings : [];
|
|
623
656
|
_refreshMessage();
|
|
624
657
|
}
|
|
625
658
|
|