signalk-navico-autopilot-bridge 0.3.0-alpha
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/LICENSE +21 -0
- package/README.md +379 -0
- package/index.js +206 -0
- package/lib/ac-emulator.js +609 -0
- package/lib/access-request.js +117 -0
- package/lib/control-head.js +118 -0
- package/lib/sk-autopilot.js +58 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johan Sölve
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# signalk-navico-autopilot-bridge
|
|
2
|
+
|
|
3
|
+
> **Status: 0.3.0-alpha.** Sea-trialled on a real rig (B&G Vulcan 7 → SignalK V2
|
|
4
|
+
> → Raymarine EV-200): engaging and holding Auto, ±course nudges, the abort /
|
|
5
|
+
> failsafe path and following a route leg all worked on the water. **Wind-mode
|
|
6
|
+
> display and Tack do not work yet**, and some MFD display frames are still
|
|
7
|
+
> unverified. One trial only, in light conditions. See
|
|
8
|
+
> [Known limitations](#known-limitations) and the
|
|
9
|
+
> [Disclaimer](#disclaimer--no-warranty) before using it.
|
|
10
|
+
|
|
11
|
+
Emulate a **Simrad AC12/AC42 autopilot computer** so a **Navico MFD** (B&G
|
|
12
|
+
Vulcan/Zeus, Simrad, Lowrance) binds to it and exposes its own **autopilot
|
|
13
|
+
control view**. The button presses that view puts on the NMEA 2000 bus (Simnet
|
|
14
|
+
`130850`) are decoded and translated into the **SignalK Autopilot V2 API**, which
|
|
15
|
+
drives whichever pilot backs it through an Autopilot V2 provider — for example a
|
|
16
|
+
Raymarine EV-200 via
|
|
17
|
+
[`signalk-autopilot`](https://github.com/SignalK/signalk-autopilot).
|
|
18
|
+
|
|
19
|
+
In other words: **press the autopilot buttons on a Navico plotter, steer a
|
|
20
|
+
non-Navico pilot.** The bridge is Navico/Simrad/B&G-specific on the *input* side
|
|
21
|
+
(only Navico MFDs bind to a Simrad AC) and **provider-agnostic on the output side**
|
|
22
|
+
(anything that implements the SignalK V2 Autopilot API).
|
|
23
|
+
|
|
24
|
+
## Disclaimer / no warranty
|
|
25
|
+
|
|
26
|
+
**An autopilot steers the boat. This software can move the rudder.** Read this
|
|
27
|
+
before you install it.
|
|
28
|
+
|
|
29
|
+
- **Alpha, experimental, unofficial.** This is a reverse-engineered emulation of
|
|
30
|
+
a proprietary Simrad device, built from bus captures — not from any
|
|
31
|
+
manufacturer specification. It is not affiliated with, endorsed by, or
|
|
32
|
+
supported by Navico, B&G, Simrad, Lowrance, Raymarine, or the SignalK project.
|
|
33
|
+
Behaviour may change or break with any firmware, plugin, or canboatjs update.
|
|
34
|
+
- **Not safety-rated.** It is **not** certified for marine navigation and must
|
|
35
|
+
**not** be relied on as a primary or sole means of steering or watchkeeping. A
|
|
36
|
+
competent helmsman must remain at the helm, keep a proper lookout, and be ready
|
|
37
|
+
to take manual control and drop the pilot to standby at all times.
|
|
38
|
+
- **It can fail silently or behave unexpectedly** — wrong mode, wrong course,
|
|
39
|
+
no response, or a course change at the wrong moment. Several features (Wind
|
|
40
|
+
display/adjust, Tack, route/track display) are unverified test candidates. Do
|
|
41
|
+
not trust it where a failure could cause a collision, grounding, injury, or
|
|
42
|
+
loss of life.
|
|
43
|
+
- **You are responsible.** By installing or running this software you accept full
|
|
44
|
+
responsibility for any consequences. Only ever use it with the **boat secured,
|
|
45
|
+
the engine off, and the pilot's own control head to hand**, until you have
|
|
46
|
+
personally sea-trialled every mode you intend to rely on.
|
|
47
|
+
- **No warranty.** Provided "AS IS", without warranty of any kind, express or
|
|
48
|
+
implied, including but not limited to fitness for a particular purpose. To the
|
|
49
|
+
maximum extent permitted by law the authors accept no liability for any damage,
|
|
50
|
+
injury, or loss arising from its use. See [License](#license).
|
|
51
|
+
|
|
52
|
+
## Why an emulator (the "firehose")
|
|
53
|
+
|
|
54
|
+
A real AC is anything but quiet: it broadcasts a full set of proprietary
|
|
55
|
+
state/telemetry PGNs (`65340` Pilot State, `65305` Device Status, `65341` AP
|
|
56
|
+
Angle, `65302`/`65420`/`130860`/`128275`, …) at 1–2 Hz. A near-silent fake is
|
|
57
|
+
ranked below a live pilot and never selected, and the commissioning "press
|
|
58
|
+
standby" gate is satisfied by the AC's *own* continuous state broadcast. This
|
|
59
|
+
plugin reproduces that broadcast faithfully (byte templates taken verbatim from a
|
|
60
|
+
real AC42 in `canboat/samples/ac42-commissioning.raw`), which is what makes the
|
|
61
|
+
MFD bind to it and unlock the control view.
|
|
62
|
+
|
|
63
|
+
The emulator runs as a **second N2K device** on the bus (its own address claim,
|
|
64
|
+
default address 35), alongside the SignalK server's own canboat connection and any
|
|
65
|
+
real pilot. It opens its own SocketCAN socket — it does **not** route through
|
|
66
|
+
SignalK's N2K output, because the firehose must be sent *as the emulated AC*, not
|
|
67
|
+
as the server.
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
1. **Firehose** — broadcasts the AC's proprietary AP state/telemetry so the MFD
|
|
72
|
+
binds and keeps the control view live. Mode bytes follow the live SignalK
|
|
73
|
+
autopilot state (read back over the loopback API).
|
|
74
|
+
2. **Commissioning readback** — answers the MFD's `130845` dockside-config reads
|
|
75
|
+
with byte-exact values from the reference AC42, so the wizard fields populate.
|
|
76
|
+
The control view unlocks **without finishing the wizard**: the values you key
|
|
77
|
+
in (rudder calibration, drive voltage, rudder test, …) are not consumed by
|
|
78
|
+
anything that steers — the backing pilot has its own commissioning — so they
|
|
79
|
+
can be left at defaults or skipped. The one setting that matters is **boat
|
|
80
|
+
type**: set it to **Sail** so the MFD exposes wind mode and the tack buttons
|
|
81
|
+
(per the Vulcan manual those functions require a Sail boat type).
|
|
82
|
+
3. **Input bridge** — reassembles incoming `130850` commands, decodes the Simnet
|
|
83
|
+
key byte, and (in `live` mode) calls the V2 Autopilot API.
|
|
84
|
+
|
|
85
|
+
The byte-level command and mode-display decoding lives in
|
|
86
|
+
[Protocol details](#protocol-details).
|
|
87
|
+
|
|
88
|
+
## Setup
|
|
89
|
+
|
|
90
|
+
Do the steps below in order:
|
|
91
|
+
|
|
92
|
+
1. [Install](#1-install) the plugin from source.
|
|
93
|
+
2. [Configure](#2-configure-the-plugin) it (start in `dry-run`).
|
|
94
|
+
3. [Enable the autopilot on the MFD](#3-enable-the-autopilot-on-the-mfd).
|
|
95
|
+
4. [Run first commissioning](#4-first-commissioning) to bind the MFD and unlock
|
|
96
|
+
the control view.
|
|
97
|
+
5. [Set source priorities](#5-source-priorities-required) so the emulator can't
|
|
98
|
+
shadow the real pilot.
|
|
99
|
+
|
|
100
|
+
### 1. Install
|
|
101
|
+
|
|
102
|
+
Install it from the **SignalK app store**: in the SignalK admin UI open
|
|
103
|
+
**Appstore → Available**, search for *Navico autopilot bridge*, install it, and
|
|
104
|
+
restart the server. It is published as an **alpha** — read the
|
|
105
|
+
[Disclaimer](#disclaimer--no-warranty) first.
|
|
106
|
+
|
|
107
|
+
`@canboat/canboatjs` is a peerDependency already present in a SignalK server
|
|
108
|
+
install, so there is nothing else to install.
|
|
109
|
+
|
|
110
|
+
To install from source instead (e.g. for development):
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
cd ~/.signalk/node_modules
|
|
114
|
+
git clone https://github.com/johansolve/signalk-navico-autopilot-bridge.git
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Restart the SignalK server, then enable and configure the plugin under
|
|
118
|
+
**Server → Plugin Config**.
|
|
119
|
+
|
|
120
|
+
### 2. Configure the plugin
|
|
121
|
+
|
|
122
|
+
| option | default | notes |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| CAN interface | `can0` | SocketCAN interface |
|
|
125
|
+
| Emulated AC model | `AC42` | both `AC42` and `AC12` bind a Vulcan 7; the model only sets the reported product info, not whether it binds |
|
|
126
|
+
| Preferred N2K address | `35` | address the emulated AC claims |
|
|
127
|
+
| Broadcast AC autopilot state | `true` | the firehose; required for binding — leave on |
|
|
128
|
+
| Standard nav PGNs | `false` | duplicates other sources; A/B testing only |
|
|
129
|
+
| **Bridge mode** | **`dry-run`** | `off` / `dry-run` (decode+log) / `live` (steer) |
|
|
130
|
+
| Target autopilot id | `_default` | which `autopilots/<id>` the V2 API drives |
|
|
131
|
+
| SignalK host / port | `127.0.0.1` / `3000` | loopback API target |
|
|
132
|
+
| API token | — | leave empty — auto-requested, you approve it once (see [Access & token](#access--token)) |
|
|
133
|
+
| Commissioning mode | `false` | emulate a control head to open the first-commissioning gate (see below) |
|
|
134
|
+
| Commissioning head address | `44` | address the emulated head claims (commissioning only) |
|
|
135
|
+
|
|
136
|
+
#### ⚠️ Safety
|
|
137
|
+
|
|
138
|
+
- **Default is `dry-run`** — it decodes and logs commands but never steers. You
|
|
139
|
+
must set `live` deliberately.
|
|
140
|
+
- Only switch to `live` with a **competent helmsman at the helm and the pilot's
|
|
141
|
+
own control head to hand** to drop to standby. Do **first commissioning and any
|
|
142
|
+
untested mode** at the dock with the boat secured.
|
|
143
|
+
- Do **not** run this on a bus that already has a real Simrad/B&G AC — two devices
|
|
144
|
+
broadcasting AP state will confuse control heads.
|
|
145
|
+
|
|
146
|
+
#### Access & token
|
|
147
|
+
|
|
148
|
+
In `live` mode the bridge has to PUT commands to the Autopilot V2 API, which needs
|
|
149
|
+
a read/write token. You normally **leave the API token field empty** and let the
|
|
150
|
+
plugin obtain one through SignalK's standard device access-request flow:
|
|
151
|
+
|
|
152
|
+
1. On first start in `live`, the plugin submits an access request (it asks for
|
|
153
|
+
`readwrite`) and prints `requesting device access` in its log.
|
|
154
|
+
2. In the SignalK admin UI, open **Security → Access Requests**. A pending entry
|
|
155
|
+
appears, described as *"Navico autopilot bridge (needs readwrite to steer)"*.
|
|
156
|
+
**Approve** it with **read/write** permission.
|
|
157
|
+
3. The granted token is stored in the plugin's data directory (`access.json`) and
|
|
158
|
+
reused across restarts, so you only approve once. The bridge now shows up under
|
|
159
|
+
**Security → Devices** and can be revoked there at any time.
|
|
160
|
+
|
|
161
|
+
If you **deny** the request the bridge stays read-only until you reconfigure and
|
|
162
|
+
restart it. If the request expires, or you later revoke the device, the plugin
|
|
163
|
+
automatically submits a fresh request to approve again.
|
|
164
|
+
|
|
165
|
+
To use a specific token instead, paste it into the **API token** field — it must
|
|
166
|
+
be a valid SignalK JWT; any non-JWT value is ignored and the auto-request is used.
|
|
167
|
+
|
|
168
|
+
### 3. Enable the autopilot on the MFD
|
|
169
|
+
|
|
170
|
+
The MFD only shows its autopilot control view once the autopilot is enabled on
|
|
171
|
+
the MFD — this is a prerequisite. Per the Navico manual, *"a device connected to
|
|
172
|
+
the NMEA 2000 network should automatically be identified by the system. If not,
|
|
173
|
+
enable the feature from the advanced option in the System settings dialog."*
|
|
174
|
+
|
|
175
|
+
> **Not yet verified with the emulator.** On this boat the Autopilot feature was
|
|
176
|
+
> enabled **manually** from System settings when the project started. Whether a
|
|
177
|
+
> running emulator now triggers fully automatic identification, or the feature
|
|
178
|
+
> still has to be enabled by hand, is untested — if the control view does not
|
|
179
|
+
> appear, enable it manually from System settings (advanced option).
|
|
180
|
+
|
|
181
|
+
### 4. First commissioning
|
|
182
|
+
|
|
183
|
+
A Navico MFD needs a control head on the bus to **start** its very first
|
|
184
|
+
commissioning of an AC. The plugin can emulate one (a B&G keypad on a second
|
|
185
|
+
address) so you can open the **"press standby"** gate without any physical Simrad
|
|
186
|
+
hardware. (See your Navico MFD's install manual — e.g. the B&G Vulcan's
|
|
187
|
+
*Software Setup → autopilot commissioning*.)
|
|
188
|
+
|
|
189
|
+
1. Enable **Commissioning mode** in the plugin config and save. SignalK restarts
|
|
190
|
+
the plugin automatically when you save config — no manual restart needed.
|
|
191
|
+
2. On the MFD, run the autopilot commissioning wizard. When it asks you to press
|
|
192
|
+
standby, the emulated head is already putting the standby command on the bus,
|
|
193
|
+
so the gate opens; the dockside config fields populate from the AC readback.
|
|
194
|
+
3. Bail out of the wizard before the rudder test / sea-trial — the control view
|
|
195
|
+
unlocks anyway. Only **boat type** matters (set **Sail** for wind/tack); the
|
|
196
|
+
rudder calibration, drive voltage and rudder-test values are ignored.
|
|
197
|
+
4. **Disable Commissioning mode** and save. It is not needed afterwards — the
|
|
198
|
+
emulated AC plus the MFD's own buttons run normal operation.
|
|
199
|
+
|
|
200
|
+
The emulated head only sends the keypad heartbeat (`65305`) and the standby
|
|
201
|
+
command (`130850` key `0x0006`); it never steers.
|
|
202
|
+
|
|
203
|
+
### 5. Source priorities (required)
|
|
204
|
+
|
|
205
|
+
The emulator is a second "autopilot" on the same NMEA 2000 bus your SignalK server
|
|
206
|
+
already reads. Its firehose includes Simnet `65305`/`65341`, which canboat maps to
|
|
207
|
+
`steering.autopilot.state` and `steering.autopilot.target.headingMagnetic` — the
|
|
208
|
+
same paths your **real** pilot writes. With two sources on one path, SignalK's
|
|
209
|
+
source arbitration can pick the emulator's value (`65305` decodes to `heading`, not
|
|
210
|
+
`auto`), and the V2 provider's `putAdjustHeading` then rejects a course change with
|
|
211
|
+
**`400 "Autopilot not in auto or wind mode"`** because the path it reads is no
|
|
212
|
+
longer `auto`/`wind`.
|
|
213
|
+
|
|
214
|
+
Symptom: state changes (standby/auto/wind) work, but **a course change is refused
|
|
215
|
+
in auto** (and intermittently in wind), even though the pilot really is in auto.
|
|
216
|
+
|
|
217
|
+
Fix: pin the real pilot as the authoritative source for these paths in
|
|
218
|
+
`~/.signalk/settings.json` (`sourcePriorities`), with an **empty timeout** so the
|
|
219
|
+
emulator can never take over:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
"sourcePriorities": {
|
|
223
|
+
"steering.autopilot.state": [
|
|
224
|
+
{ "sourceRef": "<real-pilot-source>", "timeout": "" }
|
|
225
|
+
],
|
|
226
|
+
"steering.autopilot.target.headingMagnetic": [
|
|
227
|
+
{ "sourceRef": "<real-pilot-source>", "timeout": "" }
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Find `<real-pilot-source>` by reading the path and picking the source in `values`
|
|
233
|
+
that reports the correct mode (not the emulator's `heading`):
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
curl -s http://localhost:3000/signalk/v1/api/vessels/self/steering/autopilot/state
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A non-empty timeout (e.g. `10000`) is **not** enough: the real pilot may not
|
|
240
|
+
re-publish state every few seconds at rest, so the emulator's ~2 Hz firehose wins
|
|
241
|
+
again between updates. Use `""`. Restart the server after editing.
|
|
242
|
+
|
|
243
|
+
## Protocol details
|
|
244
|
+
|
|
245
|
+
Reference for the reverse-engineered N2K layer; not needed to set the plugin up.
|
|
246
|
+
|
|
247
|
+
### Command decoding
|
|
248
|
+
|
|
249
|
+
canboatjs 2.10 has an incomplete `130850` definition for this Simnet layout, so
|
|
250
|
+
the bridge decodes on the raw **key byte**, gated by group `0x0a`, not on
|
|
251
|
+
canboat's mislabeled `Event` field. Keys verified live against a Vulcan 7:
|
|
252
|
+
|
|
253
|
+
| key (byte 6) | command | maps to |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| `0x06` | Standby | `PUT state {standby}` |
|
|
256
|
+
| `0x09` | Auto | `PUT state {auto}` |
|
|
257
|
+
| `0x0f` | Wind | `PUT state {wind}` |
|
|
258
|
+
| `0x0a` | Nav / Track | `PUT state {route}` |
|
|
259
|
+
| `0x1a` | ChangeCourse / Tack | `PUT target/adjust`, or `POST tack/*` for ~90° |
|
|
260
|
+
| `0x0c` | No Drift | decoded, not fired (no V2 state) |
|
|
261
|
+
| `0x1c` | key-press envelope | ignored (precedes every command) |
|
|
262
|
+
|
|
263
|
+
**ChangeCourse** (`0x1a`): byte 8 = direction (`0x03` starboard/+, `0x02`
|
|
264
|
+
port/−), bytes 9–10 = magnitude LE16 at `0.0001 rad/bit` (10° = 1745, 1° = 174).
|
|
265
|
+
canboat's `Angle` reads bytes 8–9 (off-by-one) and is wrong for this command, so
|
|
266
|
+
the bridge decodes from the reassembled raw frame. The V2 `adjustTarget` floors
|
|
267
|
+
`radiansToDegrees`, and some providers accept only exactly ±10/±1, so the magnitude
|
|
268
|
+
is rounded to a whole degree `N` and sent as `(N+0.5)°` in radians, landing the
|
|
269
|
+
floor exactly on `N`.
|
|
270
|
+
|
|
271
|
+
**Tack** rides the same `0x1a` channel at the MFD's configured tack angle
|
|
272
|
+
(B&G Vulcan UI default 100°; htool saw 90°), not a separate key. A single
|
|
273
|
+
ChangeCourse well above the buttons' ±10 is treated as a tack — but **only in
|
|
274
|
+
wind mode**, where a tack crosses the wind (a gybe crosses dead downwind);
|
|
275
|
+
outside wind mode a big ChangeCourse is ignored. The magnitude is discarded: the
|
|
276
|
+
pilot mirrors the apparent wind angle itself. The tack **direction is derived
|
|
277
|
+
from SK's `environment.wind.angleApparent`** (positive to starboard → tack to
|
|
278
|
+
starboard), not the unverified MFD dir byte, and mapped to
|
|
279
|
+
`POST tack/port|tack/starboard`
|
|
280
|
+
(channel per [htool](https://github.com/htool/RaymarineAPtoFakeNavicoAutoPilot)).
|
|
281
|
+
Test candidate — depends on the backing provider supporting tack, and the
|
|
282
|
+
turn-direction convention still needs on-board verification.
|
|
283
|
+
|
|
284
|
+
### Mode display (firehose)
|
|
285
|
+
|
|
286
|
+
The MFD's displayed mode is driven by the firehose, not by the button press. Per
|
|
287
|
+
htool, `65305` `00,1d,..` sets the displayed mode and `00,0a,..` the state; the
|
|
288
|
+
plugin sends distinct per-mode `65340`/`65302`/`65305` frames (auto `10,01`, wind
|
|
289
|
+
`10,03`, nav `10,06`) plus a mode-change announce. **Test candidates** — htool had
|
|
290
|
+
not fully verified the wind/route overlay and some frames are his guesses.
|
|
291
|
+
|
|
292
|
+
### Set heading (127237)
|
|
293
|
+
|
|
294
|
+
When engaged the emulator broadcasts `127237` Heading/Track Control with the
|
|
295
|
+
locked heading in the *Heading-To-Steer* field (Steering Mode = Heading Control,
|
|
296
|
+
Heading Reference = Magnetic), so the MFD shows the set heading. A real AC
|
|
297
|
+
re-broadcasts this; without it the MFD shows "- - -". It is sent at **5 Hz**
|
|
298
|
+
because the boat's other devices broadcast `127237` with an *empty*
|
|
299
|
+
Heading-To-Steer at 10–20 Hz, which otherwise blanks the value and makes the
|
|
300
|
+
display flicker. The value is `steering.autopilot.target.headingMagnetic` (the
|
|
301
|
+
locked heading), falling back to `navigation.headingMagnetic`. It is sent as
|
|
302
|
+
Magnetic; the MFD converts to its configured heading reference (e.g. True) using
|
|
303
|
+
the bus magnetic variation, so set the MFD's heading units to match the rest of
|
|
304
|
+
the boat.
|
|
305
|
+
|
|
306
|
+
## Verified behaviour (Vulcan 7 → EV-200)
|
|
307
|
+
|
|
308
|
+
- MFD binds to the emulator and raises a *lost-autopilot* alarm the instant the
|
|
309
|
+
firehose stops.
|
|
310
|
+
- "Press standby" commissioning gate opens; dockside config fields populate.
|
|
311
|
+
- Control view reachable and live **without finishing the wizard**.
|
|
312
|
+
- Standby / Auto / Wind / Nav(Track) / ±course button presses decode and, in
|
|
313
|
+
`live`, drive the EV-200 (clutch engages, rudder moves, P70 and the Vulcan
|
|
314
|
+
overlay reflect the mode). The overlay label followed the pilot in every mode
|
|
315
|
+
during the trial.
|
|
316
|
+
- **Set heading** displays on the MFD (both the overlay and the AP view) via the
|
|
317
|
+
populated `127237`; ±course on the Vulcan changes it and is confirmed on the
|
|
318
|
+
MFD without touching the pilot's own head.
|
|
319
|
+
|
|
320
|
+
### Sea trial (on the water, light wind, calm sea, single trial)
|
|
321
|
+
|
|
322
|
+
- **Auto holds course** with way on; the abort path is sound: **P70 standby frees
|
|
323
|
+
the helm immediately**, and standby from the Vulcan drops the pilot.
|
|
324
|
+
- **±1° / ±10°** nudges alter heading by the right amount and direction; a
|
|
325
|
+
cumulative ~60° alteration came round without an accidental tack.
|
|
326
|
+
- **Nav** steered along a route leg toward the waypoint and corrected cross-track.
|
|
327
|
+
- **Wind** engaged and **held the apparent wind angle** — but see the display /
|
|
328
|
+
Tack limitation below.
|
|
329
|
+
|
|
330
|
+
## Known limitations
|
|
331
|
+
|
|
332
|
+
This is an alpha; these are open:
|
|
333
|
+
|
|
334
|
+
- **Route/Track display crashes the MFD's AP view.** Opening the dedicated
|
|
335
|
+
autopilot view from scratch while in Nav crashes a Vulcan 7 (it recovers and
|
|
336
|
+
rebinds). The cause is confirmed to be the unverified route display frames
|
|
337
|
+
(`65302`/`65305`, htool guesses): substituting the proven auto frames stops the
|
|
338
|
+
crash, but showing "Auto" while tracking is poor UX so the route frames are kept.
|
|
339
|
+
Steering in Nav works, and *switching* to Nav while the AP view is already open
|
|
340
|
+
works — only opening the view from scratch in Nav crashes it. A correct fix needs
|
|
341
|
+
a capture of a real Simrad AC in route mode. Until then, avoid opening the AP view
|
|
342
|
+
while in Nav.
|
|
343
|
+
- **Wind-mode display and Tack do not work, although the pilot does hold wind.**
|
|
344
|
+
On the sea trial the EV-200 **engaged Wind and held the apparent wind angle**,
|
|
345
|
+
but the MFD shows no commanded wind angle and no true wind angle (TWA),
|
|
346
|
+
±wind-angle adjust from the Vulcan has no effect, and the **Tack button is
|
|
347
|
+
greyed out** — because the emulator's `65341` always carries heading, not a wind
|
|
348
|
+
reference. Tack is decoded (~90° ChangeCourse → V2 tack endpoint) but cannot be
|
|
349
|
+
triggered from the MFD until the wind angle is reported. Needs a capture of a real
|
|
350
|
+
Simrad AC in wind mode to get the right frame/field.
|
|
351
|
+
- **Some mode-display frames are still guesses.** The per-mode firehose frames
|
|
352
|
+
(from htool) made the Vulcan overlay follow standby/auto/wind/route correctly
|
|
353
|
+
throughout the sea trial, but some are unverified. If one is wrong the pilot is
|
|
354
|
+
still in the correct mode (confirm on its own control head); only the Navico
|
|
355
|
+
MFD's mode label would be off.
|
|
356
|
+
- **No Drift (`0x0c`) has no V2 equivalent** — the V2 states are only
|
|
357
|
+
standby/auto/wind/route. Logged, never fired.
|
|
358
|
+
- **Only one sea trial, in light conditions.** Auto course-hold, ±course, the
|
|
359
|
+
abort path and route-leg tracking are proven on the water — but in light wind
|
|
360
|
+
(~10 kn), calm sea, at ~4 kn with a single crew. Holding quality in stronger
|
|
361
|
+
wind and sea, and waypoint advance along a multi-leg route, are not yet proven.
|
|
362
|
+
(Auto behaviour may also differ on other pilots.)
|
|
363
|
+
- **Output is via loopback HTTP** with a configured token. An in-process V2 call
|
|
364
|
+
would remove the token requirement but there is no clean documented path for a
|
|
365
|
+
non-provider plugin to set V2 state; this is a candidate for a later version.
|
|
366
|
+
|
|
367
|
+
## Scope
|
|
368
|
+
|
|
369
|
+
This emulates the **AC (the commanded device)** to capture an MFD's autopilot
|
|
370
|
+
control and re-target it. It does **not** implement a real Simrad pilot's steering
|
|
371
|
+
or active-controller takeover logic — it accepts and translates commands. The
|
|
372
|
+
commissioning values an MFD writes are never consumed by anything that steers;
|
|
373
|
+
the value here is the discovery/command protocol, not the commissioning data.
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT. The software is provided "as is", without warranty of any kind and without
|
|
378
|
+
liability on the part of the authors — see the
|
|
379
|
+
[Disclaimer](#disclaimer--no-warranty).
|
package/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const ACEmulator = require('./lib/ac-emulator')
|
|
6
|
+
const ControlHead = require('./lib/control-head')
|
|
7
|
+
const AccessRequest = require('./lib/access-request')
|
|
8
|
+
|
|
9
|
+
module.exports = function (app) {
|
|
10
|
+
let emulator = null
|
|
11
|
+
let head = null
|
|
12
|
+
let accessReq = null
|
|
13
|
+
|
|
14
|
+
// Persist the granted device token (and its clientId) outside the plugin
|
|
15
|
+
// config so an admin approval survives restarts without rewriting config.
|
|
16
|
+
function tokenFile () {
|
|
17
|
+
try { return path.join(app.getDataDirPath(), 'access.json') } catch (e) { return null }
|
|
18
|
+
}
|
|
19
|
+
function readSaved () {
|
|
20
|
+
const f = tokenFile()
|
|
21
|
+
if (!f) { return {} }
|
|
22
|
+
try { return JSON.parse(fs.readFileSync(f, 'utf8')) } catch (e) { return {} }
|
|
23
|
+
}
|
|
24
|
+
function writeSaved (obj) {
|
|
25
|
+
const f = tokenFile()
|
|
26
|
+
if (!f) { return }
|
|
27
|
+
try { fs.writeFileSync(f, JSON.stringify(obj, null, 2)) } catch (e) { app.debug('could not save token: ' + (e && e.message)) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const plugin = {
|
|
31
|
+
id: 'signalk-navico-autopilot-bridge',
|
|
32
|
+
name: 'Navico autopilot bridge (Simrad AC emulator)',
|
|
33
|
+
description:
|
|
34
|
+
'Emulates a Simrad AC12/AC42 autopilot computer so a Navico MFD (B&G ' +
|
|
35
|
+
'Vulcan/Zeus, Simrad, Lowrance) binds to it and sends its autopilot ' +
|
|
36
|
+
'control-view button presses as Simnet 130850 commands. Those are decoded ' +
|
|
37
|
+
'and translated into the SignalK Autopilot V2 API, driving whichever pilot ' +
|
|
38
|
+
'backs it (e.g. a Raymarine EV-200 via signalk-autopilot). ' +
|
|
39
|
+
'ALPHA: see README for verified behaviour and known limitations.'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
plugin.schema = {
|
|
43
|
+
type: 'object',
|
|
44
|
+
required: ['canInterface', 'acModel', 'preferredAddress', 'bridge'],
|
|
45
|
+
properties: {
|
|
46
|
+
canInterface: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
title: 'CAN interface',
|
|
49
|
+
description: 'SocketCAN interface the NMEA 2000 bus is on.',
|
|
50
|
+
default: 'can0'
|
|
51
|
+
},
|
|
52
|
+
acModel: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
title: 'Emulated AC model',
|
|
55
|
+
description: 'Identity broadcast to the MFD. Both AC42 and AC12 bind a ' +
|
|
56
|
+
'Vulcan 7; the model only sets the reported product info, not whether ' +
|
|
57
|
+
'it binds. AC42 matches the reference capture.',
|
|
58
|
+
enum: ['AC42', 'AC12'],
|
|
59
|
+
default: 'AC42'
|
|
60
|
+
},
|
|
61
|
+
preferredAddress: {
|
|
62
|
+
type: 'number',
|
|
63
|
+
title: 'Preferred N2K source address',
|
|
64
|
+
description: 'Address the emulated AC claims on the bus.',
|
|
65
|
+
default: 35
|
|
66
|
+
},
|
|
67
|
+
enableFirehose: {
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
title: 'Broadcast AC autopilot state (required)',
|
|
70
|
+
description: 'Send the full Simrad AP state/telemetry broadcast a real ' +
|
|
71
|
+
'AC emits. Required for the MFD to bind and for the control view to ' +
|
|
72
|
+
'unlock. Leave on.',
|
|
73
|
+
default: true
|
|
74
|
+
},
|
|
75
|
+
enableStdPgns: {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
title: 'Also send standard nav PGNs (advanced, A/B only)',
|
|
78
|
+
description: 'Emit 127245/127237/127250 as the real AC also does. These ' +
|
|
79
|
+
'DUPLICATE other bus sources (rudder/heading/track) and can cause ' +
|
|
80
|
+
'conflicting data — only enable for protocol A/B testing.',
|
|
81
|
+
default: false
|
|
82
|
+
},
|
|
83
|
+
bridge: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
title: 'Bridge mode',
|
|
86
|
+
description: 'off = ignore incoming commands; dry-run = decode and log ' +
|
|
87
|
+
'only (no steering); live = translate commands to the autopilot. ' +
|
|
88
|
+
'Default dry-run for safety — set live deliberately.',
|
|
89
|
+
enum: ['off', 'dry-run', 'live'],
|
|
90
|
+
default: 'dry-run'
|
|
91
|
+
},
|
|
92
|
+
autopilotId: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
title: 'Target autopilot id (V2 API)',
|
|
95
|
+
description: 'Which autopilots/<id> the V2 API drives. Usually _default.',
|
|
96
|
+
default: '_default'
|
|
97
|
+
},
|
|
98
|
+
skHost: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
title: 'SignalK host',
|
|
101
|
+
description: 'Host for the loopback V2 API calls.',
|
|
102
|
+
default: '127.0.0.1'
|
|
103
|
+
},
|
|
104
|
+
skPort: {
|
|
105
|
+
type: 'number',
|
|
106
|
+
title: 'SignalK port',
|
|
107
|
+
description: 'Port for the loopback V2 API calls.',
|
|
108
|
+
default: 3000
|
|
109
|
+
},
|
|
110
|
+
token: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
title: 'API token (optional manual override)',
|
|
113
|
+
description: 'Normally leave EMPTY. In live mode the plugin requests a ' +
|
|
114
|
+
'readwrite token automatically via an access request you approve under ' +
|
|
115
|
+
'Security → Access Requests, and stores it. Set this only to force a ' +
|
|
116
|
+
'specific token (must be a valid JWT; a non-JWT value is ignored).'
|
|
117
|
+
},
|
|
118
|
+
enableCommissioningHead: {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
title: 'Commissioning mode (emulate a control head)',
|
|
121
|
+
description: 'Emulate a B&G keypad on a second address to open the MFD ' +
|
|
122
|
+
'"press standby" gate for FIRST commissioning. Enable only while ' +
|
|
123
|
+
'commissioning, then turn it off — not needed for normal operation.',
|
|
124
|
+
default: false
|
|
125
|
+
},
|
|
126
|
+
headAddress: {
|
|
127
|
+
type: 'number',
|
|
128
|
+
title: 'Commissioning head N2K address',
|
|
129
|
+
description: 'Source address the emulated control head claims (only used ' +
|
|
130
|
+
'when commissioning mode is on).',
|
|
131
|
+
default: 44
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A SignalK token is a JWT (three dot-separated parts). Ignore anything else
|
|
137
|
+
// pasted into the config field so a stray value can't shadow a valid token.
|
|
138
|
+
function validJwt (t) { return typeof t === 'string' && t.split('.').length === 3 }
|
|
139
|
+
|
|
140
|
+
plugin.start = function (options) {
|
|
141
|
+
const o = options || {}
|
|
142
|
+
try {
|
|
143
|
+
// Token precedence: a VALID config token > previously granted token.
|
|
144
|
+
const saved = readSaved()
|
|
145
|
+
let configToken = null
|
|
146
|
+
if (o.token) {
|
|
147
|
+
if (validJwt(o.token)) { configToken = o.token } else { app.error('Configured token is not a JWT — ignoring it (using the granted device token if present)') }
|
|
148
|
+
}
|
|
149
|
+
const token = configToken || saved.token || null
|
|
150
|
+
emulator = new ACEmulator(app, Object.assign({}, o, { token }))
|
|
151
|
+
emulator.start()
|
|
152
|
+
|
|
153
|
+
// No token and we intend to steer -> request device access; an admin
|
|
154
|
+
// approves it under Security -> Access Requests, then we store + use it.
|
|
155
|
+
if (!token && o.bridge === 'live') {
|
|
156
|
+
accessReq = new AccessRequest({
|
|
157
|
+
host: o.skHost || '127.0.0.1',
|
|
158
|
+
port: o.skPort || 3000,
|
|
159
|
+
description: 'Navico autopilot bridge (needs readwrite to steer)',
|
|
160
|
+
clientId: saved.clientId,
|
|
161
|
+
debug: app.debug
|
|
162
|
+
})
|
|
163
|
+
writeSaved({ clientId: accessReq.clientId, token: saved.token || null })
|
|
164
|
+
app.debug('no token configured -- requesting device access (approve under Security -> Access Requests)')
|
|
165
|
+
accessReq.start((newToken) => {
|
|
166
|
+
if (emulator) { emulator.setToken(newToken) }
|
|
167
|
+
writeSaved({ clientId: accessReq.clientId, token: newToken })
|
|
168
|
+
app.setPluginStatus('Access approved -- token stored, bridge can steer')
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (o.enableCommissioningHead) {
|
|
173
|
+
head = new ControlHead(app, {
|
|
174
|
+
canInterface: o.canInterface || 'can0',
|
|
175
|
+
headAddress: (typeof o.headAddress === 'number') ? o.headAddress : 44,
|
|
176
|
+
acAddress: (typeof o.preferredAddress === 'number') ? o.preferredAddress : 35
|
|
177
|
+
})
|
|
178
|
+
head.start()
|
|
179
|
+
}
|
|
180
|
+
app.setPluginStatus('Starting Simrad ' + (o.acModel || 'AC42') +
|
|
181
|
+
' emulator on ' + (o.canInterface || 'can0') +
|
|
182
|
+
(o.enableCommissioningHead ? ' + commissioning head' : '') + '…')
|
|
183
|
+
} catch (e) {
|
|
184
|
+
app.setPluginError('Failed to start: ' + (e && e.message))
|
|
185
|
+
app.error(e)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
plugin.stop = function () {
|
|
190
|
+
if (accessReq) {
|
|
191
|
+
accessReq.stop()
|
|
192
|
+
accessReq = null
|
|
193
|
+
}
|
|
194
|
+
if (head) {
|
|
195
|
+
head.stop()
|
|
196
|
+
head = null
|
|
197
|
+
}
|
|
198
|
+
if (emulator) {
|
|
199
|
+
emulator.stop()
|
|
200
|
+
emulator = null
|
|
201
|
+
}
|
|
202
|
+
app.setPluginStatus('Stopped')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return plugin
|
|
206
|
+
}
|