signalk-sailboat-simulator 0.1.0 → 0.2.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 +6 -0
- package/README.md +14 -32
- package/index.js +17 -40
- package/lib/plugin-schema.js +3 -3
- package/lib/simulation-engine.js +4 -116
- package/lib/state-store.js +0 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-08-17
|
|
4
|
+
|
|
5
|
+
- Follow `steering.autopilot.output.turnRate` from `signalk-autopilot-emulator-v2` directly instead of deriving and rate-limiting turns from autopilot target headings.
|
|
6
|
+
- Remove simulator-side heading target and wind steering logic; the autopilot emulator is now responsible for producing the turn-rate output.
|
|
7
|
+
- Keep simulator steering based on true heading and publish `navigation.headingMagnetic` only when `navigation.magneticVariation` is available.
|
|
8
|
+
|
|
3
9
|
## [0.1.0] - 2026-08-14
|
|
4
10
|
|
|
5
11
|
- Initial public release.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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:
|
|
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 performance.
|
|
4
4
|
|
|
5
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 calculations.
|
|
6
6
|
|
|
@@ -10,13 +10,13 @@ The easiest setup is to use the Signal K App Store:
|
|
|
10
10
|
|
|
11
11
|
1. Install these recommended plugins:
|
|
12
12
|
- `@signalk/open-meteo-provider`
|
|
13
|
-
-
|
|
13
|
+
- `signalk-autopilot-emulator-v2`
|
|
14
14
|
- `signalk-polar-performance-plugin`
|
|
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
18
|
3. Configure `signalk-polar-performance-plugin` with a polar file for the simulated boat.
|
|
19
|
-
4. Configure `signalk-derived-data` to publish `navigation.magneticVariation
|
|
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.
|
|
22
22
|
7. Open the Signal K Data Browser or Freeboard and check that the simulated vessel position, wind and boat speed are updating.
|
|
@@ -34,10 +34,10 @@ Then restart Signal K and enable the plugin from the Server > Plugin Config page
|
|
|
34
34
|
|
|
35
35
|
Minimum capabilities for a useful simulation:
|
|
36
36
|
|
|
37
|
-
- Autopilot
|
|
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
39
|
- Polar performance: `signalk-polar-performance-plugin` uses the published true wind values to calculate `performance.polarSpeed`, which the simulator uses as boat speed.
|
|
40
|
-
- Derived data: enable the `signalk-derived-data` option that publishes `navigation.magneticVariation
|
|
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
|
|
|
43
43
|
## Verify It Works
|
|
@@ -67,17 +67,14 @@ Performance input expected by the simulator:
|
|
|
67
67
|
|
|
68
68
|
All values are published with `$source: signalk-sailboat-simulator`.
|
|
69
69
|
|
|
70
|
-
The plugin status will show `waitingForPerformance` until `performance.polarSpeed` is available. If grounding protection is enabled and `navigation.distanceToShore` is below the configured minimum, the status will show `groundingProtection` and the simulated boat will stop.
|
|
70
|
+
The plugin status will show `waitingForPerformance` until `performance.polarSpeed` is available. 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
|
|
|
74
74
|
The simulator reads these fixed Signal K input paths:
|
|
75
75
|
|
|
76
|
-
- `steering.autopilot.
|
|
77
|
-
- `
|
|
78
|
-
- `steering.autopilot.mode`: autopilot mode; when it is `wind`, wind steering is enabled
|
|
79
|
-
- `steering.autopilot.target.windAngleApparent`: desired apparent wind angle in radians
|
|
80
|
-
- `navigation.magneticVariation`: magnetic variation in radians, added to magnetic heading to derive true heading
|
|
76
|
+
- `steering.autopilot.output.turnRate`: desired heading change rate in rad/s
|
|
77
|
+
- `navigation.magneticVariation`: optional magnetic variation in radians, used only to publish `navigation.headingMagnetic`
|
|
81
78
|
- `performance.polarSpeed`: boat speed from the active polar in m/s
|
|
82
79
|
- `navigation.distanceToShore`: optional distance to the nearest coast in m, used for grounding protection
|
|
83
80
|
- `navigation.shore.bearingTrue`: optional bearing from the vessel to the nearest coast in radians, used to allow recovery headings away from shore
|
|
@@ -88,16 +85,6 @@ By default the simulator uses the Signal K default weather provider. Set `wind.p
|
|
|
88
85
|
|
|
89
86
|
When no usable weather data is available yet, for example while the weather provider is still starting up, the simulator retries faster instead of waiting for the normal polling interval.
|
|
90
87
|
|
|
91
|
-
If `steering.autopilot.target.headingTrue` is missing, the simulator derives the true heading from the autopilot magnetic target:
|
|
92
|
-
|
|
93
|
-
```text
|
|
94
|
-
headingTrue = steering.autopilot.target.headingMagnetic + navigation.magneticVariation
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
For the current Signal K autopilot emulator, `steering.autopilot.target` is also read as a fallback because the target value may be exposed there.
|
|
98
|
-
|
|
99
|
-
When the autopilot mode is `wind`, the simulator ignores stale heading targets and derives the boat heading from `steering.autopilot.target.windAngleApparent`, true wind speed/direction, and boat speed. This lets the virtual boat hold the requested apparent wind angle.
|
|
100
|
-
|
|
101
88
|
The simulator publishes virtual wind for the rest of the Signal K stack. The publish options are grouped as true wind and apparent wind:
|
|
102
89
|
|
|
103
90
|
- `environment.wind.speedTrue`
|
|
@@ -108,7 +95,7 @@ The simulator publishes virtual wind for the rest of the Signal K stack. The pub
|
|
|
108
95
|
|
|
109
96
|
This gives `signalk-polar-performance-plugin` the `environment.wind.speedTrue` and `environment.wind.angleTrueWater` inputs it needs to calculate `performance.polarSpeed`.
|
|
110
97
|
|
|
111
|
-
The first version uses `performance.polarSpeed` as the boat speed and integrates
|
|
98
|
+
The first version uses `performance.polarSpeed` as the 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.
|
|
112
99
|
|
|
113
100
|
## Configuration
|
|
114
101
|
|
|
@@ -129,9 +116,9 @@ The plugin intentionally keeps its configuration surface small:
|
|
|
129
116
|
|
|
130
117
|
- `publishing`
|
|
131
118
|
What it does: publishes the simulated navigation state.
|
|
132
|
-
Requires: `steering.autopilot
|
|
119
|
+
Requires: `steering.autopilot.output.turnRate` to steer and `performance.polarSpeed` to move. Recommended providers are `signalk-autopilot-emulator-v2`, `signalk-polar-performance-plugin`, and `signalk-derived-data` when magnetic heading output is needed.
|
|
133
120
|
Options: publish navigation state.
|
|
134
|
-
Published paths: `navigation.position`, `navigation.headingTrue`, `navigation.headingMagnetic` when available, `navigation.courseOverGroundTrue`, `navigation.speedOverGround`, `navigation.speedThroughWater`.
|
|
121
|
+
Published paths: `navigation.position`, `navigation.headingTrue`, `navigation.headingMagnetic` when `navigation.magneticVariation` is available, `navigation.courseOverGroundTrue`, `navigation.speedOverGround`, `navigation.speedThroughWater`.
|
|
135
122
|
|
|
136
123
|
- `grounding`
|
|
137
124
|
What it does: stops the simulated boat when it gets too close to shore, while still allowing headings that move away from the nearest shore.
|
|
@@ -146,10 +133,6 @@ The plugin intentionally keeps its configuration surface small:
|
|
|
146
133
|
|
|
147
134
|
Signal K input paths, simulation tick timing, turn rate and persistence save timing are fixed defaults rather than UI options.
|
|
148
135
|
|
|
149
|
-
## Boat Dynamics
|
|
150
|
-
|
|
151
|
-
The simulator does not instantly snap to the autopilot target heading. It turns the virtual boat toward the target at `dynamics.maxTurnRateDegPerSecond`, which defaults to 3 degrees per second.
|
|
152
|
-
|
|
153
136
|
## Grounding Protection
|
|
154
137
|
|
|
155
138
|
When `grounding.enabled` is true, the simulator reads `navigation.distanceToShore` and stops the virtual boat when the value is less than or equal to `grounding.minimumDistanceToShore`, which defaults to 20 meters. If `navigation.shore.bearingTrue` is available, the simulator still allows movement when the selected heading points away from the nearest shore.
|
|
@@ -163,8 +146,7 @@ The simulator saves its latest runtime state every 10 seconds by default and res
|
|
|
163
146
|
Persisted values include:
|
|
164
147
|
|
|
165
148
|
- position
|
|
166
|
-
- true
|
|
167
|
-
- magnetic variation
|
|
149
|
+
- true heading
|
|
168
150
|
- COG, SOG and STW
|
|
169
151
|
|
|
170
152
|
## Uninstall
|
|
@@ -184,9 +166,9 @@ Then restart Signal K.
|
|
|
184
166
|
|
|
185
167
|
- Signal K Weather Providers: https://demo.signalk.org/documentation/Developing/Plugins/Weather_Providers.html
|
|
186
168
|
- Signal K Weather API: https://demo.signalk.org/documentation/Developing/REST_APIs/Weather_API.html
|
|
187
|
-
- Signal K Autopilot Providers: https://demo.signalk.org/documentation/develop/plugins/autopilot_provider_plugins.html
|
|
188
169
|
- `@signalk/open-meteo-provider`: https://www.npmjs.com/package/@signalk/open-meteo-provider
|
|
189
|
-
-
|
|
170
|
+
- `signalk-autopilot-emulator-v2`: https://www.npmjs.com/package/signalk-autopilot-emulator-v2
|
|
171
|
+
- `signalk-autopilot-emulator-v2` source: https://github.com/macjl/signalk-autopilot-emulator-v2
|
|
190
172
|
- `signalk-polar-performance-plugin`: https://www.npmjs.com/package/signalk-polar-performance-plugin
|
|
191
173
|
- `signalk-derived-data`: https://www.npmjs.com/package/signalk-derived-data
|
|
192
174
|
- `signalk-distance-to-shore`: https://www.npmjs.com/package/signalk-distance-to-shore
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { createInitialState, stepSimulation } = require('./lib/simulation-engine')
|
|
4
4
|
const { buildSchema } = require('./lib/plugin-schema')
|
|
5
|
-
const { degToRad, radToDeg } = require('./lib/angles')
|
|
5
|
+
const { degToRad, radToDeg, wrap360Rad } = require('./lib/angles')
|
|
6
6
|
const { apparentWindFromTrue, trueWindFromDirection, windSnapshotFromObservation } = require('./lib/wind')
|
|
7
7
|
const { applyPersistedState, createStateStore, stateFromSimulation } = require('./lib/state-store')
|
|
8
8
|
|
|
@@ -10,11 +10,7 @@ const PLUGIN_ID = 'sailboat-simulator'
|
|
|
10
10
|
const PUBLISH_SOURCE = 'signalk-sailboat-simulator'
|
|
11
11
|
|
|
12
12
|
const INPUT_PATHS = {
|
|
13
|
-
|
|
14
|
-
targetHeadingTruePath: 'steering.autopilot.target.headingTrue',
|
|
15
|
-
targetHeadingMagneticPath: 'steering.autopilot.target.headingMagnetic',
|
|
16
|
-
targetHeadingMagneticFallbackPath: 'steering.autopilot.target',
|
|
17
|
-
targetWindAngleApparentPath: 'steering.autopilot.target.windAngleApparent',
|
|
13
|
+
turnRatePath: 'steering.autopilot.output.turnRate',
|
|
18
14
|
magneticVariationPath: 'navigation.magneticVariation',
|
|
19
15
|
performanceSpeedPath: 'performance.polarSpeed',
|
|
20
16
|
distanceToShorePath: 'navigation.distanceToShore',
|
|
@@ -40,9 +36,6 @@ const DEFAULT_OPTIONS = {
|
|
|
40
36
|
fallback: {
|
|
41
37
|
speedThroughWater: 0
|
|
42
38
|
},
|
|
43
|
-
dynamics: {
|
|
44
|
-
maxTurnRateDegPerSecond: 3
|
|
45
|
-
},
|
|
46
39
|
grounding: {
|
|
47
40
|
enabled: true,
|
|
48
41
|
minimumDistanceToShore: 20
|
|
@@ -71,7 +64,7 @@ module.exports = function createPlugin (app) {
|
|
|
71
64
|
const plugin = {
|
|
72
65
|
id: PLUGIN_ID,
|
|
73
66
|
name: 'Sailboat Simulator',
|
|
74
|
-
description: 'Simulates a sailing boat position from Signal K
|
|
67
|
+
description: 'Simulates a sailing boat position from Signal K autopilot turn-rate output, weather and polar performance data.',
|
|
75
68
|
schema: buildSchema,
|
|
76
69
|
start,
|
|
77
70
|
stop,
|
|
@@ -136,13 +129,7 @@ module.exports = function createPlugin (app) {
|
|
|
136
129
|
function readInputs () {
|
|
137
130
|
const weatherWind = freshWeatherWind()
|
|
138
131
|
return {
|
|
139
|
-
|
|
140
|
-
targetHeadingTrue: readNumber(INPUT_PATHS.targetHeadingTruePath),
|
|
141
|
-
targetHeadingMagnetic: readFirstNumber([
|
|
142
|
-
INPUT_PATHS.targetHeadingMagneticPath,
|
|
143
|
-
INPUT_PATHS.targetHeadingMagneticFallbackPath
|
|
144
|
-
]),
|
|
145
|
-
targetWindAngleApparent: readNumber(INPUT_PATHS.targetWindAngleApparentPath),
|
|
132
|
+
turnRate: readNumber(INPUT_PATHS.turnRatePath),
|
|
146
133
|
magneticVariation: readNumber(INPUT_PATHS.magneticVariationPath),
|
|
147
134
|
polarSpeed: readNumber(INPUT_PATHS.performanceSpeedPath),
|
|
148
135
|
windSpeedTrue: weatherWind && weatherWind.speedTrue != null
|
|
@@ -164,20 +151,6 @@ module.exports = function createPlugin (app) {
|
|
|
164
151
|
return Number.isFinite(value) ? value : null
|
|
165
152
|
}
|
|
166
153
|
|
|
167
|
-
function readString (path) {
|
|
168
|
-
if (!path || typeof app.getSelfPath !== 'function') return null
|
|
169
|
-
const value = app.getSelfPath(`${path}.value`)
|
|
170
|
-
return typeof value === 'string' ? value : null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function readFirstNumber (paths) {
|
|
174
|
-
for (const path of paths) {
|
|
175
|
-
const value = readNumber(path)
|
|
176
|
-
if (Number.isFinite(value)) return value
|
|
177
|
-
}
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
|
|
181
154
|
function publishState (inputs) {
|
|
182
155
|
if (!state || !app.handleMessage) return
|
|
183
156
|
|
|
@@ -191,12 +164,13 @@ module.exports = function createPlugin (app) {
|
|
|
191
164
|
}
|
|
192
165
|
})
|
|
193
166
|
values.push({ path: 'navigation.headingTrue', value: state.headingTrue })
|
|
167
|
+
const headingMagnetic = magneticHeadingFromTrue(state.headingTrue, inputs.magneticVariation)
|
|
168
|
+
if (Number.isFinite(headingMagnetic)) {
|
|
169
|
+
values.push({ path: 'navigation.headingMagnetic', value: headingMagnetic })
|
|
170
|
+
}
|
|
194
171
|
values.push({ path: 'navigation.courseOverGroundTrue', value: state.courseOverGroundTrue })
|
|
195
172
|
values.push({ path: 'navigation.speedOverGround', value: state.speedOverGround })
|
|
196
173
|
values.push({ path: 'navigation.speedThroughWater', value: state.speedThroughWater })
|
|
197
|
-
if (Number.isFinite(state.headingMagnetic)) {
|
|
198
|
-
values.push({ path: 'navigation.headingMagnetic', value: state.headingMagnetic })
|
|
199
|
-
}
|
|
200
174
|
}
|
|
201
175
|
values.push(...buildWindValues(inputs))
|
|
202
176
|
|
|
@@ -379,14 +353,15 @@ module.exports = function createPlugin (app) {
|
|
|
379
353
|
}
|
|
380
354
|
|
|
381
355
|
function updateRuntime (inputs) {
|
|
356
|
+
const headingMagnetic = magneticHeadingFromTrue(state.headingTrue, inputs.magneticVariation)
|
|
382
357
|
runtime = {
|
|
383
358
|
status: state.groundingProtectionActive
|
|
384
359
|
? 'groundingProtection'
|
|
385
360
|
: Number.isFinite(state.speedThroughWater) && state.speedThroughWater > 0 ? 'sailing' : 'waitingForPerformance',
|
|
386
361
|
position: state.position,
|
|
387
362
|
headingTrueDeg: radToDeg(state.headingTrue),
|
|
388
|
-
headingMagneticDeg: Number.isFinite(
|
|
389
|
-
magneticVariationDeg: Number.isFinite(
|
|
363
|
+
headingMagneticDeg: Number.isFinite(headingMagnetic) ? radToDeg(headingMagnetic) : null,
|
|
364
|
+
magneticVariationDeg: Number.isFinite(inputs.magneticVariation) ? radToDeg(inputs.magneticVariation) : null,
|
|
390
365
|
courseOverGroundTrueDeg: radToDeg(state.courseOverGroundTrue),
|
|
391
366
|
speedOverGround: state.speedOverGround,
|
|
392
367
|
speedThroughWater: state.speedThroughWater,
|
|
@@ -404,10 +379,7 @@ module.exports = function createPlugin (app) {
|
|
|
404
379
|
}
|
|
405
380
|
: { status: publishesAnyWind() ? 'missing' : 'disabled' },
|
|
406
381
|
inputs: {
|
|
407
|
-
|
|
408
|
-
targetHeadingTrue: valueStatus(inputs.targetHeadingTrue),
|
|
409
|
-
targetHeadingMagnetic: valueStatus(inputs.targetHeadingMagnetic),
|
|
410
|
-
targetWindAngleApparent: valueStatus(inputs.targetWindAngleApparent),
|
|
382
|
+
turnRate: valueStatus(inputs.turnRate),
|
|
411
383
|
magneticVariation: valueStatus(inputs.magneticVariation),
|
|
412
384
|
performanceSpeed: valueStatus(inputs.polarSpeed),
|
|
413
385
|
distanceToShore: valueStatus(inputs.distanceToShore),
|
|
@@ -486,6 +458,11 @@ function valueStatus (value) {
|
|
|
486
458
|
return Number.isFinite(value) ? 'present' : 'missing'
|
|
487
459
|
}
|
|
488
460
|
|
|
461
|
+
function magneticHeadingFromTrue (headingTrue, magneticVariation) {
|
|
462
|
+
if (!Number.isFinite(headingTrue) || !Number.isFinite(magneticVariation)) return null
|
|
463
|
+
return wrap360Rad(headingTrue - magneticVariation)
|
|
464
|
+
}
|
|
465
|
+
|
|
489
466
|
function dateDistanceMs (date, now) {
|
|
490
467
|
const time = new Date(date).getTime()
|
|
491
468
|
if (!Number.isFinite(time)) return null
|
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
|
|
7
|
+
description: 'Simulates a virtual sailboat from autopilot turn-rate output, polar performance, weather and optional shore-distance data already present in Signal K.',
|
|
8
8
|
properties: {
|
|
9
9
|
initialState: {
|
|
10
10
|
type: 'object',
|
|
@@ -69,12 +69,12 @@ 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
|
|
72
|
+
description: 'What it does: publishes the simulated boat navigation state. Requires steering.autopilot.output.turnRate to steer and performance.polarSpeed to move. If navigation.magneticVariation is available, navigation.headingMagnetic is also published. Recommended providers: signalk-autopilot-emulator-v2, signalk-polar-performance-plugin and signalk-derived-data with navigation.magneticVariation publishing enabled.',
|
|
73
73
|
properties: {
|
|
74
74
|
navigation: {
|
|
75
75
|
type: 'boolean',
|
|
76
76
|
title: 'Publish navigation state',
|
|
77
|
-
description: 'Publishes navigation.position, navigation.headingTrue, navigation.headingMagnetic when available, navigation.courseOverGroundTrue, navigation.speedOverGround and navigation.speedThroughWater.',
|
|
77
|
+
description: 'Publishes navigation.position, navigation.headingTrue, navigation.headingMagnetic when magnetic variation is available, navigation.courseOverGroundTrue, navigation.speedOverGround and navigation.speedThroughWater.',
|
|
78
78
|
default: true
|
|
79
79
|
}
|
|
80
80
|
}
|
package/lib/simulation-engine.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { wrap180Rad, wrap360Rad } = require('./angles')
|
|
4
4
|
const { destinationPoint } = require('./geo')
|
|
5
|
-
const {
|
|
5
|
+
const { trueWindFromDirection } = require('./wind')
|
|
6
6
|
|
|
7
7
|
function createInitialState (options) {
|
|
8
8
|
return {
|
|
@@ -11,8 +11,6 @@ function createInitialState (options) {
|
|
|
11
11
|
longitude: options.initialState.longitude
|
|
12
12
|
},
|
|
13
13
|
headingTrue: wrap360Rad(options.initialState.headingTrue),
|
|
14
|
-
headingMagnetic: null,
|
|
15
|
-
magneticVariation: null,
|
|
16
14
|
courseOverGroundTrue: wrap360Rad(options.initialState.headingTrue),
|
|
17
15
|
speedOverGround: 0,
|
|
18
16
|
speedThroughWater: 0,
|
|
@@ -31,27 +29,8 @@ function stepSimulation (state, inputs, options, now) {
|
|
|
31
29
|
options.fallback.speedThroughWater,
|
|
32
30
|
0
|
|
33
31
|
))
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
targetWindAngleApparent: inputs.targetWindAngleApparent,
|
|
37
|
-
windDirectionTrue: inputs.windDirectionTrue,
|
|
38
|
-
trueWindSpeed: inputs.windSpeedTrue,
|
|
39
|
-
boatSpeed: requestedSpeedThroughWater
|
|
40
|
-
})
|
|
41
|
-
: null
|
|
42
|
-
const targetHeadingTrue = firstFinite(
|
|
43
|
-
windSteerHeadingTrue,
|
|
44
|
-
inputs.targetHeadingTrue,
|
|
45
|
-
trueHeadingFromMagnetic(inputs.targetHeadingMagnetic, inputs.magneticVariation),
|
|
46
|
-
state.headingTrue,
|
|
47
|
-
options.initialState.headingTrue
|
|
48
|
-
)
|
|
49
|
-
const headingTrue = approachHeading(
|
|
50
|
-
state.headingTrue,
|
|
51
|
-
targetHeadingTrue,
|
|
52
|
-
turnRateRadPerSecond(options),
|
|
53
|
-
stepSeconds
|
|
54
|
-
)
|
|
32
|
+
const turnRate = Number.isFinite(inputs.turnRate) ? inputs.turnRate : 0
|
|
33
|
+
const headingTrue = wrap360Rad(state.headingTrue + turnRate * stepSeconds)
|
|
55
34
|
const groundingProtectionActive = shouldStopForGrounding(inputs, options, headingTrue)
|
|
56
35
|
const speedThroughWater = groundingProtectionActive
|
|
57
36
|
? 0
|
|
@@ -64,8 +43,6 @@ function stepSimulation (state, inputs, options, now) {
|
|
|
64
43
|
return {
|
|
65
44
|
position,
|
|
66
45
|
headingTrue: wrap360Rad(headingTrue),
|
|
67
|
-
headingMagnetic: magneticHeadingFromTrue(headingTrue, inputs.magneticVariation),
|
|
68
|
-
magneticVariation: Number.isFinite(inputs.magneticVariation) ? inputs.magneticVariation : null,
|
|
69
46
|
courseOverGroundTrue: wrap360Rad(headingTrue),
|
|
70
47
|
speedOverGround: speedThroughWater,
|
|
71
48
|
speedThroughWater,
|
|
@@ -75,27 +52,6 @@ function stepSimulation (state, inputs, options, now) {
|
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
|
|
78
|
-
function approachHeading (currentHeading, targetHeading, maxTurnRate, stepSeconds) {
|
|
79
|
-
if (!Number.isFinite(currentHeading) || !Number.isFinite(targetHeading)) return targetHeading
|
|
80
|
-
if (!Number.isFinite(maxTurnRate)) return wrap360Rad(targetHeading)
|
|
81
|
-
if (maxTurnRate <= 0 || stepSeconds <= 0) return wrap360Rad(currentHeading)
|
|
82
|
-
|
|
83
|
-
const delta = wrap180Rad(targetHeading - currentHeading)
|
|
84
|
-
const maxDelta = maxTurnRate * stepSeconds
|
|
85
|
-
if (Math.abs(delta) <= maxDelta) return wrap360Rad(targetHeading)
|
|
86
|
-
|
|
87
|
-
return wrap360Rad(currentHeading + Math.sign(delta) * maxDelta)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function turnRateRadPerSecond (options) {
|
|
91
|
-
if (!options.dynamics) return Infinity
|
|
92
|
-
if (Number.isFinite(options.dynamics.maxTurnRate)) return Math.max(0, options.dynamics.maxTurnRate)
|
|
93
|
-
if (Number.isFinite(options.dynamics.maxTurnRateDegPerSecond)) {
|
|
94
|
-
return Math.max(0, options.dynamics.maxTurnRateDegPerSecond) * Math.PI / 180
|
|
95
|
-
}
|
|
96
|
-
return Infinity
|
|
97
|
-
}
|
|
98
|
-
|
|
99
55
|
function shouldStopForGrounding (inputs, options, headingTrue) {
|
|
100
56
|
const tooClose = Boolean(
|
|
101
57
|
options.grounding &&
|
|
@@ -113,71 +69,6 @@ function isHeadingAwayFromShore (headingTrue, shoreBearingTrue) {
|
|
|
113
69
|
return Math.abs(wrap180Rad(headingTrue - shoreBearingTrue)) > Math.PI / 2
|
|
114
70
|
}
|
|
115
71
|
|
|
116
|
-
function trueHeadingFromMagnetic (headingMagnetic, magneticVariation) {
|
|
117
|
-
if (!Number.isFinite(headingMagnetic) || !Number.isFinite(magneticVariation)) return null
|
|
118
|
-
return wrap360Rad(headingMagnetic + magneticVariation)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function magneticHeadingFromTrue (headingTrue, magneticVariation) {
|
|
122
|
-
if (!Number.isFinite(headingTrue) || !Number.isFinite(magneticVariation)) return null
|
|
123
|
-
return wrap360Rad(headingTrue - magneticVariation)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function trueHeadingFromApparentWind ({
|
|
127
|
-
targetWindAngleApparent,
|
|
128
|
-
windDirectionTrue,
|
|
129
|
-
trueWindSpeed,
|
|
130
|
-
boatSpeed
|
|
131
|
-
}) {
|
|
132
|
-
if (
|
|
133
|
-
!Number.isFinite(targetWindAngleApparent) ||
|
|
134
|
-
!Number.isFinite(windDirectionTrue) ||
|
|
135
|
-
!Number.isFinite(trueWindSpeed)
|
|
136
|
-
) return null
|
|
137
|
-
|
|
138
|
-
const trueWindAngle = trueWindAngleFromApparent({
|
|
139
|
-
targetWindAngleApparent,
|
|
140
|
-
trueWindSpeed,
|
|
141
|
-
boatSpeed: Number.isFinite(boatSpeed) ? boatSpeed : 0
|
|
142
|
-
})
|
|
143
|
-
if (!Number.isFinite(trueWindAngle)) return null
|
|
144
|
-
|
|
145
|
-
return wrap360Rad(windDirectionTrue - trueWindAngle)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function trueWindAngleFromApparent ({
|
|
149
|
-
targetWindAngleApparent,
|
|
150
|
-
trueWindSpeed,
|
|
151
|
-
boatSpeed
|
|
152
|
-
}) {
|
|
153
|
-
const awa = wrap180Rad(targetWindAngleApparent)
|
|
154
|
-
const tws = Math.max(0, trueWindSpeed)
|
|
155
|
-
const stw = Math.max(0, Number.isFinite(boatSpeed) ? boatSpeed : 0)
|
|
156
|
-
|
|
157
|
-
if (tws === 0) return null
|
|
158
|
-
if (stw === 0) return awa
|
|
159
|
-
|
|
160
|
-
const crosswind = stw * Math.sin(awa)
|
|
161
|
-
const discriminant = (tws * tws) - (crosswind * crosswind)
|
|
162
|
-
if (discriminant < 0) return awa
|
|
163
|
-
|
|
164
|
-
const apparentSpeed = (stw * Math.cos(awa)) + Math.sqrt(discriminant)
|
|
165
|
-
if (apparentSpeed <= 0) return awa
|
|
166
|
-
|
|
167
|
-
const trueForward = (apparentSpeed * Math.cos(awa)) - stw
|
|
168
|
-
const trueStarboard = apparentSpeed * Math.sin(awa)
|
|
169
|
-
const twa = Math.atan2(trueStarboard, trueForward)
|
|
170
|
-
|
|
171
|
-
const apparent = apparentWindFromTrue({
|
|
172
|
-
trueWindSpeed: tws,
|
|
173
|
-
trueWindAngle: twa,
|
|
174
|
-
boatSpeed: stw
|
|
175
|
-
})
|
|
176
|
-
if (!apparent) return awa
|
|
177
|
-
|
|
178
|
-
return twa
|
|
179
|
-
}
|
|
180
|
-
|
|
181
72
|
function deriveWindAngleTrueWater (windDirectionTrue, headingTrue) {
|
|
182
73
|
return trueWindFromDirection(windDirectionTrue, headingTrue)
|
|
183
74
|
}
|
|
@@ -189,8 +80,5 @@ function firstFinite (...values) {
|
|
|
189
80
|
module.exports = {
|
|
190
81
|
createInitialState,
|
|
191
82
|
deriveWindAngleTrueWater,
|
|
192
|
-
|
|
193
|
-
stepSimulation,
|
|
194
|
-
trueHeadingFromApparentWind,
|
|
195
|
-
trueHeadingFromMagnetic
|
|
83
|
+
stepSimulation
|
|
196
84
|
}
|
package/lib/state-store.js
CHANGED
|
@@ -48,8 +48,6 @@ function stateFromSimulation (state) {
|
|
|
48
48
|
longitude: state.position.longitude
|
|
49
49
|
},
|
|
50
50
|
headingTrue: state.headingTrue,
|
|
51
|
-
headingMagnetic: state.headingMagnetic,
|
|
52
|
-
magneticVariation: state.magneticVariation,
|
|
53
51
|
courseOverGroundTrue: state.courseOverGroundTrue,
|
|
54
52
|
speedOverGround: state.speedOverGround,
|
|
55
53
|
speedThroughWater: state.speedThroughWater
|
|
@@ -65,8 +63,6 @@ function applyPersistedState (state, persisted) {
|
|
|
65
63
|
longitude: persisted.position.longitude
|
|
66
64
|
},
|
|
67
65
|
headingTrue: numberOr(persisted.headingTrue, state.headingTrue),
|
|
68
|
-
headingMagnetic: numberOr(persisted.headingMagnetic, state.headingMagnetic),
|
|
69
|
-
magneticVariation: numberOr(persisted.magneticVariation, state.magneticVariation),
|
|
70
66
|
courseOverGroundTrue: numberOr(persisted.courseOverGroundTrue, state.courseOverGroundTrue),
|
|
71
67
|
speedOverGround: numberOr(persisted.speedOverGround, 0),
|
|
72
68
|
speedThroughWater: numberOr(persisted.speedThroughWater, 0),
|
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
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Signal K plugin that simulates a sailing boat from autopilot turn-rate output, weather and polar performance data.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node --test"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"signalk": {
|
|
42
42
|
"displayName": "Sailboat Simulator",
|
|
43
43
|
"recommends": [
|
|
44
|
-
"
|
|
44
|
+
"signalk-autopilot-emulator-v2",
|
|
45
45
|
"signalk-polar-performance-plugin",
|
|
46
46
|
"signalk-derived-data",
|
|
47
47
|
"@signalk/open-meteo-provider",
|