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 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: target heading, weather and polar performance.
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
- - `@signalk/signalk-autopilot`
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`. This is needed by the autopilot plugin when magnetic headings are involved, and lets the simulator publish `navigation.headingMagnetic`.
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 intent: `@signalk/signalk-autopilot` can provide heading and wind-mode targets under `steering.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`. The autopilot plugin needs it for magnetic heading handling, and the simulator uses it to derive or publish magnetic headings.
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.target.headingTrue`: desired true heading in radians
77
- - `steering.autopilot.target.headingMagnetic`: desired magnetic heading in radians
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 the position along the target heading. Current, leeway, route following and manoeuvre rules are intentionally left as separate steps.
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.*` target data to steer and `performance.polarSpeed` to move. Recommended providers are `@signalk/signalk-autopilot` and `signalk-polar-performance-plugin`.
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 and magnetic heading
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
- - `@signalk/signalk-autopilot`: https://www.npmjs.com/package/@signalk/signalk-autopilot
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
- autopilotModePath: 'steering.autopilot.mode',
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 heading, weather and polar performance data.',
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
- autopilotMode: readString(INPUT_PATHS.autopilotModePath) || readString('steering.autopilot.state'),
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(state.headingMagnetic) ? radToDeg(state.headingMagnetic) : null,
389
- magneticVariationDeg: Number.isFinite(state.magneticVariation) ? radToDeg(state.magneticVariation) : null,
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
- autopilotMode: inputs.autopilotMode || null,
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
@@ -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 intent, polar performance, weather and optional shore-distance data already present in Signal K.',
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: steering.autopilot target data to steer and performance.polarSpeed to move. Recommended providers: @signalk/signalk-autopilot, signalk-polar-performance-plugin and signalk-derived-data with navigation.magneticVariation publishing enabled for magnetic heading handling.',
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
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { wrap180Rad, wrap360Rad } = require('./angles')
4
4
  const { destinationPoint } = require('./geo')
5
- const { apparentWindFromTrue, trueWindFromDirection } = require('./wind')
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 windSteerHeadingTrue = inputs.autopilotMode === 'wind'
35
- ? trueHeadingFromApparentWind({
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
- magneticHeadingFromTrue,
193
- stepSimulation,
194
- trueHeadingFromApparentWind,
195
- trueHeadingFromMagnetic
83
+ stepSimulation
196
84
  }
@@ -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.1.0",
4
- "description": "Signal K plugin that simulates a sailing boat from heading, weather and polar performance data.",
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
- "@signalk/signalk-autopilot",
44
+ "signalk-autopilot-emulator-v2",
45
45
  "signalk-polar-performance-plugin",
46
46
  "signalk-derived-data",
47
47
  "@signalk/open-meteo-provider",