speedandcurrent 2.3.2 → 2.3.4
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/index.js +31 -6
- package/package.json +2 -2
- package/public/app.js +114 -98
package/index.js
CHANGED
|
@@ -39,6 +39,17 @@ function isCogOverrideActive(groundSpeedPolar, speedThreshold) {
|
|
|
39
39
|
&& sogHandler.value < speedThreshold;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Leeway is the angle of the corrected boatspeed vector, so a small lateral
|
|
44
|
+
* correction divided by a near-zero magnitude yields a large, meaningless angle.
|
|
45
|
+
*/
|
|
46
|
+
function isLeewayValid(speed, speedThreshold, navigationStateHandler) {
|
|
47
|
+
if (!(speed >= speedThreshold)) return false;
|
|
48
|
+
const state = navigationStateHandler?.state;
|
|
49
|
+
if (state?.ready !== true) return true;
|
|
50
|
+
return !ALWAYS_BLOCKING_NAVIGATION_STATES.has(normalizeNavigationState(navigationStateHandler.value));
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
function evaluateLearningMode({ options = {}, navigationState, stabilizingUntil = 0, stabilizingReason = null, now = Date.now() }) {
|
|
43
54
|
const normalizedNavigationState = normalizeNavigationState(navigationState?.value);
|
|
44
55
|
const navigationGateReady = navigationState?.ready === true;
|
|
@@ -316,6 +327,8 @@ module.exports = function (app) {
|
|
|
316
327
|
const safePath = path || 'unknown path';
|
|
317
328
|
const message = status === 'idle'
|
|
318
329
|
? `Input ${id} is idle on ${safePath}; resubscribing`
|
|
330
|
+
: status === 'incomplete'
|
|
331
|
+
? `Input ${id} is missing ${safePath}`
|
|
319
332
|
: `Input ${id} is stale on ${safePath}`;
|
|
320
333
|
lifecycleWarningMap.set(id, {
|
|
321
334
|
id,
|
|
@@ -621,7 +634,7 @@ module.exports = function (app) {
|
|
|
621
634
|
app, pluginId: plugin.id,
|
|
622
635
|
id: 'boatSpeed',
|
|
623
636
|
path: 'navigation.speedThroughWater',
|
|
624
|
-
subscribe:
|
|
637
|
+
subscribe: false,
|
|
625
638
|
SmootherClass,
|
|
626
639
|
smootherOptions,
|
|
627
640
|
onDelta: () => {
|
|
@@ -637,7 +650,7 @@ module.exports = function (app) {
|
|
|
637
650
|
now: Date.now()
|
|
638
651
|
});
|
|
639
652
|
|
|
640
|
-
const wellUnderway =
|
|
653
|
+
const wellUnderway = Date.now() >= learningStabilizingUntil;
|
|
641
654
|
setStatus(learningMode.state === 'stabilizing' ? 'Stabilizing' : 'Running');
|
|
642
655
|
if (options.estimateBoatSpeed) correct(wellUnderway);
|
|
643
656
|
updateTable();
|
|
@@ -755,6 +768,7 @@ module.exports = function (app) {
|
|
|
755
768
|
lastNavigationStateValue = normalizeNavigationState(navigationStateHandler?.value);
|
|
756
769
|
resetLearningStabilization('startup', LONG_STABILIZING_MS);
|
|
757
770
|
setStatus('Running');
|
|
771
|
+
smoothedBoatSpeed.subscribe();
|
|
758
772
|
app.debug("Running");
|
|
759
773
|
|
|
760
774
|
}
|
|
@@ -830,10 +844,15 @@ module.exports = function (app) {
|
|
|
830
844
|
if (options.sogFallback && rawGroundSpeed.ready && rawBoatSpeed.ready && rawBoatSpeed.value === 0 && rawGroundSpeed.magnitude >= minSpeed) {
|
|
831
845
|
correctedBoatSpeed.setVectorValue({ x: rawGroundSpeed.magnitude, y: 0 });
|
|
832
846
|
}
|
|
833
|
-
else if (rawAttitude.ready) {
|
|
847
|
+
else if (rawAttitude.ready && Number.isFinite(rawAttitude.value?.roll)) {
|
|
848
|
+
clearLifecycleWarning('attitude.roll');
|
|
834
849
|
if (correctedBoatSpeed.magnitude > 0) {
|
|
835
850
|
const { correction, variance } = table.getCorrection(correctedBoatSpeed.magnitude, rawAttitude.value?.roll);
|
|
836
|
-
|
|
851
|
+
const leewayValid = isLeewayValid(correctedBoatSpeed.magnitude, minSpeed, navigationStateHandler);
|
|
852
|
+
speedCorrection.setVectorValue(
|
|
853
|
+
{ x: correction.x, y: leewayValid ? correction.y : 0 },
|
|
854
|
+
{ x: variance.x, y: leewayValid ? variance.y : 0 }
|
|
855
|
+
);
|
|
837
856
|
correctedBoatSpeed.add(speedCorrection);
|
|
838
857
|
}
|
|
839
858
|
// Current estimation and residual also require heading (to rotate into ground frame).
|
|
@@ -862,7 +881,10 @@ module.exports = function (app) {
|
|
|
862
881
|
}
|
|
863
882
|
}
|
|
864
883
|
}
|
|
865
|
-
// Implicit fallback: attitude not ready — correctedBoatSpeed = raw STW, no correction
|
|
884
|
+
// Implicit fallback: attitude not ready or missing roll — correctedBoatSpeed = raw STW, no correction
|
|
885
|
+
else if (rawAttitude.ready) {
|
|
886
|
+
setLifecycleWarning('attitude.roll', 'incomplete', 'navigation.attitude.roll');
|
|
887
|
+
}
|
|
866
888
|
|
|
867
889
|
PolarSmoother.send(app, plugin.id, [smoothedCurrent, smoothedResidual]);
|
|
868
890
|
|
|
@@ -887,7 +909,8 @@ module.exports = function (app) {
|
|
|
887
909
|
learningMode.state = 'suspended';
|
|
888
910
|
learningMode.reason = 'cog_override';
|
|
889
911
|
}
|
|
890
|
-
const inputsReady = smoothedAttitude.ready &&
|
|
912
|
+
const inputsReady = smoothedAttitude.ready && Number.isFinite(smoothedAttitude.value?.roll)
|
|
913
|
+
&& smoothedBoatSpeed.ready && smoothedHeading.ready && smoothedGroundSpeed.ready;
|
|
891
914
|
const currentReady = !options.assumeCurrent || smoothedCurrent.ready;
|
|
892
915
|
const observationGate = evaluateObservationGate({
|
|
893
916
|
learningMode,
|
|
@@ -901,6 +924,7 @@ module.exports = function (app) {
|
|
|
901
924
|
|
|
902
925
|
if (observationGate.state !== 'pending') {
|
|
903
926
|
if (observationGate.state === 'invalid') {
|
|
927
|
+
setObservationStatus('rejected', observationGate.reason);
|
|
904
928
|
resetLearningStabilization('observation_reset', getShortStabilizingMs(options));
|
|
905
929
|
}
|
|
906
930
|
return;
|
|
@@ -1050,6 +1074,7 @@ module.exports._test = {
|
|
|
1050
1074
|
getShortStabilizingMs,
|
|
1051
1075
|
evaluateLearningMode,
|
|
1052
1076
|
isCogOverrideActive,
|
|
1077
|
+
isLeewayValid,
|
|
1053
1078
|
evaluateObservationGate,
|
|
1054
1079
|
getDerivedObservationStatus,
|
|
1055
1080
|
buildNavigationStateStatus,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speedandcurrent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.4",
|
|
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": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"kalman-filter": "^2.3.0",
|
|
44
|
-
"signalkutilities": "^3.1.
|
|
44
|
+
"signalkutilities": "^3.1.5"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"icon.png",
|
package/public/app.js
CHANGED
|
@@ -97,7 +97,6 @@ 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 = [];
|
|
101
100
|
|
|
102
101
|
async function loadMeta() {
|
|
103
102
|
const data = await apiGet('/api/meta');
|
|
@@ -117,90 +116,99 @@ async function loadMeta() {
|
|
|
117
116
|
applyUnitLabels();
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
function isNotReady(item) {
|
|
126
|
-
if (!item) return true;
|
|
127
|
-
return item.state?.ready !== true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Returns a human-readable reason why an item is not ready.
|
|
131
|
-
// Handles three state shapes:
|
|
132
|
-
// handler-nested (MessageSmoother, SmoothedAngle): state.handler.*
|
|
133
|
-
// magnitude/angle (PolarSmoother): state.magnitude.* / state.angle.*
|
|
134
|
-
// flat top-level (MessageHandler, Polar): state.*
|
|
135
|
-
function getNotReadyReason(item) {
|
|
136
|
-
if (!item) return 'not available';
|
|
137
|
-
const s = item.state;
|
|
138
|
-
if (!s) return 'not available';
|
|
119
|
+
const computedItemIds = new Set([
|
|
120
|
+
'current.smoothed', 'boatSpeedRefGround', 'speedCorrection',
|
|
121
|
+
'correctedBoatSpeed', 'residual', 'residual.smoothed'
|
|
122
|
+
]);
|
|
139
123
|
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
124
|
+
function getPathStatus(pathState, hasValue, computed = false, computedReady = false) {
|
|
125
|
+
if (computed) {
|
|
126
|
+
return hasValue && computedReady
|
|
127
|
+
? { category: 'computed', reason: '' }
|
|
128
|
+
: { category: 'inactive', reason: 'not computed' };
|
|
129
|
+
}
|
|
130
|
+
if (pathState?.subscribed === false) {
|
|
131
|
+
return { category: 'inactive', reason: 'not subscribed' };
|
|
132
|
+
}
|
|
133
|
+
if (hasValue && pathState?.ready === true) {
|
|
134
|
+
return { category: 'subscribed', reason: '' };
|
|
135
|
+
}
|
|
136
|
+
if (!pathState) {
|
|
137
|
+
return { category: 'problem', reason: 'not available' };
|
|
138
|
+
}
|
|
139
|
+
if (pathState.pathKnown === false) {
|
|
140
|
+
return { category: 'problem', reason: 'path not found in Signal K' };
|
|
141
|
+
}
|
|
142
|
+
if (pathState.hasDelta === false) {
|
|
143
|
+
return { category: 'problem', reason: 'waiting for first data' };
|
|
144
|
+
}
|
|
145
|
+
if (pathState.isStale === true) {
|
|
146
|
+
return { category: 'problem', reason: 'data is stale' };
|
|
147
|
+
}
|
|
148
|
+
return { category: 'problem', reason: 'not available' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getScalarPathState(state) {
|
|
152
|
+
if (!state?.handler) return state;
|
|
153
|
+
return {
|
|
154
|
+
...state,
|
|
155
|
+
subscribed: state.handler.subscribed,
|
|
156
|
+
pathKnown: state.handler.pathKnown,
|
|
157
|
+
hasDelta: state.hasDelta === true && state.handler.hasDelta === true,
|
|
158
|
+
isStale: state.isStale === true || state.handler.isStale === true,
|
|
159
|
+
ready: state.ready === true && state.handler.ready === true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getItemStatuses(item) {
|
|
164
|
+
const label = itemLabel(item);
|
|
165
|
+
const computed = computedItemIds.has(item.id);
|
|
166
|
+
const computedReady = item.state?.ready === true;
|
|
167
|
+
if (state.polarsById[item.id] === item) {
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
label: `${label} magnitude`,
|
|
171
|
+
status: getPathStatus(item.state?.magnitude, Number.isFinite(item.magnitude), computed, computedReady)
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
label: `${label} angle`,
|
|
175
|
+
status: getPathStatus(
|
|
176
|
+
item.state?.angle,
|
|
177
|
+
Number.isFinite(item.angle),
|
|
178
|
+
computed || item.state?.angleFallbackActive === true,
|
|
179
|
+
computedReady
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
];
|
|
151
183
|
}
|
|
152
184
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function getAnyItem(id) {
|
|
161
|
-
return state.polarsById[id] || state.deltasById[id] || state.attitudesById[id] || null;
|
|
185
|
+
const pathState = getScalarPathState(item.state);
|
|
186
|
+
const hasValue = state.attitudesById[item.id] === item
|
|
187
|
+
? Number.isFinite(item.value?.roll) || Number.isFinite(item.value?.pitch)
|
|
188
|
+
: Number.isFinite(item.value);
|
|
189
|
+
return [{ label, status: getPathStatus(pathState, hasValue, computed, computedReady) }];
|
|
162
190
|
}
|
|
163
191
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
if (!el) return;
|
|
167
|
-
el.innerHTML = '';
|
|
168
|
-
const notReady = ids.filter(id => isNotReady(getAnyItem(id)));
|
|
169
|
-
if (notReady.length === 0) return;
|
|
170
|
-
const h = document.createElement('h6');
|
|
171
|
-
h.className = 'text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small';
|
|
172
|
-
h.textContent = 'Warnings';
|
|
173
|
-
el.appendChild(h);
|
|
174
|
-
const ul = document.createElement('ul');
|
|
175
|
-
ul.className = 'list-unstyled text-danger small ps-3';
|
|
176
|
-
notReady.forEach(id => {
|
|
177
|
-
const item = getAnyItem(id);
|
|
178
|
-
const label = metaById[id]?.displayName ?? id;
|
|
179
|
-
const li = document.createElement('li');
|
|
180
|
-
li.textContent = `"${label}" — ${getNotReadyReason(item)}`;
|
|
181
|
-
ul.appendChild(li);
|
|
182
|
-
});
|
|
183
|
-
el.appendChild(ul);
|
|
192
|
+
function isAvailableStatus(status) {
|
|
193
|
+
return status.category === 'subscribed' || status.category === 'computed';
|
|
184
194
|
}
|
|
185
195
|
|
|
186
|
-
function
|
|
196
|
+
function renderWarnings(elId, items) {
|
|
187
197
|
const el = document.getElementById(elId);
|
|
188
198
|
if (!el) return;
|
|
189
199
|
el.innerHTML = '';
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
194
|
-
if (lines.length === 0) return;
|
|
200
|
+
const problems = items.flatMap(item => getItemStatuses(item))
|
|
201
|
+
.filter(({ status }) => status.category === 'problem');
|
|
202
|
+
if (problems.length === 0) return;
|
|
195
203
|
const h = document.createElement('h6');
|
|
196
204
|
h.className = 'text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small';
|
|
197
205
|
h.textContent = 'Warnings';
|
|
198
206
|
el.appendChild(h);
|
|
199
207
|
const ul = document.createElement('ul');
|
|
200
208
|
ul.className = 'list-unstyled text-danger small ps-3';
|
|
201
|
-
|
|
209
|
+
problems.forEach(({ label, status }) => {
|
|
202
210
|
const li = document.createElement('li');
|
|
203
|
-
li.textContent =
|
|
211
|
+
li.textContent = `"${label}" — ${status.reason}`;
|
|
204
212
|
ul.appendChild(li);
|
|
205
213
|
});
|
|
206
214
|
el.appendChild(ul);
|
|
@@ -447,18 +455,24 @@ function renderSettingsPanel() {
|
|
|
447
455
|
|
|
448
456
|
function formatPolarValue(p) {
|
|
449
457
|
if (!p) return '—';
|
|
458
|
+
const statuses = getItemStatuses(p);
|
|
450
459
|
const m = metaById[p.id];
|
|
451
460
|
const speedC = buildConverter(m?.magnitude?.displayUnits) || DEFAULTS.speed;
|
|
452
461
|
const angleC = buildConverter(m?.angle?.displayUnits) || DEFAULTS.angle;
|
|
453
|
-
const spd =
|
|
454
|
-
|
|
455
|
-
|
|
462
|
+
const spd = isAvailableStatus(statuses[0].status)
|
|
463
|
+
? `${speedC.convert(p.magnitude).toFixed(speedC.decimals)} ${speedC.symbol}`
|
|
464
|
+
: statuses[0].status.reason;
|
|
465
|
+
const ang = isAvailableStatus(statuses[1].status)
|
|
466
|
+
? `${angleC.convert(p.angle).toFixed(angleC.decimals)}${angleC.symbol}`
|
|
467
|
+
: statuses[1].status.reason;
|
|
468
|
+
const sigma = (isAvailableStatus(statuses[0].status) && config?.showStatistics && p.id.endsWith('.smoothed') && typeof p.trace === 'number')
|
|
456
469
|
? ` (σ=${speedC.convert(Math.sqrt(p.trace)).toFixed(speedC.decimals + 2)})` : '';
|
|
457
|
-
return `${spd}
|
|
470
|
+
return `${spd}${sigma} / ${ang}`;
|
|
458
471
|
}
|
|
459
472
|
|
|
460
473
|
function formatDeltaValue(d) {
|
|
461
|
-
|
|
474
|
+
const status = getItemStatuses(d)[0].status;
|
|
475
|
+
if (!isAvailableStatus(status)) return status.reason;
|
|
462
476
|
const m = metaById[d.id];
|
|
463
477
|
const uc = buildConverter(m?.displayUnits)
|
|
464
478
|
|| (m?.units === 'm/s' ? DEFAULTS.speed : DEFAULTS.angle);
|
|
@@ -468,6 +482,8 @@ function formatDeltaValue(d) {
|
|
|
468
482
|
}
|
|
469
483
|
|
|
470
484
|
function formatAttitudeValue(a) {
|
|
485
|
+
const status = getItemStatuses(a)[0].status;
|
|
486
|
+
if (!isAvailableStatus(status)) return status.reason;
|
|
471
487
|
const v = (a && a.value) || {};
|
|
472
488
|
const variance = a && a.variance;
|
|
473
489
|
const m = metaById[a?.id];
|
|
@@ -490,7 +506,6 @@ function buildDataTable(rows) {
|
|
|
490
506
|
const tbody = document.createElement('tbody');
|
|
491
507
|
rows.forEach(row => {
|
|
492
508
|
const tr = document.createElement('tr');
|
|
493
|
-
if (row.stale) tr.className = 'stale';
|
|
494
509
|
const tdL = document.createElement('td'); tdL.textContent = row.label;
|
|
495
510
|
const tdV = document.createElement('td'); tdV.textContent = row.value;
|
|
496
511
|
tr.appendChild(tdL); tr.appendChild(tdV);
|
|
@@ -518,9 +533,9 @@ function renderGroupInto(elId, polars, deltas, attitudes) {
|
|
|
518
533
|
if (!el) return;
|
|
519
534
|
el.innerHTML = '';
|
|
520
535
|
const rows = [
|
|
521
|
-
...polars .map(p => ({ label: itemLabel(p), value: formatPolarValue(p)
|
|
522
|
-
...deltas .map(d => ({ label: itemLabel(d), value: formatDeltaValue(d)
|
|
523
|
-
...attitudes.map(a => ({ label: itemLabel(a), value: formatAttitudeValue(a)
|
|
536
|
+
...polars .map(p => ({ label: itemLabel(p), value: formatPolarValue(p) })),
|
|
537
|
+
...deltas .map(d => ({ label: itemLabel(d), value: formatDeltaValue(d) })),
|
|
538
|
+
...attitudes.map(a => ({ label: itemLabel(a), value: formatAttitudeValue(a) }))
|
|
524
539
|
];
|
|
525
540
|
if (rows.length) el.appendChild(buildDataTable(rows));
|
|
526
541
|
}
|
|
@@ -533,6 +548,7 @@ function renderLiveSections() {
|
|
|
533
548
|
const fallbackInputPolars = inputPolars.length ? inputPolars : filterById(state.polarsAll, ['groundSpeed.smoothed']);
|
|
534
549
|
const fallbackInputDeltas = inputDeltas.length ? inputDeltas : filterById(state.deltasAll, ['heading.smoothed', 'boatSpeed.smoothed']);
|
|
535
550
|
const fallbackInputAttitudes = inputAttitudes.length ? inputAttitudes : filterById(state.attitudesAll, ['attitude.smoothed']);
|
|
551
|
+
const displayedInputs = [...fallbackInputPolars, ...fallbackInputDeltas, ...fallbackInputAttitudes];
|
|
536
552
|
|
|
537
553
|
// Inputs section — raw sensor readings only (smoothing is internal to the plugin)
|
|
538
554
|
renderGroupInto('inputs-values',
|
|
@@ -540,43 +556,44 @@ function renderLiveSections() {
|
|
|
540
556
|
fallbackInputDeltas,
|
|
541
557
|
fallbackInputAttitudes
|
|
542
558
|
);
|
|
543
|
-
|
|
544
|
-
if (config && config.assumeCurrent) inputWarningIds.push('current.smoothed');
|
|
545
|
-
renderInputWarnings('inputs-warnings', inputWarningIds);
|
|
559
|
+
renderWarnings('inputs-warnings', displayedInputs);
|
|
546
560
|
|
|
547
561
|
// Estimation — inputs (raw sensor data used for boat speed estimation)
|
|
562
|
+
const estimationInputs = [
|
|
563
|
+
...filterById(state.polarsAll, ['groundSpeed']),
|
|
564
|
+
...filterById(state.deltasAll, ['heading.angle', 'boatSpeed']),
|
|
565
|
+
...filterById(state.attitudesAll, ['attitude'])
|
|
566
|
+
];
|
|
548
567
|
renderGroupInto('estimation-inputs',
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
568
|
+
estimationInputs.filter(item => state.polarsById[item.id] === item),
|
|
569
|
+
estimationInputs.filter(item => state.deltasById[item.id] === item),
|
|
570
|
+
estimationInputs.filter(item => state.attitudesById[item.id] === item)
|
|
552
571
|
);
|
|
553
572
|
// Estimation — intermediates
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
[], []
|
|
557
|
-
);
|
|
573
|
+
const estimationIntermediates = filterById(state.polarsAll, ['boatSpeedRefGround', 'speedCorrection', 'residual', 'residual.smoothed']);
|
|
574
|
+
renderGroupInto('estimation-intermediates', estimationIntermediates, [], []);
|
|
558
575
|
// Estimation — outputs
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
[], []
|
|
562
|
-
);
|
|
576
|
+
const estimationOutputs = filterById(state.polarsAll, ['correctedBoatSpeed', 'current.smoothed']);
|
|
577
|
+
renderGroupInto('estimation-outputs', estimationOutputs, [], []);
|
|
563
578
|
// Estimation — warnings
|
|
564
|
-
|
|
565
|
-
if (estimationWarnings) estimationWarnings.innerHTML = '';
|
|
579
|
+
renderWarnings('estimation-warnings', [...estimationInputs, ...estimationIntermediates, ...estimationOutputs]);
|
|
566
580
|
|
|
567
581
|
// Learning — inputs: smoothed sensors + current if assumeCurrent
|
|
568
582
|
const learningCurrentPolars = (config && config.assumeCurrent)
|
|
569
583
|
? filterById(state.polarsAll, ['current.smoothed'])
|
|
570
584
|
: [];
|
|
585
|
+
const learningPolars = [...filterById(state.polarsAll, ['groundSpeed.smoothed']), ...learningCurrentPolars];
|
|
586
|
+
const learningDeltas = filterById(state.deltasAll, ['heading.smoothed', 'boatSpeed.smoothed']);
|
|
587
|
+
const learningAttitudes = filterById(state.attitudesAll, ['attitude.smoothed']);
|
|
571
588
|
renderGroupInto('learning-inputs',
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
589
|
+
learningPolars,
|
|
590
|
+
learningDeltas,
|
|
591
|
+
learningAttitudes
|
|
575
592
|
);
|
|
576
593
|
// Learning — warnings
|
|
577
594
|
const learningWarnings = document.getElementById('learning-warnings');
|
|
595
|
+
renderWarnings('learning-warnings', [...learningPolars, ...learningDeltas, ...learningAttitudes]);
|
|
578
596
|
if (learningWarnings) {
|
|
579
|
-
learningWarnings.innerHTML = '';
|
|
580
597
|
const navState = learningState?.navigationState;
|
|
581
598
|
if (navState?.enabled && navState.pathKnown === false) {
|
|
582
599
|
const ul = document.createElement('ul');
|
|
@@ -675,7 +692,6 @@ async function tick() {
|
|
|
675
692
|
.then(r => r.ok ? r.json() : null)
|
|
676
693
|
.catch(() => null);
|
|
677
694
|
_pluginStatus = statusData?.status ?? '';
|
|
678
|
-
lifecycleWarnings = Array.isArray(statusData?.lifecycleWarnings) ? statusData.lifecycleWarnings : [];
|
|
679
695
|
_refreshMessage();
|
|
680
696
|
}
|
|
681
697
|
|