signalk-polar-performance-plugin 1.3.3 → 2.0.0
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/CHANGELOG.md +25 -1
- package/README.md +41 -86
- package/docs/developer-reference.md +22 -130
- package/openApi.json +12 -798
- package/package.json +13 -7
- package/plugin/index.js +178 -521
- package/public/app.js +53 -638
- package/public/index.html +0 -2
- package/public/plotter.html +20 -106
- package/test/lifecycle.test.js +6 -1
- package/docs/import.png +0 -0
- package/docs/polar.png +0 -0
- package/docs/polar_management.png +0 -0
- package/plugin/PolarFileStore.js +0 -133
- package/plugin/PolarTable.js +0 -897
- package/plugin/SI.js +0 -21
- package/plugin/import/ExpeditionImporter.js +0 -152
- package/plugin/import/FormatRegistry.js +0 -21
- package/plugin/import/ImportService.js +0 -199
- package/plugin/import/JieterImporter.js +0 -18
- package/plugin/import/OrcSource.js +0 -396
- package/plugin/import/SourceRegistry.js +0 -17
- package/plugin/import/canonical.js +0 -73
- package/plugin/import/matrixText.js +0 -133
- package/plugin/index.js.bak +0 -1003
- package/test/PolarFileStore.test.js +0 -216
- package/test/PolarTable.test.js +0 -349
- package/test/api.test.js +0 -502
- package/test/migration.test.js +0 -164
package/plugin/index.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const PolarFileStore = require('./PolarFileStore')
|
|
5
|
-
const { ImportService, ImportError, createTimestampedId } = require('./import/ImportService')
|
|
6
|
-
const canonical = require('./import/canonical')
|
|
7
|
-
const { parseMatrixPolarText } = require('./import/matrixText')
|
|
3
|
+
const { Polar } = require('polar-math')
|
|
8
4
|
const {
|
|
5
|
+
MessageHandler,
|
|
9
6
|
createSmoothedPolar,
|
|
10
7
|
createSmoothedHandler,
|
|
11
8
|
SmoothedAngle,
|
|
@@ -15,14 +12,12 @@ const {
|
|
|
15
12
|
KalmanSmoother
|
|
16
13
|
} = require('signalkutilities')
|
|
17
14
|
|
|
18
|
-
const CURRENT_SETTINGS_VERSION =
|
|
15
|
+
const CURRENT_SETTINGS_VERSION = 2
|
|
19
16
|
|
|
20
17
|
const STALE_RESUBSCRIBE_PERIOD = 60000 // ms — idle period before live input subscriptions are re-established
|
|
21
18
|
|
|
22
19
|
const DEFAULT_SETTINGS = {
|
|
23
20
|
settingsVersion: CURRENT_SETTINGS_VERSION,
|
|
24
|
-
activePolar: '',
|
|
25
|
-
perfAdjust: 1,
|
|
26
21
|
showAllTwsLines: true,
|
|
27
22
|
smootherType: 'Exponential',
|
|
28
23
|
smootherParamExponential: 1,
|
|
@@ -47,9 +42,17 @@ module.exports = (app) => {
|
|
|
47
42
|
let changedOptions = {} // staged but not yet applied
|
|
48
43
|
let hasPendingChanges = false
|
|
49
44
|
let isRunning = false
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
|
|
46
|
+
// Active polar — sourced from the `polars.activePolar` / `polars.performanceFactor`
|
|
47
|
+
// SK paths (published by a 'polars' resource-provider plugin, e.g. signalk-polar-management).
|
|
48
|
+
// This plugin no longer stores or manages polar files itself.
|
|
49
|
+
let polar = null // polar-math Polar instance, or null when unavailable
|
|
50
|
+
let activePolarId = null
|
|
51
|
+
let activePolarDoc = null // raw canonical resource, for descriptive metadata
|
|
52
|
+
let performanceFactor = 1
|
|
53
|
+
let activePolarHandler = null
|
|
54
|
+
let performanceFactorHandler = null
|
|
55
|
+
|
|
53
56
|
let windSmoother = null
|
|
54
57
|
let bspSmoother = null
|
|
55
58
|
let hdgSmoother = null
|
|
@@ -158,39 +161,92 @@ module.exports = (app) => {
|
|
|
158
161
|
delete s.useTWSsource
|
|
159
162
|
delete s.useSOGsource
|
|
160
163
|
delete s.trueWindSpeedPath
|
|
161
|
-
|
|
162
|
-
// Migrate embedded CSV polar to a canonical polar file.
|
|
163
|
-
// Only advance settingsVersion if the CSV is successfully written —
|
|
164
|
-
// if it fails, keep csvTable so the migration retries on next start.
|
|
165
|
-
if (s.csvTable && s.csvTable.trim()) {
|
|
166
|
-
try {
|
|
167
|
-
const { resource } = parseMatrixPolarText(s.csvTable)
|
|
168
|
-
store.saveCanonical('migrated-polar', resource)
|
|
169
|
-
s.activePolar = 'migrated-polar'
|
|
170
|
-
delete s.csvTable
|
|
171
|
-
app.debug('Legacy csvTable migrated to migrated-polar.json')
|
|
172
|
-
} catch (e) {
|
|
173
|
-
app.setPluginError('Migration failed — could not save legacy CSV: ' + e.message)
|
|
174
|
-
app.debug('CSV migration error: %s', e.message)
|
|
175
|
-
return s // abort; csvTable kept so next start retries
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
delete s.csvTable
|
|
179
|
-
}
|
|
180
|
-
|
|
164
|
+
delete s.csvTable
|
|
181
165
|
s.settingsVersion = 1
|
|
182
166
|
app.debug('Settings migrated from v0 to v1')
|
|
183
167
|
}
|
|
184
168
|
|
|
185
|
-
|
|
169
|
+
if (s.settingsVersion < 2) {
|
|
170
|
+
// v1 → v2: polar selection and performance adjustment moved to the
|
|
171
|
+
// `polars.activePolar` / `polars.performanceFactor` SK paths, published
|
|
172
|
+
// by a 'polars' resource-provider plugin (e.g. signalk-polar-management).
|
|
173
|
+
delete s.activePolar
|
|
174
|
+
delete s.perfAdjust
|
|
175
|
+
s.settingsVersion = 2
|
|
176
|
+
app.debug('Settings migrated from v1 to v2')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Persist if any migration ran, so migrations don't repeat on next start.
|
|
180
|
+
// Signal K requires a callback; omitting it throws TypeError and aborts start().
|
|
186
181
|
if ((s.settingsVersion ?? 0) > version) {
|
|
187
|
-
app.savePluginOptions(s)
|
|
188
|
-
|
|
182
|
+
app.savePluginOptions(s, (err) => {
|
|
183
|
+
if (err) {
|
|
184
|
+
app.error('Failed to save migrated settings: ' + err.message)
|
|
185
|
+
} else {
|
|
186
|
+
app.debug('Migrated settings saved (v%d → v%d)', version, s.settingsVersion)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
return s
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Active polar — sourced from SK paths published by a 'polars' resource provider
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
// Extracts the resource id from an `href` of the shape `/resources/polars/<id>`.
|
|
199
|
+
function hrefToId(href) {
|
|
200
|
+
if (typeof href !== 'string') return null
|
|
201
|
+
const match = href.match(/\/resources\/polars\/([^/]+)$/)
|
|
202
|
+
return match ? match[1] : null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function checkPolarProvider() {
|
|
206
|
+
if (!app.resourcesApi || typeof app.resourcesApi.listResources !== 'function') return false
|
|
207
|
+
try {
|
|
208
|
+
await app.resourcesApi.listResources('polars', {})
|
|
209
|
+
return true
|
|
210
|
+
} catch (_e) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function handleActivePolarDelta() {
|
|
216
|
+
const value = activePolarHandler.value
|
|
217
|
+
const href = value?.href
|
|
218
|
+
if (!href) {
|
|
219
|
+
polar = null
|
|
220
|
+
activePolarId = null
|
|
221
|
+
activePolarDoc = null
|
|
222
|
+
nullifyOutputs()
|
|
223
|
+
app.setPluginStatus('No active polar selected — select one in the polar management webapp')
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const id = hrefToId(href)
|
|
228
|
+
if (!id) {
|
|
229
|
+
app.setPluginError(`Cannot parse active polar href: ${href}`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
app.resourcesApi.getResource('polars', id).then((doc) => {
|
|
234
|
+
try {
|
|
235
|
+
polar = Polar.fromTable(doc)
|
|
236
|
+
activePolarId = id
|
|
237
|
+
activePolarDoc = doc
|
|
238
|
+
app.setPluginStatus(`Polar '${doc.name || id}' loaded`)
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// Keep the last valid polar (if any) rather than dropping outputs on a bad update.
|
|
241
|
+
app.setPluginError(`Invalid active polar '${id}': ${e.message}`)
|
|
242
|
+
if (!polar) nullifyOutputs()
|
|
243
|
+
}
|
|
244
|
+
}).catch((e) => {
|
|
245
|
+
app.setPluginError(`Cannot load active polar '${id}': ${e.message}`)
|
|
246
|
+
if (!polar) nullifyOutputs()
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
194
250
|
// ---------------------------------------------------------------------------
|
|
195
251
|
// Hot-apply runtime option changes
|
|
196
252
|
// ---------------------------------------------------------------------------
|
|
@@ -205,25 +261,6 @@ module.exports = (app) => {
|
|
|
205
261
|
changedOptions = {}
|
|
206
262
|
hasPendingChanges = false
|
|
207
263
|
|
|
208
|
-
// Active polar — load new table
|
|
209
|
-
if ('activePolar' in settings || 'perfAdjust' in settings) {
|
|
210
|
-
if (keys.includes('activePolar') && settings.activePolar) {
|
|
211
|
-
try {
|
|
212
|
-
polarTable = store.load(settings.activePolar)
|
|
213
|
-
polarTable.setPerformanceAdjustment(settings.perfAdjust || 1)
|
|
214
|
-
app.setPluginStatus(`Polar '${settings.activePolar}' loaded`)
|
|
215
|
-
} catch (e) {
|
|
216
|
-
app.setPluginError(`Cannot load polar '${settings.activePolar}': ${e.message}`)
|
|
217
|
-
}
|
|
218
|
-
} else if (keys.includes('activePolar') && !settings.activePolar) {
|
|
219
|
-
polarTable = null
|
|
220
|
-
nullifyOutputs()
|
|
221
|
-
app.setPluginStatus('No polar configured — set activePolar or upload a CSV file')
|
|
222
|
-
} else if (keys.includes('perfAdjust') && polarTable) {
|
|
223
|
-
polarTable.setPerformanceAdjustment(settings.perfAdjust)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
264
|
// Smoother type or parameter changes — update all running smoothers in-place
|
|
228
265
|
const SMOOTHER_KEYS = ['smootherType', 'smootherParamExponential', 'smootherParamMovingAverage', 'smootherParamKalman']
|
|
229
266
|
if (keys.some(k => SMOOTHER_KEYS.includes(k))) {
|
|
@@ -297,7 +334,7 @@ module.exports = (app) => {
|
|
|
297
334
|
|
|
298
335
|
function computeAndSend() {
|
|
299
336
|
if (hasPendingChanges) applyOptionChanges()
|
|
300
|
-
if (!
|
|
337
|
+
if (!polar) return
|
|
301
338
|
|
|
302
339
|
const wind = windSmoother.polarValue
|
|
303
340
|
const TWS = wind.magnitude
|
|
@@ -333,10 +370,11 @@ module.exports = (app) => {
|
|
|
333
370
|
|
|
334
371
|
// Polar lookups
|
|
335
372
|
const isUpwind = TWA < Math.PI / 2
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
const
|
|
373
|
+
const { value: targets } = polar.targetsAt({ tws: TWS, performanceFactor })
|
|
374
|
+
const beatAngle = targets?.beat?.twa ?? null
|
|
375
|
+
const runAngle = targets?.run?.twa ?? null
|
|
376
|
+
const beatVMG = targets?.beat?.vmg ?? null
|
|
377
|
+
const runVMG = targets?.run?.vmg ?? null
|
|
340
378
|
const targetAngle = isUpwind ? beatAngle : runAngle
|
|
341
379
|
const targetVMG = isUpwind ? beatVMG : runVMG
|
|
342
380
|
|
|
@@ -396,7 +434,7 @@ module.exports = (app) => {
|
|
|
396
434
|
}
|
|
397
435
|
|
|
398
436
|
// Polar speed and performance ratios
|
|
399
|
-
const polarSpeed =
|
|
437
|
+
const { value: polarSpeed } = polar.speedAt({ tws: TWS, twa: TWA, performanceFactor })
|
|
400
438
|
if (Number.isFinite(polarSpeed) && polarSpeed > 0) {
|
|
401
439
|
if (settings.polarSpeed) {
|
|
402
440
|
add('performance.polarSpeed', polarSpeed, 'm/s',
|
|
@@ -441,8 +479,8 @@ module.exports = (app) => {
|
|
|
441
479
|
|
|
442
480
|
// Max speed for current TWS
|
|
443
481
|
if (settings.maxSpeed) {
|
|
444
|
-
const maxSpeed =
|
|
445
|
-
const maxSpeedAngle =
|
|
482
|
+
const maxSpeed = targets?.maxSpeed?.speed ?? null
|
|
483
|
+
const maxSpeedAngle = targets?.maxSpeed?.twa ?? null
|
|
446
484
|
if (Number.isFinite(maxSpeed)) {
|
|
447
485
|
add('performance.maxSpeed', maxSpeed, 'm/s',
|
|
448
486
|
'Maximum polar boat speed for current TWS.')
|
|
@@ -485,7 +523,7 @@ module.exports = (app) => {
|
|
|
485
523
|
|
|
486
524
|
schema: () => ({
|
|
487
525
|
type: "object",
|
|
488
|
-
description: "The plugin is configured through its webapp. Open it from the Signal K app list (Webapps →
|
|
526
|
+
description: "The plugin is configured through its webapp. Open it from the Signal K app list (Webapps → Polar Performance) to set sources, enable corrections and adjust parameters. Select the active polar and performance factor in the Polar Management webapp.",
|
|
489
527
|
properties: {}
|
|
490
528
|
}),
|
|
491
529
|
|
|
@@ -497,76 +535,24 @@ module.exports = (app) => {
|
|
|
497
535
|
registerWithRouter(router) {
|
|
498
536
|
app.debug('registerWithRouter')
|
|
499
537
|
|
|
500
|
-
function getStore() {
|
|
501
|
-
if (!store) {
|
|
502
|
-
store = new PolarFileStore(app.getDataDirPath())
|
|
503
|
-
}
|
|
504
|
-
return store
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function loadStoredPolar(id) {
|
|
508
|
-
return getStore().load(id)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Size-1 cache: avoids reloading from disk when consecutive query requests
|
|
512
|
-
// target the same polar ID (e.g. the N parallel curve fetches on startup).
|
|
513
|
-
// perfAdjust is applied fresh on every use so it is never part of the key.
|
|
514
|
-
let cachedPolar = null // { id, table } | null
|
|
515
|
-
|
|
516
|
-
function loadPolarCached(id) {
|
|
517
|
-
if (!cachedPolar || cachedPolar.id !== id) {
|
|
518
|
-
cachedPolar = { id, table: loadStoredPolar(id) }
|
|
519
|
-
}
|
|
520
|
-
cachedPolar.table.setPerformanceAdjustment(settings.perfAdjust || 1)
|
|
521
|
-
return cachedPolar.table
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function invalidatePolarCache(id) {
|
|
525
|
-
if (cachedPolar && cachedPolar.id === id) cachedPolar = null
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function getImportService() {
|
|
529
|
-
if (!importService) {
|
|
530
|
-
importService = new ImportService(getStore())
|
|
531
|
-
}
|
|
532
|
-
return importService
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Lightweight internet connectivity probe.
|
|
536
|
-
// Attempts a HEAD request to Cloudflare's public DNS (1.1.1.1) with a
|
|
537
|
-
// 3-second timeout. Returns true if any HTTP response is received,
|
|
538
|
-
// false on network error or timeout.
|
|
539
|
-
async function checkInternet() {
|
|
540
|
-
const TIMEOUT_MS = 3000
|
|
541
|
-
try {
|
|
542
|
-
const controller = new AbortController()
|
|
543
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
544
|
-
const response = await fetch('https://1.1.1.1', { method: 'HEAD', signal: controller.signal })
|
|
545
|
-
.finally(() => clearTimeout(timer))
|
|
546
|
-
return response.status < 600
|
|
547
|
-
} catch (_) {
|
|
548
|
-
return false
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
538
|
function errorStatus(error) {
|
|
553
|
-
return /
|
|
539
|
+
return /No active polar/i.test(error.message) ? 404 : 500
|
|
554
540
|
}
|
|
555
541
|
|
|
556
542
|
function toFixedNumber(value, digits) {
|
|
557
543
|
return Number.isFinite(value) ? parseFloat(value.toFixed(digits)) : null
|
|
558
544
|
}
|
|
559
545
|
|
|
560
|
-
function getPolarTwsValues(
|
|
561
|
-
return
|
|
546
|
+
function getPolarTwsValues() {
|
|
547
|
+
return polar.entries
|
|
562
548
|
.filter(entry => entry.tws > 0.001)
|
|
563
549
|
.map(entry => parseFloat(entry.tws.toFixed(4)))
|
|
564
550
|
}
|
|
565
551
|
|
|
566
|
-
function buildCurveResult(
|
|
552
|
+
function buildCurveResult(tws, stepRad) {
|
|
567
553
|
const points = []
|
|
568
554
|
for (let twa = 0; twa <= Math.PI + 1e-9; twa += stepRad) {
|
|
569
|
-
const tbs =
|
|
555
|
+
const { value: tbs } = polar.speedAt({ tws, twa, performanceFactor })
|
|
570
556
|
if (Number.isFinite(tbs) && tbs > 0) {
|
|
571
557
|
points.push({
|
|
572
558
|
twa: toFixedNumber(twa, 5),
|
|
@@ -575,252 +561,38 @@ module.exports = (app) => {
|
|
|
575
561
|
}
|
|
576
562
|
}
|
|
577
563
|
|
|
578
|
-
const
|
|
579
|
-
const beatVMG = table.getBeatVMG(tws)
|
|
580
|
-
const runAngle = table.getRunAngle(tws)
|
|
581
|
-
const runVMG = table.getRunVMG(tws)
|
|
582
|
-
const beatTbs = Number.isFinite(beatAngle) ? table.getBoatSpeed(tws, beatAngle) : null
|
|
583
|
-
const runTbs = Number.isFinite(runAngle) ? table.getBoatSpeed(tws, runAngle) : null
|
|
584
|
-
|
|
564
|
+
const { value: targets } = polar.targetsAt({ tws, performanceFactor })
|
|
585
565
|
return {
|
|
586
566
|
tws,
|
|
587
567
|
points,
|
|
588
|
-
beat:
|
|
589
|
-
twa: toFixedNumber(
|
|
590
|
-
tbs: toFixedNumber(
|
|
591
|
-
vmg: toFixedNumber(
|
|
568
|
+
beat: targets?.beat ? {
|
|
569
|
+
twa: toFixedNumber(targets.beat.twa, 5),
|
|
570
|
+
tbs: toFixedNumber(targets.beat.speed, 4),
|
|
571
|
+
vmg: toFixedNumber(targets.beat.vmg, 4)
|
|
592
572
|
} : null,
|
|
593
|
-
run:
|
|
594
|
-
twa: toFixedNumber(
|
|
595
|
-
tbs: toFixedNumber(
|
|
596
|
-
vmg: toFixedNumber(
|
|
573
|
+
run: targets?.run ? {
|
|
574
|
+
twa: toFixedNumber(targets.run.twa, 5),
|
|
575
|
+
tbs: toFixedNumber(targets.run.speed, 4),
|
|
576
|
+
vmg: toFixedNumber(targets.run.vmg, 4)
|
|
597
577
|
} : null
|
|
598
578
|
}
|
|
599
579
|
}
|
|
600
580
|
|
|
601
|
-
function buildSpeedResult(table, tws, twa) {
|
|
602
|
-
const tbs = table.getBoatSpeed(tws, twa)
|
|
603
|
-
return {
|
|
604
|
-
tws,
|
|
605
|
-
twa,
|
|
606
|
-
tbs: toFixedNumber(tbs, 4),
|
|
607
|
-
state: table.getInterpolationState(tws, twa)
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function buildTargetsResult(table, tws) {
|
|
612
|
-
const beatAngle = table.getBeatAngle(tws)
|
|
613
|
-
const runAngle = table.getRunAngle(tws)
|
|
614
|
-
const beatVMG = table.getBeatVMG(tws)
|
|
615
|
-
const runVMG = table.getRunVMG(tws)
|
|
616
|
-
const beatTbs = Number.isFinite(beatAngle) ? table.getBoatSpeed(tws, beatAngle) : null
|
|
617
|
-
const runTbs = Number.isFinite(runAngle) ? table.getBoatSpeed(tws, runAngle) : null
|
|
618
|
-
|
|
619
|
-
return {
|
|
620
|
-
tws,
|
|
621
|
-
beat: (Number.isFinite(beatAngle) && Number.isFinite(beatTbs)) ? {
|
|
622
|
-
twa: toFixedNumber(beatAngle, 5),
|
|
623
|
-
tbs: toFixedNumber(beatTbs, 4),
|
|
624
|
-
vmg: toFixedNumber(beatVMG, 4)
|
|
625
|
-
} : null,
|
|
626
|
-
run: (Number.isFinite(runAngle) && Number.isFinite(runTbs)) ? {
|
|
627
|
-
twa: toFixedNumber(runAngle, 5),
|
|
628
|
-
tbs: toFixedNumber(runTbs, 4),
|
|
629
|
-
vmg: toFixedNumber(runVMG, 4)
|
|
630
|
-
} : null
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function buildPerformanceMetric(polarValue, actualValue) {
|
|
635
|
-
if (!Number.isFinite(polarValue) || polarValue <= 0 || !Number.isFinite(actualValue)) {
|
|
636
|
-
return null
|
|
637
|
-
}
|
|
638
|
-
return {
|
|
639
|
-
polar: toFixedNumber(polarValue, 4),
|
|
640
|
-
actual: toFixedNumber(actualValue, 4),
|
|
641
|
-
ratio: toFixedNumber(actualValue / polarValue, 5)
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function buildPerformanceResult(table, tws, twa, bsp) {
|
|
646
|
-
const polarSpeed = table.getBoatSpeed(tws, twa)
|
|
647
|
-
const upwindLimit = Math.PI / 3
|
|
648
|
-
const downwindLimit = 2 * Math.PI / 3
|
|
649
|
-
|
|
650
|
-
let direction = 'reaching'
|
|
651
|
-
let polarVmg = null
|
|
652
|
-
let actualVmg = null
|
|
653
|
-
if (twa < upwindLimit) {
|
|
654
|
-
direction = 'upwind'
|
|
655
|
-
polarVmg = table.getBeatVMG(tws)
|
|
656
|
-
actualVmg = bsp * Math.cos(twa)
|
|
657
|
-
} else if (twa > downwindLimit) {
|
|
658
|
-
direction = 'downwind'
|
|
659
|
-
polarVmg = table.getRunVMG(tws)
|
|
660
|
-
actualVmg = Math.abs(bsp * Math.cos(twa))
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return {
|
|
664
|
-
tws,
|
|
665
|
-
twa,
|
|
666
|
-
bsp,
|
|
667
|
-
direction,
|
|
668
|
-
speed: buildPerformanceMetric(polarSpeed, bsp),
|
|
669
|
-
vmg: direction === 'reaching' ? null : buildPerformanceMetric(polarVmg, actualVmg)
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function validatePolarResourceBody(resource) {
|
|
674
|
-
return canonical.validateCanonicalPolarResourceBody(resource)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function generateCanonicalPolarId(resource) {
|
|
678
|
-
return createTimestampedId(
|
|
679
|
-
resource?.sailnumber || resource?.name || 'polar',
|
|
680
|
-
'polar',
|
|
681
|
-
(candidate) => getStore().exists(candidate),
|
|
682
|
-
Date.now
|
|
683
|
-
)
|
|
684
|
-
}
|
|
685
|
-
|
|
686
581
|
router.use((req, res, next) => {
|
|
687
582
|
res.set('Origin-Agent-Cluster', '?1')
|
|
688
583
|
next()
|
|
689
584
|
})
|
|
690
585
|
|
|
691
|
-
|
|
692
|
-
if (!settings.activePolar) {
|
|
693
|
-
return res.status(404).json({ error: 'No polar is currently active' })
|
|
694
|
-
}
|
|
695
|
-
res.json({ id: settings.activePolar })
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
router.put('/polars/active', (req, res) => {
|
|
699
|
-
const id = req.body?.id
|
|
700
|
-
if (typeof id !== 'string') {
|
|
701
|
-
return res.status(400).json({ error: "Expected JSON body with string 'id'" })
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (!id) {
|
|
705
|
-
polarTable = null
|
|
706
|
-
settings.activePolar = ''
|
|
707
|
-
app.savePluginOptions(settings, (err) => {
|
|
708
|
-
if (err) app.error('Failed to save settings: ' + err.message)
|
|
709
|
-
})
|
|
710
|
-
nullifyOutputs()
|
|
711
|
-
app.setPluginStatus('No polar configured — set activePolar or upload a CSV file')
|
|
712
|
-
return res.json({ id: '' })
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
try {
|
|
716
|
-
polarTable = loadStoredPolar(id)
|
|
717
|
-
polarTable.setPerformanceAdjustment(settings.perfAdjust || 1)
|
|
718
|
-
settings.activePolar = id
|
|
719
|
-
app.savePluginOptions(settings, (err) => {
|
|
720
|
-
if (err) app.error('Failed to save settings: ' + err.message)
|
|
721
|
-
})
|
|
722
|
-
app.setPluginStatus(`Polar '${id}' loaded`)
|
|
723
|
-
res.json({ id })
|
|
724
|
-
} catch (e) {
|
|
725
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
726
|
-
}
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
router.delete('/polars/active', (_req, res) => {
|
|
730
|
-
polarTable = null
|
|
731
|
-
settings.activePolar = ''
|
|
732
|
-
app.savePluginOptions(settings, (err) => {
|
|
733
|
-
if (err) app.error('Failed to save settings: ' + err.message)
|
|
734
|
-
})
|
|
735
|
-
nullifyOutputs()
|
|
736
|
-
app.setPluginStatus('No polar configured — set activePolar or upload a CSV file')
|
|
737
|
-
res.json({ id: '' })
|
|
738
|
-
})
|
|
739
|
-
|
|
740
|
-
router.get('/imports/formats', (_req, res) => {
|
|
741
|
-
res.json(getImportService().listFormats())
|
|
742
|
-
})
|
|
743
|
-
|
|
744
|
-
router.get('/internet', async (_req, res) => {
|
|
745
|
-
res.json({ online: await checkInternet() })
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
router.get('/imports/sources', (_req, res) => {
|
|
749
|
-
res.json(getImportService().listSources())
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
router.post('/polars', (req, res) => {
|
|
753
|
-
const validationError = validatePolarResourceBody(req.body)
|
|
754
|
-
if (validationError) {
|
|
755
|
-
return res.status(400).json({ error: validationError })
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
try {
|
|
759
|
-
const id = generateCanonicalPolarId(req.body)
|
|
760
|
-
getStore().saveCanonical(id, {
|
|
761
|
-
...req.body,
|
|
762
|
-
name: req.body.name || id
|
|
763
|
-
})
|
|
764
|
-
res.status(201).json({ id })
|
|
765
|
-
} catch (e) {
|
|
766
|
-
res.status(500).json({ error: e.message })
|
|
767
|
-
}
|
|
768
|
-
})
|
|
586
|
+
// ---- Active polar curve queries (read-only — for the webapp's polar canvas) --
|
|
769
587
|
|
|
770
|
-
router.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
res.status(201).json({ id: result.id })
|
|
774
|
-
} catch (error) {
|
|
775
|
-
if (error instanceof ImportError) {
|
|
776
|
-
return res.status(error.status).json({ error: error.message })
|
|
777
|
-
}
|
|
778
|
-
res.status(500).json({ error: error.message })
|
|
779
|
-
}
|
|
588
|
+
router.get('/polar/axes/tws', (req, res) => {
|
|
589
|
+
if (!polar) return res.status(404).json({ error: 'No active polar selected' })
|
|
590
|
+
res.json(getPolarTwsValues())
|
|
780
591
|
})
|
|
781
592
|
|
|
782
|
-
router.get('/
|
|
783
|
-
if (!
|
|
784
|
-
return res.status(503).json({ error: 'No internet connection — external source imports are unavailable' })
|
|
785
|
-
}
|
|
593
|
+
router.get('/polar/queries/curve', (req, res) => {
|
|
594
|
+
if (!polar) return res.status(404).json({ error: 'No active polar selected' })
|
|
786
595
|
try {
|
|
787
|
-
const results = await getImportService().searchSource(req.params.source, req.query?.q)
|
|
788
|
-
res.json(results)
|
|
789
|
-
} catch (error) {
|
|
790
|
-
if (error instanceof ImportError) {
|
|
791
|
-
return res.status(error.status).json({ error: error.message })
|
|
792
|
-
}
|
|
793
|
-
res.status(500).json({ error: error.message })
|
|
794
|
-
}
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
router.post('/imports/sources/:source/items/:externalId', async (req, res) => {
|
|
798
|
-
if (!await checkInternet()) {
|
|
799
|
-
return res.status(503).json({ error: 'No internet connection — external source imports are unavailable' })
|
|
800
|
-
}
|
|
801
|
-
try {
|
|
802
|
-
const result = await getImportService().importSource(req.params.source, req.params.externalId, req.body)
|
|
803
|
-
res.status(201).json({ id: result.id })
|
|
804
|
-
} catch (error) {
|
|
805
|
-
if (error instanceof ImportError) {
|
|
806
|
-
return res.status(error.status).json({ error: error.message })
|
|
807
|
-
}
|
|
808
|
-
res.status(500).json({ error: error.message })
|
|
809
|
-
}
|
|
810
|
-
})
|
|
811
|
-
|
|
812
|
-
router.get('/polars/:id/axes/tws', (req, res) => {
|
|
813
|
-
try {
|
|
814
|
-
const table = loadStoredPolar(req.params.id)
|
|
815
|
-
res.json(getPolarTwsValues(table))
|
|
816
|
-
} catch (e) {
|
|
817
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
818
|
-
}
|
|
819
|
-
})
|
|
820
|
-
|
|
821
|
-
router.get('/polars/:id/queries/curve', (req, res) => {
|
|
822
|
-
try {
|
|
823
|
-
const table = loadPolarCached(req.params.id)
|
|
824
596
|
const tws = parseFloat(req.query.tws)
|
|
825
597
|
if (!Number.isFinite(tws) || tws < 0) {
|
|
826
598
|
return res.status(400).json({ error: "'tws' query parameter required (m/s)" })
|
|
@@ -829,71 +601,7 @@ module.exports = (app) => {
|
|
|
829
601
|
if (!Number.isFinite(stepRad) || stepRad <= 0 || stepRad > Math.PI / 2) {
|
|
830
602
|
return res.status(400).json({ error: "'step' must be between 0 and π/2 radians" })
|
|
831
603
|
}
|
|
832
|
-
res.json(buildCurveResult(
|
|
833
|
-
} catch (e) {
|
|
834
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
835
|
-
}
|
|
836
|
-
})
|
|
837
|
-
|
|
838
|
-
router.get('/polars/:id/queries/speed', (req, res) => {
|
|
839
|
-
try {
|
|
840
|
-
const table = loadPolarCached(req.params.id)
|
|
841
|
-
const tws = parseFloat(req.query.tws)
|
|
842
|
-
const twa = parseFloat(req.query.twa)
|
|
843
|
-
if (!Number.isFinite(tws) || tws < 0 || !Number.isFinite(twa) || twa < 0) {
|
|
844
|
-
return res.status(400).json({ error: "'tws' and 'twa' query parameters are required" })
|
|
845
|
-
}
|
|
846
|
-
res.json(buildSpeedResult(table, tws, twa))
|
|
847
|
-
} catch (e) {
|
|
848
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
849
|
-
}
|
|
850
|
-
})
|
|
851
|
-
|
|
852
|
-
router.get('/polars/:id/queries/targets', (req, res) => {
|
|
853
|
-
try {
|
|
854
|
-
const table = loadPolarCached(req.params.id)
|
|
855
|
-
const tws = parseFloat(req.query.tws)
|
|
856
|
-
if (!Number.isFinite(tws) || tws < 0) {
|
|
857
|
-
return res.status(400).json({ error: "'tws' query parameter required (m/s)" })
|
|
858
|
-
}
|
|
859
|
-
res.json(buildTargetsResult(table, tws))
|
|
860
|
-
} catch (e) {
|
|
861
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
862
|
-
}
|
|
863
|
-
})
|
|
864
|
-
|
|
865
|
-
router.get('/polars/:id/queries/performance', (req, res) => {
|
|
866
|
-
try {
|
|
867
|
-
const table = loadPolarCached(req.params.id)
|
|
868
|
-
const tws = parseFloat(req.query.tws)
|
|
869
|
-
const twa = parseFloat(req.query.twa)
|
|
870
|
-
const bsp = parseFloat(req.query.bsp)
|
|
871
|
-
if (!Number.isFinite(tws) || tws < 0 || !Number.isFinite(twa) || twa < 0 || !Number.isFinite(bsp) || bsp < 0) {
|
|
872
|
-
return res.status(400).json({ error: "'tws', 'twa', and 'bsp' query parameters are required" })
|
|
873
|
-
}
|
|
874
|
-
res.json(buildPerformanceResult(table, tws, twa, bsp))
|
|
875
|
-
} catch (e) {
|
|
876
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
877
|
-
}
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
router.get('/polars/:id/meta', (req, res) => {
|
|
881
|
-
try {
|
|
882
|
-
const polarStore = getStore()
|
|
883
|
-
const table = polarStore.load(req.params.id)
|
|
884
|
-
const meta = polarStore.readMeta(req.params.id)
|
|
885
|
-
const twsValues = getPolarTwsValues(table)
|
|
886
|
-
res.json({
|
|
887
|
-
id: req.params.id,
|
|
888
|
-
name: meta.boatName || req.params.id,
|
|
889
|
-
sailnumber: meta.sailnumber,
|
|
890
|
-
boatType: meta.boatType,
|
|
891
|
-
year: meta.year,
|
|
892
|
-
source: meta.source,
|
|
893
|
-
notes: meta.notes,
|
|
894
|
-
twsMin: twsValues.length ? twsValues[0] : null,
|
|
895
|
-
twsMax: twsValues.length ? twsValues[twsValues.length - 1] : null
|
|
896
|
-
})
|
|
604
|
+
res.json(buildCurveResult(tws, stepRad))
|
|
897
605
|
} catch (e) {
|
|
898
606
|
res.status(errorStatus(e)).json({ error: e.message })
|
|
899
607
|
}
|
|
@@ -910,9 +618,10 @@ module.exports = (app) => {
|
|
|
910
618
|
const TWA = Number.isFinite(TWAsigned) ? Math.abs(TWAsigned) : null
|
|
911
619
|
const BSP = bspSmoother ? bspSmoother.value : null
|
|
912
620
|
|
|
913
|
-
const
|
|
914
|
-
?
|
|
621
|
+
const polarResult = (polar && Number.isFinite(TWS) && Number.isFinite(TWA))
|
|
622
|
+
? polar.speedAt({ tws: TWS, twa: TWA, performanceFactor })
|
|
915
623
|
: null
|
|
624
|
+
const polarSpeed = polarResult ? polarResult.value : null
|
|
916
625
|
|
|
917
626
|
const performance = (Number.isFinite(BSP) && Number.isFinite(polarSpeed) && polarSpeed > 0)
|
|
918
627
|
? BSP / polarSpeed
|
|
@@ -920,17 +629,13 @@ module.exports = (app) => {
|
|
|
920
629
|
|
|
921
630
|
const si = v => Number.isFinite(v) ? parseFloat(v.toFixed(5)) : null
|
|
922
631
|
|
|
923
|
-
const polarState = (polarTable && Number.isFinite(TWS) && Number.isFinite(TWA))
|
|
924
|
-
? polarTable.getInterpolationState(TWS, TWA)
|
|
925
|
-
: null
|
|
926
|
-
|
|
927
632
|
res.json({
|
|
928
633
|
tws: si(TWS),
|
|
929
634
|
twa: si(TWAsigned),
|
|
930
635
|
bsp: si(BSP),
|
|
931
636
|
polarSpeed: si(polarSpeed),
|
|
932
637
|
performance: Number.isFinite(performance) ? parseFloat(performance.toFixed(5)) : null,
|
|
933
|
-
polarState
|
|
638
|
+
polarState: polarResult ? polarResult.state : null
|
|
934
639
|
})
|
|
935
640
|
})
|
|
936
641
|
|
|
@@ -958,8 +663,8 @@ module.exports = (app) => {
|
|
|
958
663
|
|
|
959
664
|
const bspPath = settings.useSOG ? 'navigation.speedOverGround' : 'navigation.speedThroughWater'
|
|
960
665
|
|
|
961
|
-
const polarState = (
|
|
962
|
-
?
|
|
666
|
+
const polarState = (polar && Number.isFinite(TWS) && Number.isFinite(TWAsigned))
|
|
667
|
+
? polar.speedAt({ tws: TWS, twa: Math.abs(TWAsigned), performanceFactor }).state
|
|
963
668
|
: null
|
|
964
669
|
|
|
965
670
|
// Build outputs object: only include paths that are enabled and were
|
|
@@ -1001,9 +706,9 @@ module.exports = (app) => {
|
|
|
1001
706
|
})
|
|
1002
707
|
|
|
1003
708
|
// Metadata describing the units and display preferences for each field
|
|
1004
|
-
// returned by /live and the canonical curve query endpoints
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
709
|
+
// returned by /live and the canonical curve query endpoints, plus a
|
|
710
|
+
// read-only summary of the active polar and performance factor
|
|
711
|
+
// (both sourced from the `polars.*` SK paths — not editable here).
|
|
1007
712
|
router.get('/meta', (req, res) => {
|
|
1008
713
|
const speed = { formula: 'value * 1.943844', symbol: 'kn', displayFormat: '0.0' }
|
|
1009
714
|
const angle = { formula: 'value * 57.29577951308231', symbol: '\u00b0', displayFormat: '0.0' }
|
|
@@ -1017,7 +722,17 @@ module.exports = (app) => {
|
|
|
1017
722
|
performance: { units: 'ratio', displayUnits: ratio },
|
|
1018
723
|
'curve.tbs': { units: 'm/s', displayUnits: speed },
|
|
1019
724
|
'curve.vmg': { units: 'm/s', displayUnits: speed },
|
|
1020
|
-
'curve.twa': { units: 'rad', displayUnits: angle }
|
|
725
|
+
'curve.twa': { units: 'rad', displayUnits: angle },
|
|
726
|
+
activePolar: activePolarDoc ? {
|
|
727
|
+
id: activePolarId,
|
|
728
|
+
name: activePolarDoc.name || activePolarId,
|
|
729
|
+
boatType: activePolarDoc.boatType ?? null,
|
|
730
|
+
sailnumber: activePolarDoc.sailnumber ?? null,
|
|
731
|
+
year: activePolarDoc.year ?? null,
|
|
732
|
+
source: activePolarDoc.source ?? null,
|
|
733
|
+
notes: activePolarDoc.notes ?? null
|
|
734
|
+
} : null,
|
|
735
|
+
performanceFactor
|
|
1021
736
|
})
|
|
1022
737
|
})
|
|
1023
738
|
|
|
@@ -1037,115 +752,52 @@ module.exports = (app) => {
|
|
|
1037
752
|
// wind update (or immediately below if the plugin is already running).
|
|
1038
753
|
Object.assign(changedOptions, req.body)
|
|
1039
754
|
hasPendingChanges = true
|
|
1040
|
-
// Drain immediately so source
|
|
755
|
+
// Drain immediately so source changes take effect even when the
|
|
1041
756
|
// wind data stream is idle.
|
|
1042
757
|
if (isRunning) applyOptionChanges()
|
|
1043
758
|
res.json({ ...settings, ...changedOptions, _defaults: DEFAULT_SETTINGS })
|
|
1044
759
|
})
|
|
1045
|
-
|
|
1046
|
-
// ---- Polar file operations -------------------------------------------
|
|
1047
|
-
|
|
1048
|
-
router.get('/polars', (req, res) => {
|
|
1049
|
-
try {
|
|
1050
|
-
const polarStore = getStore()
|
|
1051
|
-
const polars = polarStore.list().map((id) => {
|
|
1052
|
-
const meta = polarStore.readMeta(id)
|
|
1053
|
-
return {
|
|
1054
|
-
id,
|
|
1055
|
-
name: meta.boatName || id,
|
|
1056
|
-
sailnumber: meta.sailnumber,
|
|
1057
|
-
boatType: meta.boatType,
|
|
1058
|
-
year: meta.year,
|
|
1059
|
-
source: meta.source
|
|
1060
|
-
}
|
|
1061
|
-
})
|
|
1062
|
-
res.json(polars)
|
|
1063
|
-
} catch (e) {
|
|
1064
|
-
res.status(500).json({ error: e.message })
|
|
1065
|
-
}
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
router.get('/polars/:id', (req, res) => {
|
|
1069
|
-
try {
|
|
1070
|
-
const stored = getStore().readObject(req.params.id)
|
|
1071
|
-
res.json({ ...stored, id: req.params.id })
|
|
1072
|
-
} catch (e) {
|
|
1073
|
-
res.status(404).json({ error: e.message })
|
|
1074
|
-
}
|
|
1075
|
-
})
|
|
1076
|
-
|
|
1077
|
-
router.put('/polars/:id', (req, res) => {
|
|
1078
|
-
const validationError = validatePolarResourceBody(req.body)
|
|
1079
|
-
if (validationError) {
|
|
1080
|
-
return res.status(400).json({ error: validationError })
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
try {
|
|
1084
|
-
getStore().saveCanonical(req.params.id, {
|
|
1085
|
-
...req.body,
|
|
1086
|
-
name: req.body.name || req.params.id
|
|
1087
|
-
})
|
|
1088
|
-
invalidatePolarCache(req.params.id)
|
|
1089
|
-
if (settings.activePolar === req.params.id) {
|
|
1090
|
-
polarTable = loadStoredPolar(req.params.id)
|
|
1091
|
-
polarTable.setPerformanceAdjustment(settings.perfAdjust || 1)
|
|
1092
|
-
}
|
|
1093
|
-
res.json({ id: req.params.id })
|
|
1094
|
-
} catch (e) {
|
|
1095
|
-
res.status(500).json({ error: e.message })
|
|
1096
|
-
}
|
|
1097
|
-
})
|
|
1098
|
-
|
|
1099
|
-
router.delete('/polars/:id', (req, res) => {
|
|
1100
|
-
try {
|
|
1101
|
-
getStore().delete(req.params.id)
|
|
1102
|
-
invalidatePolarCache(req.params.id)
|
|
1103
|
-
if (settings.activePolar === req.params.id) {
|
|
1104
|
-
settings.activePolar = ''
|
|
1105
|
-
polarTable = null
|
|
1106
|
-
nullifyOutputs()
|
|
1107
|
-
app.savePluginOptions(settings, (err) => {
|
|
1108
|
-
if (err) app.error('Failed to save settings: ' + err.message)
|
|
1109
|
-
})
|
|
1110
|
-
}
|
|
1111
|
-
res.json({ id: req.params.id })
|
|
1112
|
-
} catch (e) {
|
|
1113
|
-
res.status(errorStatus(e)).json({ error: e.message })
|
|
1114
|
-
}
|
|
1115
|
-
})
|
|
1116
760
|
},
|
|
1117
761
|
|
|
1118
762
|
start(options) {
|
|
763
|
+
app.debug('Starting')
|
|
1119
764
|
metaSentPaths = new Set() // reset so metadata is re-emitted after restart
|
|
1120
765
|
lifecycleWarningMap = new Map()
|
|
1121
766
|
lifecycleWarnings = []
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
767
|
+
polar = null
|
|
768
|
+
activePolarId = null
|
|
769
|
+
activePolarDoc = null
|
|
770
|
+
performanceFactor = 1
|
|
1125
771
|
|
|
1126
772
|
settings = migrateSettings({ ...DEFAULT_SETTINGS, ...options, settingsVersion: options.settingsVersion ?? 0 })
|
|
1127
773
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
if (settings.activePolar) {
|
|
1133
|
-
try {
|
|
1134
|
-
polarTable = store.load(settings.activePolar)
|
|
1135
|
-
polarTable.setPerformanceAdjustment(settings.perfAdjust || 1)
|
|
1136
|
-
app.setPluginStatus(`Polar '${settings.activePolar}' loaded`)
|
|
1137
|
-
} catch (e) {
|
|
1138
|
-
// File missing or corrupt — run without a polar rather than crashing.
|
|
1139
|
-
// Keep settings.activePolar so the user can see which file is broken
|
|
1140
|
-
// and re-upload it without reconfiguring. Nullify any stale SK values.
|
|
1141
|
-
polarTable = null
|
|
1142
|
-
nullifyOutputs()
|
|
1143
|
-
app.setPluginError(`Polar '${settings.activePolar}' could not be loaded: ${e.message}`)
|
|
1144
|
-
app.debug('Polar load error: %s', e.message)
|
|
774
|
+
checkPolarProvider().then((available) => {
|
|
775
|
+
app.debug(`checkPolarProvider: ${available ? 'polars resource provider found' : 'no polars resource provider'}`)
|
|
776
|
+
if (!available) {
|
|
777
|
+
app.setPluginStatus("No 'polars' resource provider registered — install signalk-polar-management (or another plugin providing the 'polars' resource type) to select an active polar.")
|
|
1145
778
|
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
// Active polar / performance factor — read-only, published by a 'polars' resource provider.
|
|
782
|
+
// These are event-driven (change only on user action), so the idle-watchdog
|
|
783
|
+
// unsubscribe/resubscribe used for sensor handlers below does not apply here: absence
|
|
784
|
+
// of deltas is the normal state, not a sign of a broken subscription, and periodically
|
|
785
|
+
// forcing a resubscribe would just be churn (and possible side effects) for no reason.
|
|
786
|
+
activePolarHandler = new MessageHandler(app, plugin.id, 'activePolar')
|
|
787
|
+
activePolarHandler.path = 'polars.activePolar'
|
|
788
|
+
activePolarHandler.onDelta = () => handleActivePolarDelta()
|
|
789
|
+
activePolarHandler.subscribe()
|
|
790
|
+
|
|
791
|
+
performanceFactorHandler = new MessageHandler(app, plugin.id, 'performanceFactor')
|
|
792
|
+
performanceFactorHandler.path = 'polars.performanceFactor'
|
|
793
|
+
performanceFactorHandler.onDelta = () => {
|
|
794
|
+
const value = performanceFactorHandler.value
|
|
795
|
+
performanceFactor = Number.isFinite(value) ? value : 1
|
|
1148
796
|
}
|
|
797
|
+
performanceFactorHandler.subscribe()
|
|
798
|
+
|
|
799
|
+
const SmootherClass = getSmootherClass(settings.smootherType)
|
|
800
|
+
const smootherOptions = getSmootherOptions(settings.smootherType, settings)
|
|
1149
801
|
|
|
1150
802
|
// Wind vector smoother (TWS + TWA combined as a Cartesian vector —
|
|
1151
803
|
// avoids ±π wraparound discontinuity during smoothing)
|
|
@@ -1211,12 +863,17 @@ module.exports = (app) => {
|
|
|
1211
863
|
},
|
|
1212
864
|
|
|
1213
865
|
stop() {
|
|
866
|
+
app.debug('Stopping')
|
|
1214
867
|
isRunning = false
|
|
1215
|
-
importService = null
|
|
1216
868
|
nullifyOutputs()
|
|
1217
869
|
if (windSmoother) { windSmoother.terminate(); windSmoother = null }
|
|
1218
870
|
if (bspSmoother) { bspSmoother.terminate(); bspSmoother = null }
|
|
1219
871
|
if (hdgSmoother) { hdgSmoother.terminate(); hdgSmoother = null }
|
|
872
|
+
if (activePolarHandler) { activePolarHandler.unsubscribe(); activePolarHandler = null }
|
|
873
|
+
if (performanceFactorHandler) { performanceFactorHandler.unsubscribe(); performanceFactorHandler = null }
|
|
874
|
+
polar = null
|
|
875
|
+
activePolarId = null
|
|
876
|
+
activePolarDoc = null
|
|
1220
877
|
lifecycleWarningMap = new Map()
|
|
1221
878
|
lifecycleWarnings = []
|
|
1222
879
|
app.debug('Plugin stopped')
|