signalk-sailboat-simulator 0.2.0 → 0.3.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 +7 -0
- package/README.md +19 -17
- package/index.js +135 -18
- package/lib/plugin-schema.js +5 -5
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-09-09
|
|
4
|
+
|
|
5
|
+
- Calculate simulated boat speed directly from the active `signalk-polar-management` polar resource via `polar-math`.
|
|
6
|
+
- Replace the `signalk-polar-performance-plugin` recommendation with `signalk-polar-management`.
|
|
7
|
+
- Stop requiring `performance.polarSpeed` as an input path.
|
|
8
|
+
- Keep the active polar cached for up to 60 seconds so edits made under the same resource id are picked up automatically.
|
|
9
|
+
|
|
3
10
|
## [0.2.0] - 2026-08-17
|
|
4
11
|
|
|
5
12
|
- Follow `steering.autopilot.output.turnRate` from `signalk-autopilot-emulator-v2` directly instead of deriving and rate-limiting turns from autopilot target headings.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Signal K Sailboat Simulator
|
|
2
2
|
|
|
3
|
-
Signal K plugin that simulates a sailing boat by integrating a virtual position from data already present in Signal K: autopilot turn-rate output, weather and polar
|
|
3
|
+
Signal K plugin that simulates a sailing boat by integrating a virtual position from data already present in Signal K: autopilot turn-rate output, weather and an active polar resource.
|
|
4
4
|
|
|
5
|
-
The design goal is to keep this plugin small. It publishes only the simulated vessel state and expects specialised plugins to provide autopilot intent, weather and polar
|
|
5
|
+
The design goal is to keep this plugin small. It publishes only the simulated vessel state and expects specialised plugins to provide autopilot intent, weather and polar management.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -11,11 +11,11 @@ The easiest setup is to use the Signal K App Store:
|
|
|
11
11
|
1. Install these recommended plugins:
|
|
12
12
|
- `@signalk/open-meteo-provider`
|
|
13
13
|
- `signalk-autopilot-emulator-v2`
|
|
14
|
-
- `signalk-polar-
|
|
14
|
+
- `signalk-polar-management`
|
|
15
15
|
- `signalk-derived-data`
|
|
16
16
|
- `signalk-distance-to-shore`
|
|
17
17
|
2. Configure `@signalk/open-meteo-provider` and make it the default Signal K Weather API provider. You can also leave the Signal K default as-is and set `wind.providerId` to `open-meteo` in this plugin.
|
|
18
|
-
3.
|
|
18
|
+
3. Open `signalk-polar-management`, import a polar file for the simulated boat, and set it as the active polar.
|
|
19
19
|
4. Configure `signalk-derived-data` to publish `navigation.magneticVariation`, so the simulator can publish `navigation.headingMagnetic` for plugins that need magnetic heading.
|
|
20
20
|
5. Install and enable `signalk-sailboat-simulator`.
|
|
21
21
|
6. Keep the default simulator options for a first run.
|
|
@@ -36,7 +36,7 @@ Minimum capabilities for a useful simulation:
|
|
|
36
36
|
|
|
37
37
|
- Autopilot output: `signalk-autopilot-emulator-v2` provides `steering.autopilot.output.turnRate`, which the simulator integrates as the boat heading change.
|
|
38
38
|
- Weather: `@signalk/open-meteo-provider` can provide Weather API observations or forecasts at the simulated position.
|
|
39
|
-
- Polar
|
|
39
|
+
- Polar management: `signalk-polar-management` stores the simulated boat polars and publishes the active polar resource. The simulator uses `polar-math` internally to calculate boat speed from that active polar.
|
|
40
40
|
- Derived data: enable the `signalk-derived-data` option that publishes `navigation.magneticVariation`; when this path is available, the simulator also publishes `navigation.headingMagnetic` for plugins that need magnetic heading.
|
|
41
41
|
- Shore distance: `signalk-distance-to-shore` can provide `navigation.distanceToShore` and `navigation.shore.bearingTrue` for grounding protection.
|
|
42
42
|
|
|
@@ -61,13 +61,13 @@ Wind:
|
|
|
61
61
|
- `environment.wind.speedApparent`
|
|
62
62
|
- `environment.wind.angleApparent`
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Polar input expected by the simulator:
|
|
65
65
|
|
|
66
|
-
- `
|
|
66
|
+
- `polars.activePolar`
|
|
67
67
|
|
|
68
68
|
All values are published with `$source: signalk-sailboat-simulator`.
|
|
69
69
|
|
|
70
|
-
The plugin status will show `
|
|
70
|
+
The plugin status will show `waitingForPolar` until an active polar is selected, `waitingForWind` until weather wind is available, or `waitingForPolarSpeed` when the active polar cannot produce a speed for the current wind angle. If `steering.autopilot.output.turnRate` is missing, the simulated boat keeps its current heading. If grounding protection is enabled and `navigation.distanceToShore` is below the configured minimum, the status will show `groundingProtection` and the simulated boat will stop.
|
|
71
71
|
|
|
72
72
|
## Input contract
|
|
73
73
|
|
|
@@ -75,11 +75,12 @@ The simulator reads these fixed Signal K input paths:
|
|
|
75
75
|
|
|
76
76
|
- `steering.autopilot.output.turnRate`: desired heading change rate in rad/s
|
|
77
77
|
- `navigation.magneticVariation`: optional magnetic variation in radians, used only to publish `navigation.headingMagnetic`
|
|
78
|
-
- `
|
|
78
|
+
- `polars.activePolar`: active polar resource pointer published by `signalk-polar-management`
|
|
79
|
+
- `polars.performanceFactor`: optional speed multiplier published by `signalk-polar-management`
|
|
79
80
|
- `navigation.distanceToShore`: optional distance to the nearest coast in m, used for grounding protection
|
|
80
81
|
- `navigation.shore.bearingTrue`: optional bearing from the vessel to the nearest coast in radians, used to allow recovery headings away from shore
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
Wind is always read from Signal K Weather API data at the simulated position because it is required for the internal polar speed calculation. The simulator first tries observations, then falls back to the closest point forecast when no usable observation is available. The true and apparent wind options only control whether those values are also published as Signal K paths.
|
|
83
84
|
|
|
84
85
|
By default the simulator uses the Signal K default weather provider. Set `wind.providerId` to a registered provider id, for example `open-meteo`, to use that provider explicitly.
|
|
85
86
|
|
|
@@ -93,9 +94,9 @@ The simulator publishes virtual wind for the rest of the Signal K stack. The pub
|
|
|
93
94
|
- `environment.wind.speedApparent`
|
|
94
95
|
- `environment.wind.angleApparent`
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
The simulator uses the same true wind values with the active polar resource to calculate boat speed. The active polar must use the canonical `polar-format` structure provided by `signalk-polar-management`.
|
|
97
98
|
|
|
98
|
-
The
|
|
99
|
+
The simulator uses the calculated polar speed as boat speed, integrates heading from `steering.autopilot.output.turnRate`, and integrates position along that simulated heading. Current, leeway, route following and manoeuvre rules are intentionally left as separate steps.
|
|
99
100
|
|
|
100
101
|
## Configuration
|
|
101
102
|
|
|
@@ -107,16 +108,16 @@ The plugin intentionally keeps its configuration surface small:
|
|
|
107
108
|
Options: latitude, longitude and heading in degrees. These values are only used when persistence is disabled or no previous runtime state has been saved.
|
|
108
109
|
|
|
109
110
|
- `wind`
|
|
110
|
-
What it does: reads wind at the simulated position from the Signal K Weather API and publishes the selected wind values.
|
|
111
|
-
Goal:
|
|
112
|
-
Requires: a Weather API provider. The recommended provider is `@signalk/open-meteo-provider
|
|
111
|
+
What it does: always reads wind at the simulated position from the Signal K Weather API for the speed calculation, and optionally publishes the selected wind values.
|
|
112
|
+
Goal: provide the true wind data used by the simulator to calculate polar speed, and publish that same virtual wind for other Signal K tools.
|
|
113
|
+
Requires: a Weather API provider. The recommended provider is `@signalk/open-meteo-provider`.
|
|
113
114
|
Options: publish true wind, publish apparent wind, optional provider id, and polling interval.
|
|
114
115
|
Published true wind paths: `environment.wind.speedTrue`, `environment.wind.directionTrue`, `environment.wind.angleTrueWater`.
|
|
115
116
|
Published apparent wind paths: `environment.wind.speedApparent`, `environment.wind.angleApparent`.
|
|
116
117
|
|
|
117
118
|
- `publishing`
|
|
118
119
|
What it does: publishes the simulated navigation state.
|
|
119
|
-
Requires: `steering.autopilot.output.turnRate` to steer
|
|
120
|
+
Requires: `steering.autopilot.output.turnRate` to steer, an active polar from `signalk-polar-management`, and weather wind to calculate boat speed. Recommended providers are `signalk-autopilot-emulator-v2`, `signalk-polar-management`, and `signalk-derived-data` when magnetic heading output is needed.
|
|
120
121
|
Options: publish navigation state.
|
|
121
122
|
Published paths: `navigation.position`, `navigation.headingTrue`, `navigation.headingMagnetic` when `navigation.magneticVariation` is available, `navigation.courseOverGroundTrue`, `navigation.speedOverGround`, `navigation.speedThroughWater`.
|
|
122
123
|
|
|
@@ -169,7 +170,8 @@ Then restart Signal K.
|
|
|
169
170
|
- `@signalk/open-meteo-provider`: https://www.npmjs.com/package/@signalk/open-meteo-provider
|
|
170
171
|
- `signalk-autopilot-emulator-v2`: https://www.npmjs.com/package/signalk-autopilot-emulator-v2
|
|
171
172
|
- `signalk-autopilot-emulator-v2` source: https://github.com/macjl/signalk-autopilot-emulator-v2
|
|
172
|
-
- `signalk-polar-
|
|
173
|
+
- `signalk-polar-management`: https://www.npmjs.com/package/signalk-polar-management
|
|
174
|
+
- `polar-math`: https://www.npmjs.com/package/polar-math
|
|
173
175
|
- `signalk-derived-data`: https://www.npmjs.com/package/signalk-derived-data
|
|
174
176
|
- `signalk-distance-to-shore`: https://www.npmjs.com/package/signalk-distance-to-shore
|
|
175
177
|
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { Polar } = require('polar-math')
|
|
3
4
|
const { createInitialState, stepSimulation } = require('./lib/simulation-engine')
|
|
4
5
|
const { buildSchema } = require('./lib/plugin-schema')
|
|
5
6
|
const { degToRad, radToDeg, wrap360Rad } = require('./lib/angles')
|
|
@@ -8,11 +9,13 @@ const { applyPersistedState, createStateStore, stateFromSimulation } = require('
|
|
|
8
9
|
|
|
9
10
|
const PLUGIN_ID = 'sailboat-simulator'
|
|
10
11
|
const PUBLISH_SOURCE = 'signalk-sailboat-simulator'
|
|
12
|
+
const POLAR_CACHE_MAX_AGE_MS = 60_000
|
|
11
13
|
|
|
12
14
|
const INPUT_PATHS = {
|
|
13
15
|
turnRatePath: 'steering.autopilot.output.turnRate',
|
|
14
16
|
magneticVariationPath: 'navigation.magneticVariation',
|
|
15
|
-
|
|
17
|
+
activePolarPath: 'polars.activePolar',
|
|
18
|
+
performanceFactorPath: 'polars.performanceFactor',
|
|
16
19
|
distanceToShorePath: 'navigation.distanceToShore',
|
|
17
20
|
shoreBearingTruePath: 'navigation.shore.bearingTrue'
|
|
18
21
|
}
|
|
@@ -60,11 +63,12 @@ module.exports = function createPlugin (app) {
|
|
|
60
63
|
let lastStateSaveAt = 0
|
|
61
64
|
let weatherSnapshot = null
|
|
62
65
|
let stateStore = null
|
|
66
|
+
let polarCache = null
|
|
63
67
|
|
|
64
68
|
const plugin = {
|
|
65
69
|
id: PLUGIN_ID,
|
|
66
70
|
name: 'Sailboat Simulator',
|
|
67
|
-
description: 'Simulates a sailing boat position from Signal K autopilot turn-rate output, weather and polar
|
|
71
|
+
description: 'Simulates a sailing boat position from Signal K autopilot turn-rate output, weather and an active polar resource.',
|
|
68
72
|
schema: buildSchema,
|
|
69
73
|
start,
|
|
70
74
|
stop,
|
|
@@ -78,6 +82,7 @@ module.exports = function createPlugin (app) {
|
|
|
78
82
|
lastWeatherFetchAt = 0
|
|
79
83
|
lastStateSaveAt = 0
|
|
80
84
|
weatherSnapshot = null
|
|
85
|
+
polarCache = null
|
|
81
86
|
stateStore = createStateStore(app, PLUGIN_ID)
|
|
82
87
|
state = createInitialState(options)
|
|
83
88
|
if (options.persistence.enabled) {
|
|
@@ -103,7 +108,7 @@ module.exports = function createPlugin (app) {
|
|
|
103
108
|
const now = Date.now()
|
|
104
109
|
try {
|
|
105
110
|
await refreshWeather(now)
|
|
106
|
-
const inputs = readInputs()
|
|
111
|
+
const inputs = await readInputs()
|
|
107
112
|
state = stepSimulation(state, inputs, options, now)
|
|
108
113
|
publishState(inputs)
|
|
109
114
|
persistState(now)
|
|
@@ -126,12 +131,13 @@ module.exports = function createPlugin (app) {
|
|
|
126
131
|
if (stateStore.save(persisted)) lastStateSaveAt = now
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
function readInputs () {
|
|
134
|
+
async function readInputs () {
|
|
130
135
|
const weatherWind = freshWeatherWind()
|
|
131
|
-
|
|
136
|
+
const inputs = {
|
|
132
137
|
turnRate: readNumber(INPUT_PATHS.turnRatePath),
|
|
133
138
|
magneticVariation: readNumber(INPUT_PATHS.magneticVariationPath),
|
|
134
|
-
|
|
139
|
+
activePolar: readValue(INPUT_PATHS.activePolarPath),
|
|
140
|
+
performanceFactor: readNumber(INPUT_PATHS.performanceFactorPath),
|
|
135
141
|
windSpeedTrue: weatherWind && weatherWind.speedTrue != null
|
|
136
142
|
? weatherWind.speedTrue
|
|
137
143
|
: null,
|
|
@@ -143,14 +149,89 @@ module.exports = function createPlugin (app) {
|
|
|
143
149
|
weatherObservedAt: weatherWind ? weatherWind.observedAt : null,
|
|
144
150
|
weatherDescription: weatherWind ? weatherWind.description : ''
|
|
145
151
|
}
|
|
152
|
+
const polarSpeed = await calculatePolarSpeed(inputs)
|
|
153
|
+
inputs.polarSpeed = polarSpeed.speed
|
|
154
|
+
inputs.polar = polarSpeed.status
|
|
155
|
+
return inputs
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
function
|
|
158
|
+
function readValue (path) {
|
|
149
159
|
if (!path || typeof app.getSelfPath !== 'function') return null
|
|
150
|
-
|
|
160
|
+
return app.getSelfPath(`${path}.value`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readNumber (path) {
|
|
164
|
+
const value = readValue(path)
|
|
151
165
|
return Number.isFinite(value) ? value : null
|
|
152
166
|
}
|
|
153
167
|
|
|
168
|
+
async function calculatePolarSpeed (inputs) {
|
|
169
|
+
const activePolar = activePolarFromValue(inputs.activePolar)
|
|
170
|
+
const performanceFactor = Number.isFinite(inputs.performanceFactor)
|
|
171
|
+
? inputs.performanceFactor
|
|
172
|
+
: 1
|
|
173
|
+
const status = {
|
|
174
|
+
status: 'missingActivePolar',
|
|
175
|
+
id: activePolar.id,
|
|
176
|
+
href: activePolar.href,
|
|
177
|
+
performanceFactor,
|
|
178
|
+
speedState: null,
|
|
179
|
+
speed: null,
|
|
180
|
+
windAngleTrueWaterDeg: null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!activePolar.id) {
|
|
184
|
+
return { speed: null, status }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!Number.isFinite(inputs.windSpeedTrue) || !Number.isFinite(inputs.windDirectionTrue)) {
|
|
188
|
+
status.status = 'waitingForWind'
|
|
189
|
+
return { speed: null, status }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const windAngleTrueWater = trueWindFromDirection(inputs.windDirectionTrue, state.headingTrue)
|
|
193
|
+
status.windAngleTrueWaterDeg = Number.isFinite(windAngleTrueWater)
|
|
194
|
+
? radToDeg(windAngleTrueWater)
|
|
195
|
+
: null
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const polar = await loadActivePolar(activePolar.id)
|
|
199
|
+
const result = polar.speedAt({
|
|
200
|
+
tws: inputs.windSpeedTrue,
|
|
201
|
+
twa: windAngleTrueWater,
|
|
202
|
+
performanceFactor
|
|
203
|
+
})
|
|
204
|
+
status.speedState = result.state
|
|
205
|
+
if (Number.isFinite(result.value)) {
|
|
206
|
+
status.status = 'ready'
|
|
207
|
+
status.speed = result.value
|
|
208
|
+
return { speed: result.value, status }
|
|
209
|
+
}
|
|
210
|
+
status.status = 'noSpeedForConditions'
|
|
211
|
+
return { speed: null, status }
|
|
212
|
+
} catch (error) {
|
|
213
|
+
status.status = 'error'
|
|
214
|
+
status.error = error.message
|
|
215
|
+
return { speed: null, status }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function loadActivePolar (id) {
|
|
220
|
+
const now = Date.now()
|
|
221
|
+
if (polarCache && polarCache.id === id && now - polarCache.loadedAt < POLAR_CACHE_MAX_AGE_MS) {
|
|
222
|
+
return polarCache.polar
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!app.resourcesApi || typeof app.resourcesApi.getResource !== 'function') {
|
|
226
|
+
throw new Error('Signal K resources API is not available')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const table = await app.resourcesApi.getResource('polars', id)
|
|
230
|
+
const polar = Polar.fromTable(table)
|
|
231
|
+
polarCache = { id, polar, loadedAt: now }
|
|
232
|
+
return polar
|
|
233
|
+
}
|
|
234
|
+
|
|
154
235
|
function publishState (inputs) {
|
|
155
236
|
if (!state || !app.handleMessage) return
|
|
156
237
|
|
|
@@ -224,12 +305,7 @@ module.exports = function createPlugin (app) {
|
|
|
224
305
|
return options.wind.apparentWind !== false
|
|
225
306
|
}
|
|
226
307
|
|
|
227
|
-
function publishesAnyWind () {
|
|
228
|
-
return publishTrueWind() || publishApparentWind()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
308
|
async function refreshWeather (now) {
|
|
232
|
-
if (!publishesAnyWind()) return
|
|
233
309
|
const intervalSeconds = freshWeatherWind()
|
|
234
310
|
? options.wind.pollIntervalSeconds
|
|
235
311
|
: options.wind.retryIntervalSeconds
|
|
@@ -355,9 +431,7 @@ module.exports = function createPlugin (app) {
|
|
|
355
431
|
function updateRuntime (inputs) {
|
|
356
432
|
const headingMagnetic = magneticHeadingFromTrue(state.headingTrue, inputs.magneticVariation)
|
|
357
433
|
runtime = {
|
|
358
|
-
status:
|
|
359
|
-
? 'groundingProtection'
|
|
360
|
-
: Number.isFinite(state.speedThroughWater) && state.speedThroughWater > 0 ? 'sailing' : 'waitingForPerformance',
|
|
434
|
+
status: runtimeStatus(inputs),
|
|
361
435
|
position: state.position,
|
|
362
436
|
headingTrueDeg: radToDeg(state.headingTrue),
|
|
363
437
|
headingMagneticDeg: Number.isFinite(headingMagnetic) ? radToDeg(headingMagnetic) : null,
|
|
@@ -377,11 +451,14 @@ module.exports = function createPlugin (app) {
|
|
|
377
451
|
providerId: weatherSnapshot.providerId,
|
|
378
452
|
description: weatherSnapshot.description
|
|
379
453
|
}
|
|
380
|
-
: { status:
|
|
454
|
+
: { status: 'missing' },
|
|
455
|
+
polar: inputs.polar || { status: 'missingActivePolar' },
|
|
381
456
|
inputs: {
|
|
382
457
|
turnRate: valueStatus(inputs.turnRate),
|
|
383
458
|
magneticVariation: valueStatus(inputs.magneticVariation),
|
|
384
|
-
|
|
459
|
+
activePolar: activePolarFromValue(inputs.activePolar).id ? 'present' : 'missing',
|
|
460
|
+
performanceFactor: valueStatus(inputs.performanceFactor),
|
|
461
|
+
polarSpeed: valueStatus(inputs.polarSpeed),
|
|
385
462
|
distanceToShore: valueStatus(inputs.distanceToShore),
|
|
386
463
|
shoreBearingTrue: valueStatus(inputs.shoreBearingTrue),
|
|
387
464
|
windSpeedTrue: valueStatus(inputs.windSpeedTrue),
|
|
@@ -391,6 +468,16 @@ module.exports = function createPlugin (app) {
|
|
|
391
468
|
}
|
|
392
469
|
}
|
|
393
470
|
|
|
471
|
+
function runtimeStatus (inputs) {
|
|
472
|
+
if (state.groundingProtectionActive) return 'groundingProtection'
|
|
473
|
+
if (Number.isFinite(state.speedThroughWater) && state.speedThroughWater > 0) return 'sailing'
|
|
474
|
+
if (!inputs.polar || inputs.polar.status === 'missingActivePolar') return 'waitingForPolar'
|
|
475
|
+
if (inputs.polar.status === 'waitingForWind') return 'waitingForWind'
|
|
476
|
+
if (inputs.polar.status === 'error') return 'polarError'
|
|
477
|
+
if (inputs.polar.status === 'noSpeedForConditions') return 'waitingForPolarSpeed'
|
|
478
|
+
return 'waitingForPolarSpeed'
|
|
479
|
+
}
|
|
480
|
+
|
|
394
481
|
function setStatus () {
|
|
395
482
|
if (!app.setPluginStatus) return
|
|
396
483
|
if (runtime.status === 'sailing') {
|
|
@@ -458,6 +545,36 @@ function valueStatus (value) {
|
|
|
458
545
|
return Number.isFinite(value) ? 'present' : 'missing'
|
|
459
546
|
}
|
|
460
547
|
|
|
548
|
+
function activePolarFromValue (value) {
|
|
549
|
+
if (typeof value === 'string' && value.trim()) {
|
|
550
|
+
const hrefId = polarIdFromHref(value)
|
|
551
|
+
return { id: hrefId || value.trim(), href: hrefId ? value : null }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!value || typeof value !== 'object') {
|
|
555
|
+
return { id: null, href: null }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const href = typeof value.href === 'string' ? value.href : null
|
|
559
|
+
return {
|
|
560
|
+
id: typeof value.id === 'string' && value.id.trim()
|
|
561
|
+
? value.id.trim()
|
|
562
|
+
: polarIdFromHref(href),
|
|
563
|
+
href
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function polarIdFromHref (href) {
|
|
568
|
+
if (typeof href !== 'string') return null
|
|
569
|
+
const match = href.match(/\/resources\/polars\/([^/?#]+)/)
|
|
570
|
+
if (!match) return null
|
|
571
|
+
try {
|
|
572
|
+
return decodeURIComponent(match[1])
|
|
573
|
+
} catch (_) {
|
|
574
|
+
return match[1]
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
461
578
|
function magneticHeadingFromTrue (headingTrue, magneticVariation) {
|
|
462
579
|
if (!Number.isFinite(headingTrue) || !Number.isFinite(magneticVariation)) return null
|
|
463
580
|
return wrap360Rad(headingTrue - magneticVariation)
|
package/lib/plugin-schema.js
CHANGED
|
@@ -4,7 +4,7 @@ function buildSchema () {
|
|
|
4
4
|
return {
|
|
5
5
|
type: 'object',
|
|
6
6
|
title: 'Sailboat Simulator',
|
|
7
|
-
description: 'Simulates a virtual sailboat from autopilot turn-rate output, polar
|
|
7
|
+
description: 'Simulates a virtual sailboat from autopilot turn-rate output, an active polar resource, weather and optional shore-distance data already present in Signal K.',
|
|
8
8
|
properties: {
|
|
9
9
|
initialState: {
|
|
10
10
|
type: 'object',
|
|
@@ -37,12 +37,12 @@ function buildSchema () {
|
|
|
37
37
|
wind: {
|
|
38
38
|
type: 'object',
|
|
39
39
|
title: 'Wind',
|
|
40
|
-
description: 'What it does: reads wind at the simulated boat position from the Signal K Weather API and publishes the selected wind values. The
|
|
40
|
+
description: 'What it does: reads wind at the simulated boat position from the Signal K Weather API and publishes the selected wind values. The simulator also uses this wind with the active polar resource to calculate boat speed. Requires: a Weather API provider, preferably @signalk/open-meteo-provider. Options: publish true wind, publish apparent wind, optionally force a provider id instead of the Signal K default, and choose the polling interval.',
|
|
41
41
|
properties: {
|
|
42
42
|
trueWind: {
|
|
43
43
|
type: 'boolean',
|
|
44
44
|
title: 'Publish true wind',
|
|
45
|
-
description: 'Publishes environment.wind.speedTrue, environment.wind.directionTrue and environment.wind.angleTrueWater.
|
|
45
|
+
description: 'Publishes environment.wind.speedTrue, environment.wind.directionTrue and environment.wind.angleTrueWater. These are also the true wind values used for the internal polar speed calculation.',
|
|
46
46
|
default: true
|
|
47
47
|
},
|
|
48
48
|
apparentWind: {
|
|
@@ -60,7 +60,7 @@ function buildSchema () {
|
|
|
60
60
|
pollIntervalSeconds: {
|
|
61
61
|
type: 'number',
|
|
62
62
|
title: 'Weather polling interval in seconds',
|
|
63
|
-
description: 'How often the simulator refreshes wind from the Weather API
|
|
63
|
+
description: 'How often the simulator refreshes wind from the Weather API for its speed calculation and optional wind publication.',
|
|
64
64
|
default: 60,
|
|
65
65
|
minimum: 30
|
|
66
66
|
}
|
|
@@ -69,7 +69,7 @@ function buildSchema () {
|
|
|
69
69
|
publishing: {
|
|
70
70
|
type: 'object',
|
|
71
71
|
title: 'Navigation output',
|
|
72
|
-
description: 'What it does: publishes the simulated boat navigation state. Requires steering.autopilot.output.turnRate to steer and
|
|
72
|
+
description: 'What it does: publishes the simulated boat navigation state. Requires steering.autopilot.output.turnRate to steer, an active polar from signalk-polar-management, and weather wind to calculate boat speed. If navigation.magneticVariation is available, navigation.headingMagnetic is also published. Recommended providers: signalk-autopilot-emulator-v2, signalk-polar-management and signalk-derived-data with navigation.magneticVariation publishing enabled.',
|
|
73
73
|
properties: {
|
|
74
74
|
navigation: {
|
|
75
75
|
type: 'boolean',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-sailboat-simulator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Signal K plugin that simulates a sailing boat from autopilot turn-rate output, weather and polar
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Signal K plugin that simulates a sailing boat from autopilot turn-rate output, weather and an active polar resource.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node --test"
|
|
@@ -33,16 +33,17 @@
|
|
|
33
33
|
"url": "https://github.com/macjl/signalk-sailboat-simulator/issues"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"polar-math": "^1.0.0"
|
|
37
40
|
},
|
|
38
|
-
"dependencies": {},
|
|
39
|
-
"devDependencies": {},
|
|
40
41
|
"signalk-plugin-enabled-by-default": false,
|
|
41
42
|
"signalk": {
|
|
42
43
|
"displayName": "Sailboat Simulator",
|
|
43
44
|
"recommends": [
|
|
44
45
|
"signalk-autopilot-emulator-v2",
|
|
45
|
-
"signalk-polar-
|
|
46
|
+
"signalk-polar-management",
|
|
46
47
|
"signalk-derived-data",
|
|
47
48
|
"@signalk/open-meteo-provider",
|
|
48
49
|
"signalk-distance-to-shore"
|