signalk-pypilot-newui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/NOTICE +30 -0
- package/README.md +171 -0
- package/dist/autopilot-provider.js +220 -0
- package/dist/index.js +1551 -0
- package/dist/publisher.js +265 -0
- package/dist/pypilot-client.js +218 -0
- package/dist/scanner.js +125 -0
- package/package.json +68 -0
- package/public/Pypilot_New_UI.svg +25 -0
- package/public/app.js +2831 -0
- package/public/boat-cenital.svg +865 -0
- package/public/icon.svg +6 -0
- package/public/index.html +823 -0
- package/public/screenshots/01-control-rose.jpg +0 -0
- package/public/screenshots/02-tune-gains.jpg +0 -0
- package/public/screenshots/03-info.jpg +0 -0
- package/public/screenshots/04-setup.jpg +0 -0
- package/public/style.css +1487 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 — 2026-08-18 — Rev32..Rev62 first public release
|
|
4
|
+
|
|
5
|
+
The webapp went from a minimum viable control panel to a full-blown touch
|
|
6
|
+
visor + first-class Signal K bridge in this batch. Highlights:
|
|
7
|
+
|
|
8
|
+
### UI - Control tab
|
|
9
|
+
|
|
10
|
+
- **Compass rose** (Rev47..Rev58): cardinals + 30° tick labels, boat-fixed
|
|
11
|
+
green/red sailing wedges reaching under the hull, AWA (amber) + TWA
|
|
12
|
+
(magenta dashed) arrows, cyan target diamond outside the ring, HDG at
|
|
13
|
+
the top rim, wind speed at the bottom.
|
|
14
|
+
- **4 configurable corner tiles** (Rev48): long-press to pick datum (HDG,
|
|
15
|
+
AWA/TWA/TWS/TWD, SOG, COG, TGT, depth, mode, polar-performance keys,
|
|
16
|
+
custom SK path).
|
|
17
|
+
- **Wind arrow independent toggles** (Rev58): AWA and TWA on/off separately,
|
|
18
|
+
persisted in localStorage.
|
|
19
|
+
- **Custom SK path** (Rev58): add any published Signal K path as a corner
|
|
20
|
+
tile with label + unit. Datalist auto-suggests paths seen on the bus.
|
|
21
|
+
- **Polar-performance opt-in** (Rev58): checkboxes for the 10 keys of
|
|
22
|
+
`signalk-polar-performance-plugin` + water temperature.
|
|
23
|
+
- **Optimistic AP engage** (Rev56): visual state flips instantly on click,
|
|
24
|
+
auto-retries on Tailscale flakes, rolls back with red-border flash on
|
|
25
|
+
hard failure. Guard blocks double-taps from firing opposite POSTs.
|
|
26
|
+
- **KIP-friendly Simple Switches** (Rev33..Rev38): `electrical.switches.pypilot.*`
|
|
27
|
+
for ap.state (momentary boolean), nudge (4 momentary), tack (3 momentary),
|
|
28
|
+
mode (radio), dynamic profile (radio).
|
|
29
|
+
|
|
30
|
+
### UI - Tune tab
|
|
31
|
+
|
|
32
|
+
- **Locked-by-default gain sliders** (Rev42): unlock checkbox, auto-relocks
|
|
33
|
+
on tab exit.
|
|
34
|
+
- **±10 / ±1 fine-tune buttons** (Rev43) flanking each slider.
|
|
35
|
+
- **Orange "was" reference** shown when unlocked so the operator sees what
|
|
36
|
+
they are moving from.
|
|
37
|
+
- All `RangeSetting` config sliders from the pypilot catalog appear here
|
|
38
|
+
automatically (Rev45).
|
|
39
|
+
|
|
40
|
+
### UI - Race tab (new, Rev35)
|
|
41
|
+
|
|
42
|
+
- T1/T2 countdown timer with audible beeps, screen flash, spoken seconds
|
|
43
|
+
("10, 5, 4, 3, 2, 1, Start"). Runs in the background across tabs.
|
|
44
|
+
- SYNC rounds to the nearest minute mid-countdown; RESET returns to idle.
|
|
45
|
+
|
|
46
|
+
### UI - Setup tab
|
|
47
|
+
|
|
48
|
+
- **Remote `RESTART pypilot`** (Rev55): SSH into the TinyPilot using the
|
|
49
|
+
user + password stored in plugin config (via `ssh2` npm), runs
|
|
50
|
+
`sudo systemctl restart pypilot pypilot_web`, reconnects local socket.
|
|
51
|
+
Falls back to reconnect-only if SSH fails or password not set.
|
|
52
|
+
- **`_pluginConfigMerge` helper** (Rev55): GETs the full plugin config from
|
|
53
|
+
`/skServer/plugins/<id>/config`, overlays only the patched fields, POSTs
|
|
54
|
+
back - so saving Nudge or Absorb no longer wipes the SSH credentials
|
|
55
|
+
(or vice versa) via full-replace.
|
|
56
|
+
- Pause/Resume buttons removed from the UI (backend endpoints kept).
|
|
57
|
+
- Absorb-provider hint reworded, security note added for the plain-text
|
|
58
|
+
plugin config storage.
|
|
59
|
+
|
|
60
|
+
### UI - Paths & API tab
|
|
61
|
+
|
|
62
|
+
- KIP mini-table trimmed to the current recommended surface (Simple Switch
|
|
63
|
+
+ Autopilot API v2 Put). Legacy `.actions.engage/nudge/tack/*Int` paths
|
|
64
|
+
hidden from the UI (backend handlers still registered for backwards
|
|
65
|
+
compatibility).
|
|
66
|
+
- Horizontal-scroll wrapper on both tables so long SK paths remain readable
|
|
67
|
+
on portrait phones.
|
|
68
|
+
|
|
69
|
+
### UI - Info tab (new, Rev58..Rev59)
|
|
70
|
+
|
|
71
|
+
- Safety notice, quick-start (10 steps), About, Acknowledgements
|
|
72
|
+
(Sean D'Epagnier, Jean-Marc at Navitop, Signal K community, Panaaj),
|
|
73
|
+
Apache 2.0 license attribution, GitHub issue links, coexistence rules
|
|
74
|
+
with `pypilot-autopilot-provider`.
|
|
75
|
+
- **Config export/import** (Rev59): Web Share API to email / WhatsApp /
|
|
76
|
+
Signal / AirDrop, `.json` download, clipboard copy, file / text import,
|
|
77
|
+
reset. Payload includes corners, wind-arrow flags, polar toggles, custom
|
|
78
|
+
paths. Never includes SSH credentials.
|
|
79
|
+
- Version + Rev header, link to the classic pypilot_web UI on the TinyPilot.
|
|
80
|
+
|
|
81
|
+
### i18n (Rev61)
|
|
82
|
+
|
|
83
|
+
- Full **English / Español / Deutsch / Français** with browser auto-detect
|
|
84
|
+
on first visit, override in Setup → Language.
|
|
85
|
+
- ~90 keys covering tabs, Setup, popup, Paths, Regatta, alerts, prompts,
|
|
86
|
+
confirms. EN + ES complete; DE + FR cover tabs + core Setup + calibration
|
|
87
|
+
and fall back to EN for the rest.
|
|
88
|
+
- Info tab uses `data-lang` sibling blocks (EN + ES fully written).
|
|
89
|
+
- `<html lang="…">` reflects the active language.
|
|
90
|
+
|
|
91
|
+
### Layout / responsiveness (Rev49..Rev57)
|
|
92
|
+
|
|
93
|
+
- Landscape: rose stretches to fill the left column; control column becomes
|
|
94
|
+
a `1fr 1fr auto` grid so tack + AP rows fill the height instead of
|
|
95
|
+
clustering at the top. Text auto-shrinks via `clamp(min, min(vw, vh), max)`.
|
|
96
|
+
- Portrait: nav bar and swipe hint always visible; corner tiles inside the
|
|
97
|
+
rose square; long paths get horizontal scroll wrappers.
|
|
98
|
+
- **Swipe navigation** (Rev43..Rev57) between tabs: 80px threshold, 2.2×
|
|
99
|
+
H/V ratio, first-move axis lock so scrolling never triggers a stray
|
|
100
|
+
tab flip. Excluded over unlocked sliders and `.paths-scroll` wrappers.
|
|
101
|
+
- Nav-bar auto-hide toggle via yellow triangle in the swipe-hint strip.
|
|
102
|
+
|
|
103
|
+
### Backend (src/index.ts)
|
|
104
|
+
|
|
105
|
+
- `PLUGIN_REVISION` bumped Rev32 → Rev61 (11 packaged Revs since 0.1.0).
|
|
106
|
+
- New `sshUser` + `sshPassword` plugin config fields (Rev55).
|
|
107
|
+
- New `POST /plugins/*/restart-pypilot` router using `ssh2` (Rev55).
|
|
108
|
+
- `/status` exposes `sshUser`, `sshPasswordSet` (never the password itself),
|
|
109
|
+
and the package `version` from `package.json` (Rev59).
|
|
110
|
+
- Wildcard subscription to `performance.*` for polar-performance keys +
|
|
111
|
+
`environment.water.temperature` (Rev58).
|
|
112
|
+
- Optimistic UI + retry helper in the frontend (Rev56); no backend change.
|
|
113
|
+
|
|
114
|
+
### Screenshots + logo (Rev61)
|
|
115
|
+
|
|
116
|
+
- App Store screenshots in `public/screenshots/` (JPEG < 500 KB each,
|
|
117
|
+
4 shots: Control / Tune / Info / Setup).
|
|
118
|
+
- Vector logo `public/Pypilot_New_UI.svg` (33 KB) wired as
|
|
119
|
+
`signalk.appIcon`.
|
|
120
|
+
- `signalk.screenshots[]` populated for the App Store card.
|
|
121
|
+
|
|
122
|
+
### Docs
|
|
123
|
+
|
|
124
|
+
- README rewritten: full feature list, coexistence table, remote restart,
|
|
125
|
+
updated endpoint table, acknowledgements, safety, reporting.
|
|
126
|
+
- App Store `description` in `package.json` replaced with a
|
|
127
|
+
user-facing one-paragraph blurb.
|
|
128
|
+
|
|
129
|
+
## 0.1.0 — 2026-07-21
|
|
130
|
+
|
|
131
|
+
### Rev1..Rev6 first-boot iteration on the Tunatunes Pi
|
|
132
|
+
|
|
133
|
+
- Rev1 scaffold.
|
|
134
|
+
- Rev2 fix: factory function forgot to `return plugin` -> SK crashed with
|
|
135
|
+
`Cannot read properties of undefined (reading 'id')`.
|
|
136
|
+
- Rev3 fix: `publishMeta` sent `updates:[{meta:[...]}]` without `values`,
|
|
137
|
+
triggering `update.values is not iterable` in the SK delta iterator.
|
|
138
|
+
- Rev4 refactor: attach `meta` INLINE on the first `publishValue` of each
|
|
139
|
+
path so no delta ever carries an empty `values` array. Dropped the
|
|
140
|
+
separate `publishMeta` call.
|
|
141
|
+
- Rev5 diagnostic: catalog counts in `setPluginStatus`.
|
|
142
|
+
- Rev6 fix: pypilot exposes gains as `ap.pilot.<pilot>.<gain>` (SINGULAR),
|
|
143
|
+
not `ap.pilots.*` as the initial brief assumed. Confirmed by inspecting
|
|
144
|
+
`pypilot_values` on the socket.io wire. Corrected in publisher.ts,
|
|
145
|
+
index.ts (setupWatches + status), and public/app.js (renderGains).
|
|
146
|
+
|
|
147
|
+
### Known limitation
|
|
148
|
+
|
|
149
|
+
`signalk-pushover-plugin@0.0.6` emits two `Unhandled rejection: TypeError:
|
|
150
|
+
update.values is not iterable` at every SK restart, even before our plugin
|
|
151
|
+
publishes anything. The stack is pushover's own (`index.js:193:71`) and
|
|
152
|
+
does not reference our plugin. Ignore, or file upstream against
|
|
153
|
+
pushover-plugin.
|
|
154
|
+
|
|
155
|
+
### Backend
|
|
156
|
+
- src/pypilot-client.ts: socket.io client to `pypilot_web`, reconnect,
|
|
157
|
+
`pypilot_values` catalog discovery, watch/set primitives, ping/pong.
|
|
158
|
+
- src/publisher.ts: fixed + dynamic mapping table pypilot names to
|
|
159
|
+
`steering.autopilot.pypilot.*` (gains, servo, calibration, tack,
|
|
160
|
+
errors, warnings, runtime, version). Deg->rad and C->K conversions.
|
|
161
|
+
- src/scanner.ts: /24 LAN scan, TCP probe on ports 80 and 8000, HTTP
|
|
162
|
+
fingerprint on `<title>pypilot control</title>`.
|
|
163
|
+
- src/index.ts: plugin lifecycle + JSON schema + PUT handlers (with
|
|
164
|
+
reverse rad->deg on angle paths) + HTTP router (`/status`, `/paths`,
|
|
165
|
+
`/catalog`, `/scan`, `/raw`).
|
|
166
|
+
|
|
167
|
+
### Reserved for pypilot-autopilot-provider (never republished)
|
|
168
|
+
`ap.enabled`, `ap.heading`, `ap.heading_command`, `ap.mode`. Both plugins
|
|
169
|
+
coexist.
|
|
170
|
+
|
|
171
|
+
- Backend: socket.io client to `pypilot_web`, catalog discovery via
|
|
172
|
+
`pypilot_values`, watch subscriptions, delta publisher with
|
|
173
|
+
configurable per-path enable/disable.
|
|
174
|
+
- Paths: full `steering.autopilot.pypilot.*` tree (gains, servo,
|
|
175
|
+
calibration, tack, errors, warnings, runtime, version).
|
|
176
|
+
- HTTP: `/scan` LAN discovery, `/status`, `/paths` KIP-friendly catalog,
|
|
177
|
+
`/raw` protected write.
|
|
178
|
+
- Webapp: mobile-first dark, four tabs (Control, Ajustes, Paths & API,
|
|
179
|
+
Setup). Control tab fully wired against the Autopilot API v1 of
|
|
180
|
+
Signal K (engage / disengage / mode / target / tack / heading nudges);
|
|
181
|
+
Ajustes tab with translucent-while-adjusting sliders for gains and
|
|
182
|
+
calibration; Paths & API tab with live path list + copy-for-KIP;
|
|
183
|
+
Setup tab with LAN scan.
|
|
184
|
+
- Coexists with the official `pypilot-autopilot-provider` — this plugin
|
|
185
|
+
never publishes `steering.autopilot.state / mode / target / engaged`
|
|
186
|
+
(those stay with the provider).
|
package/NOTICE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
signalk-pypilot-newui
|
|
2
|
+
Copyright 2026 Aitonos
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this project except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
This project includes an "Autopilot Provider" adapter
|
|
12
|
+
(src/autopilot-provider.ts) whose method structure and command
|
|
13
|
+
translation semantics are derived from:
|
|
14
|
+
|
|
15
|
+
pypilot-autopilot-provider
|
|
16
|
+
Copyright (c) Panaaj
|
|
17
|
+
https://github.com/panaaj/pypilot-autopilot-provider
|
|
18
|
+
Apache License 2.0
|
|
19
|
+
|
|
20
|
+
The upstream project ships its own socket.io client. In our fork we
|
|
21
|
+
re-implemented the same command surface on top of the PypilotClient we
|
|
22
|
+
already carry (src/pypilot-client.ts) so that a Signal K server with
|
|
23
|
+
both plugins effectively active only ever opens ONE socket to the
|
|
24
|
+
pypilot_web process on the TinyPilot. This is a functional benefit for
|
|
25
|
+
Pi Zero W hardware; no derivative works of the upstream code itself are
|
|
26
|
+
distributed.
|
|
27
|
+
|
|
28
|
+
For the socket.io protocol layer talking to pypilot_web (watch=, name=value)
|
|
29
|
+
we independently confirmed it against the upstream pypilot sources
|
|
30
|
+
(https://github.com/pypilot/pypilot) which are LGPL-3.0.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# signalk-pypilot-newui
|
|
2
|
+
|
|
3
|
+
**PyPilot New-UI + SK Paths** — a modern, touch-first control panel for the
|
|
4
|
+
open-source [pypilot](https://github.com/pypilot/pypilot) autopilot, plus every
|
|
5
|
+
pypilot value exposed as a first-class Signal K path so tools like
|
|
6
|
+
[KIP](https://kip.signalk.org), WilhelmSK and freeboard can build custom gauges
|
|
7
|
+
and switches out of the box.
|
|
8
|
+
|
|
9
|
+
Runs alongside [`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)
|
|
10
|
+
by Panaaj — see [Working alongside pypilot-autopilot-provider](#working-alongside-pypilot-autopilot-provider).
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
**Touch webapp** (mobile / tablet / chart-plotter, dark theme):
|
|
15
|
+
|
|
16
|
+
- Compass rose with heading readout, cardinals + degree ticks, hull-fixed
|
|
17
|
+
green/red sailing wedges, AWA + TWA arrows (independent, toggleable), cyan
|
|
18
|
+
target diamond, and 4 configurable corner tiles (long-press to reconfigure).
|
|
19
|
+
- Big-touch AP engage / disengage with optimistic UI + auto-retry over
|
|
20
|
+
flaky links (Tailscale-safe).
|
|
21
|
+
- Tack port / starboard, mode selector (compass / GPS / wind / true wind / nav),
|
|
22
|
+
nudge ±1 / ±10 with configurable step size, current heading snap on engage.
|
|
23
|
+
- **Tune** tab with locked-by-default gain sliders (auto-relock on tab exit),
|
|
24
|
+
±10 / ±1 buttons flanking each slider, orange "previous value" reference.
|
|
25
|
+
- **Race** timer T1/T2 with beeps, screen flash, spoken seconds countdown
|
|
26
|
+
("10, 5, 4, 3, 2, 1, Start"). Runs in the background across tabs.
|
|
27
|
+
- **Setup** with LAN scan, manual host+port, KIP absorb-provider toggle,
|
|
28
|
+
SSH-based remote `RESTART pypilot` button (credentials stored in plugin
|
|
29
|
+
config), nudge step size, language, IMU / rudder calibration.
|
|
30
|
+
- **Paths & API** tab with live catalog of published paths, per-path publish
|
|
31
|
+
toggles, KIP copy-to-clipboard helpers.
|
|
32
|
+
- **Info** tab with quick-start, coexistence rules, credits, license, GitHub
|
|
33
|
+
link, config export / import (Web Share API → email / WhatsApp / Signal /
|
|
34
|
+
AirDrop, `.json` download, clipboard).
|
|
35
|
+
- Full i18n: **English / Español / Deutsch / Français** (auto-detected from
|
|
36
|
+
browser, override in Setup → Language).
|
|
37
|
+
- Regatta-safe swipe navigation between tabs with vertical-axis-lock so
|
|
38
|
+
scrolling never triggers a stray tab flip.
|
|
39
|
+
- Signal K polar-performance and any custom SK path bindable to a corner tile.
|
|
40
|
+
|
|
41
|
+
**~60 Signal K paths** under `steering.autopilot.pypilot.*` (opt-in per path):
|
|
42
|
+
|
|
43
|
+
- Gains per pilot (`P`, `I`, `D`, `DD`, `PR`, `FF`).
|
|
44
|
+
- Servo telemetry (voltage, current, controller / motor temperatures, flags,
|
|
45
|
+
amp-hours, clutch engaged, controller error).
|
|
46
|
+
- Rudder calibration (offset, scale, non-linearity, range, calibration state).
|
|
47
|
+
- IMU heading offset, errors, warnings.
|
|
48
|
+
- Active pilot, active profile, available pilots / profiles / modes.
|
|
49
|
+
- Tack state, tack direction, tack timeout.
|
|
50
|
+
- Runtime, version.
|
|
51
|
+
|
|
52
|
+
**KIP-ready Simple Switches** under `electrical.switches.pypilot.*`:
|
|
53
|
+
|
|
54
|
+
- `ap.state` — momentary boolean, engage / disengage.
|
|
55
|
+
- `nudge.{bigPort,smallPort,smallStar,bigStar}` — momentary, auto-reset.
|
|
56
|
+
- `tack.{port,starboard,cancel}` — momentary.
|
|
57
|
+
- `mode.{compass,gps,wind,trueWind,nav}` — radio (mutually exclusive).
|
|
58
|
+
- `profile.<name>` — radio, dynamic per pilot profile.
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
1. Signal K Server → **App Store** → search **PyPilot New-UI**.
|
|
63
|
+
2. Server → **Plugin Config** → **PyPilot New-UI + SK Paths**.
|
|
64
|
+
3. Enter the `pypilot_web` host (IP or hostname) and port. On TinyPilot the
|
|
65
|
+
default is port `80`; on a classic pypilot install it is `8000`.
|
|
66
|
+
4. The **Scan LAN** button in the webapp auto-detects hosts running
|
|
67
|
+
`pypilot_web` on ports 80 / 8000 of your subnet if you don't know the IP.
|
|
68
|
+
5. Enable the plugin.
|
|
69
|
+
|
|
70
|
+
**Webapp URL**: `http://<sk-host>:3000/signalk-pypilot-newui/`
|
|
71
|
+
|
|
72
|
+
## Working alongside `pypilot-autopilot-provider`
|
|
73
|
+
|
|
74
|
+
We build on top of the excellent
|
|
75
|
+
[`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)
|
|
76
|
+
by Panaaj — the plugin that gives Signal K the standard
|
|
77
|
+
**Autopilot API v2** surface (`/signalk/v2/api/vessels/self/autopilots/*`) so
|
|
78
|
+
WilhelmSK, freeboard and other clients can drive pypilot through the canonical
|
|
79
|
+
interface. Panaaj focuses tightly on that API and opens its own socket.io
|
|
80
|
+
connection to `pypilot_web`.
|
|
81
|
+
|
|
82
|
+
This plugin adds ~60 additional pypilot values on top (gains, servo telemetry,
|
|
83
|
+
calibration, warnings, profiles, tack detail, etc.), ships the touch webapp,
|
|
84
|
+
and can optionally register the Autopilot API v2 itself via the
|
|
85
|
+
**absorbProvider** option in Setup, so a single socket hits `pypilot_web`
|
|
86
|
+
instead of two — useful when pypilot runs on a Pi Zero W.
|
|
87
|
+
|
|
88
|
+
Signal K only allows one provider per pilot ID, so if you turn on
|
|
89
|
+
**absorbProvider** here you should disable Panaaj's plugin (otherwise both
|
|
90
|
+
register the same ID and behaviour is undefined). Valid setups:
|
|
91
|
+
|
|
92
|
+
| Panaaj `pypilot-autopilot-provider` | `signalk-pypilot-newui` absorb | Result |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Enabled | Off (default) | Both run happily side by side. Panaaj owns the AP v2 API; this plugin adds the extra paths + UI. **2 sockets** to `pypilot_web`. |
|
|
95
|
+
| Disabled | **On** | Only this plugin. Same AP v2 API + all the extras. **1 socket**. Handy on Pi Zero W. |
|
|
96
|
+
| Enabled | On | Both try to register the same AP ID — leave one off. |
|
|
97
|
+
| Disabled | Off | WilhelmSK / freeboard / KIP won't see the AP. Only useful if you exclusively use this plugin's own webapp. |
|
|
98
|
+
|
|
99
|
+
Absorb mode lives in **Setup → Autopilot Provider (one-socket mode)**.
|
|
100
|
+
|
|
101
|
+
## Remote `RESTART pypilot`
|
|
102
|
+
|
|
103
|
+
When the autopilot process on the TinyPilot gets wedged you don't need to open
|
|
104
|
+
an SSH session by hand. In **Setup → Emergency**, save the TinyPilot's SSH user
|
|
105
|
+
+ password once; the **RESTART pypilot** button then runs
|
|
106
|
+
`sudo systemctl restart pypilot pypilot_web` over SSH and reconnects the local
|
|
107
|
+
socket. Signal K stores plugin configs in plain text under
|
|
108
|
+
`~/.signalk/plugin-config-data/` so use credentials that are only valid for
|
|
109
|
+
that isolated TinyPilot.
|
|
110
|
+
|
|
111
|
+
## HTTP endpoints (plugin router)
|
|
112
|
+
|
|
113
|
+
| Method | Path | Purpose |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `GET` | `/plugins/signalk-pypilot-newui/status` | Connection + last-seen catalog summary + Rev + version |
|
|
116
|
+
| `GET` | `/plugins/signalk-pypilot-newui/scan` | Scan LAN for `pypilot_web` hosts |
|
|
117
|
+
| `GET` | `/plugins/signalk-pypilot-newui/paths` | Live list of published SK paths with GET / PUT URLs and units |
|
|
118
|
+
| `GET` | `/plugins/signalk-pypilot-newui/catalog` | Raw pypilot catalog (all values + metadata) |
|
|
119
|
+
| `PUT` | `/plugins/signalk-pypilot-newui/raw` | Send raw `name=value` to pypilot (protected by `allowWrites`) |
|
|
120
|
+
| `POST` | `/plugins/signalk-pypilot-newui/restart-pypilot` | SSH remote restart, uses `sshUser` + `sshPassword` from plugin config |
|
|
121
|
+
| `POST` | `/plugins/signalk-pypilot-newui/pause` | Disconnect local socket (for scripts) |
|
|
122
|
+
| `POST` | `/plugins/signalk-pypilot-newui/resume` | Reconnect local socket (for scripts) |
|
|
123
|
+
|
|
124
|
+
## Safety
|
|
125
|
+
|
|
126
|
+
- `allowDirectServo` defaults to **off** — the direct `servo.command` back-door
|
|
127
|
+
used by pypilot's own UI for manual steering is not exposed until you enable
|
|
128
|
+
it explicitly in the plugin config.
|
|
129
|
+
- All PUT handlers validate ranges from the pypilot catalog before sending.
|
|
130
|
+
- On plugin stop the socket closes cleanly; the visor auto-reconnects on
|
|
131
|
+
Signal K restart.
|
|
132
|
+
- This plugin is an aid to navigation and steering. It does not replace the
|
|
133
|
+
vigilance of the skipper, correct manoeuvring, direct observation of the
|
|
134
|
+
surroundings, or official charts and notices. Verify in safe waters before
|
|
135
|
+
relying on the autopilot for demanding manoeuvres.
|
|
136
|
+
|
|
137
|
+
## Acknowledgements
|
|
138
|
+
|
|
139
|
+
- **Sean D'Epagnier** — creator of [pypilot](https://github.com/pypilot/pypilot),
|
|
140
|
+
the open-source autopilot that makes this plugin possible.
|
|
141
|
+
- **Jean-Marc** at [Navitop](https://www.navitop.fr/) — for the TinyPilot
|
|
142
|
+
hardware and marine integrations, and for making pypilot accessible to
|
|
143
|
+
European sailors.
|
|
144
|
+
- The [Signal K](https://signalk.org/) community and OpenPlotter for the open
|
|
145
|
+
ecosystem this plugin runs on.
|
|
146
|
+
- Panaaj — for
|
|
147
|
+
[`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider),
|
|
148
|
+
the reference AP v2 provider this plugin builds on top of.
|
|
149
|
+
|
|
150
|
+
## Reporting issues / feature requests
|
|
151
|
+
|
|
152
|
+
Open an issue on GitHub —
|
|
153
|
+
[github.com/Aitonos/signalk-pypilot-newui/issues](https://github.com/Aitonos/signalk-pypilot-newui/issues).
|
|
154
|
+
Include the Rev (visible in the app under **Info** → top header, or via
|
|
155
|
+
`GET /plugins/signalk-pypilot-newui/status`), what you expected, what happened,
|
|
156
|
+
and a screenshot if possible.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
git clone https://github.com/Aitonos/signalk-pypilot-newui
|
|
162
|
+
cd signalk-pypilot-newui
|
|
163
|
+
npm install
|
|
164
|
+
npm run build
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Deploy from a Windows laptop to a Raspberry Pi: `.\deploy.ps1 -Restart`.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
Apache-2.0, Aitonos. See `NOTICE` for third-party attributions.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Autopilot Provider adapter. When props.absorbProvider is on, this plugin
|
|
3
|
+
// registers itself with the SK server as an autopilot provider, in addition
|
|
4
|
+
// to its own steering.autopilot.pypilot.* paths. Purpose: eliminate the
|
|
5
|
+
// duplicate socket to pypilot_web that occurs when both this plugin and the
|
|
6
|
+
// official pypilot-autopilot-provider are enabled.
|
|
7
|
+
//
|
|
8
|
+
// Translation semantics of engage / setMode / setTarget / tack / dodge come
|
|
9
|
+
// from the official pypilot-autopilot-provider (Apache-2.0, Panaaj). We
|
|
10
|
+
// re-implemented them here on top of our own PypilotClient so we keep a
|
|
11
|
+
// single socket to pypilot_web. See NOTICE + CHANGELOG for attribution.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.AutopilotProvider = void 0;
|
|
14
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
15
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
16
|
+
class AutopilotProvider {
|
|
17
|
+
constructor(client,
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
app, opts) {
|
|
20
|
+
this.client = client;
|
|
21
|
+
this.app = app;
|
|
22
|
+
this.deviceId = "pypilot-newui";
|
|
23
|
+
this.pilotIds = ["pypilot-newui"];
|
|
24
|
+
this.data = {
|
|
25
|
+
state: "off-line",
|
|
26
|
+
mode: null,
|
|
27
|
+
target: null,
|
|
28
|
+
engaged: false,
|
|
29
|
+
options: {
|
|
30
|
+
states: [
|
|
31
|
+
{ name: "enabled", engaged: true },
|
|
32
|
+
{ name: "disabled", engaged: false },
|
|
33
|
+
],
|
|
34
|
+
modes: [],
|
|
35
|
+
actions: [
|
|
36
|
+
{ id: "tack", name: "Tack", available: false },
|
|
37
|
+
{ id: "courseCurrentPoint", name: "To Destination", available: false },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
this.pypilotModes = [];
|
|
42
|
+
this.allowDodge = false;
|
|
43
|
+
if (opts?.allowDodge)
|
|
44
|
+
this.allowDodge = true;
|
|
45
|
+
}
|
|
46
|
+
// Called from PypilotClient 'value' event. Returns true if apData changed
|
|
47
|
+
// and an autopilotUpdate should be pushed.
|
|
48
|
+
receiveValue(name, value) {
|
|
49
|
+
let changed = false;
|
|
50
|
+
switch (name) {
|
|
51
|
+
case "ap.heading_command":
|
|
52
|
+
if (typeof value === "number") {
|
|
53
|
+
const rad = value * DEG_TO_RAD;
|
|
54
|
+
if (rad !== this.data.target) {
|
|
55
|
+
this.data.target = rad;
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (value === false && this.data.target !== null) {
|
|
60
|
+
this.data.target = null;
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case "ap.mode":
|
|
65
|
+
if (typeof value === "string" && value !== this.data.mode) {
|
|
66
|
+
this.data.mode = value;
|
|
67
|
+
if (this.data.options.modes.length === 0) {
|
|
68
|
+
this.data.options.modes.push(value);
|
|
69
|
+
}
|
|
70
|
+
changed = true;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "ap.modes":
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
this.pypilotModes = value.map(String);
|
|
76
|
+
this.data.options.modes = [...this.pypilotModes];
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case "ap.enabled": {
|
|
80
|
+
const eng = !!value;
|
|
81
|
+
const st = eng ? "enabled" : "disabled";
|
|
82
|
+
if (this.data.state !== st || this.data.engaged !== eng) {
|
|
83
|
+
this.data.state = st;
|
|
84
|
+
this.data.engaged = eng;
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (changed)
|
|
91
|
+
this.recomputeActions();
|
|
92
|
+
return changed;
|
|
93
|
+
}
|
|
94
|
+
markOffline() {
|
|
95
|
+
if (this.data.state !== "off-line" || this.data.engaged) {
|
|
96
|
+
this.data.state = "off-line";
|
|
97
|
+
this.data.engaged = false;
|
|
98
|
+
this.recomputeActions();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ---- write path ----
|
|
102
|
+
async setState(state) {
|
|
103
|
+
const st = this.data.options.states.find((s) => s.name === state);
|
|
104
|
+
if (!st)
|
|
105
|
+
throw new Error(`Invalid state: ${state}`);
|
|
106
|
+
this.client.set("ap.enabled", st.engaged);
|
|
107
|
+
return st.engaged;
|
|
108
|
+
}
|
|
109
|
+
async setMode(mode) {
|
|
110
|
+
if (this.data.options.modes.length > 0 &&
|
|
111
|
+
!this.data.options.modes.includes(mode)) {
|
|
112
|
+
throw new Error(`Invalid mode: ${mode}`);
|
|
113
|
+
}
|
|
114
|
+
this.client.set("ap.mode", mode);
|
|
115
|
+
}
|
|
116
|
+
async setTarget(rad) {
|
|
117
|
+
const deg = rad * RAD_TO_DEG;
|
|
118
|
+
this.client.set("ap.heading_command", deg);
|
|
119
|
+
}
|
|
120
|
+
async adjustTarget(rad) {
|
|
121
|
+
if (this.data.engaged) {
|
|
122
|
+
const base = typeof this.data.target === "number" ? this.data.target : 0;
|
|
123
|
+
const newRad = base + rad;
|
|
124
|
+
this.client.set("ap.heading_command", newRad * RAD_TO_DEG);
|
|
125
|
+
}
|
|
126
|
+
else if (this.allowDodge) {
|
|
127
|
+
await this.dodge(rad);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw new Error("Adjust while disengaged requires allowDirectServo");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async tack(direction) {
|
|
134
|
+
// The upstream provider only writes ap.tack.direction and lets pypilot
|
|
135
|
+
// auto-begin. We explicitly send begin too so the tack starts even on
|
|
136
|
+
// pypilot versions that require it.
|
|
137
|
+
this.client.set("ap.tack.direction", direction);
|
|
138
|
+
this.client.set("ap.tack.state", "begin");
|
|
139
|
+
}
|
|
140
|
+
async engage() {
|
|
141
|
+
try {
|
|
142
|
+
await this.setNavMode();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
await this.setState("enabled");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async disengage() {
|
|
149
|
+
await this.setState("disabled");
|
|
150
|
+
}
|
|
151
|
+
async setNavMode() {
|
|
152
|
+
const cdata = await this.app.getCourse?.();
|
|
153
|
+
if (cdata?.nextPoint &&
|
|
154
|
+
this.getAvailableActionIds().includes("courseCurrentPoint")) {
|
|
155
|
+
await this.setMode("nav");
|
|
156
|
+
setTimeout(() => this.setState("enabled").catch(() => { }), 500);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
throw new Error("Nav mode is not available");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async dodge(rad) {
|
|
163
|
+
if (!this.allowDodge) {
|
|
164
|
+
throw new Error("Dodge requires allowDirectServo=true in plugin config");
|
|
165
|
+
}
|
|
166
|
+
// Simple dodge: emit a servo.command pulse. Upstream watchdogs it every
|
|
167
|
+
// 200 ms for a couple of ticks; we only emit once and let pypilot's own
|
|
168
|
+
// servo watchdog return the rudder to neutral (6 s upstream default).
|
|
169
|
+
const sign = rad > 0 ? 1 : -1;
|
|
170
|
+
this.client.set("servo.command", -sign);
|
|
171
|
+
}
|
|
172
|
+
recomputeActions() {
|
|
173
|
+
for (const a of this.data.options.actions) {
|
|
174
|
+
if (a.id === "tack") {
|
|
175
|
+
a.available = this.data.engaged;
|
|
176
|
+
}
|
|
177
|
+
else if (a.id === "courseCurrentPoint") {
|
|
178
|
+
a.available = this.data.engaged && this.pypilotModes.includes("nav");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
getAvailableActionIds() {
|
|
183
|
+
return this.data.options.actions
|
|
184
|
+
.filter((a) => a.available)
|
|
185
|
+
.map((a) => a.id);
|
|
186
|
+
}
|
|
187
|
+
// Object literal expected by app.registerAutopilotProvider(...).
|
|
188
|
+
toProviderInterface() {
|
|
189
|
+
const self = this;
|
|
190
|
+
return {
|
|
191
|
+
getData: async (_id) => self.data,
|
|
192
|
+
getState: async (_id) => self.data.state,
|
|
193
|
+
setState: async (state, _id) => {
|
|
194
|
+
await self.setState(state);
|
|
195
|
+
},
|
|
196
|
+
getMode: async (_id) => self.data.mode,
|
|
197
|
+
setMode: async (mode, _id) => self.setMode(mode),
|
|
198
|
+
getTarget: async (_id) => self.data.target,
|
|
199
|
+
setTarget: async (value, _id) => self.setTarget(value),
|
|
200
|
+
adjustTarget: async (value, _id) => self.adjustTarget(value),
|
|
201
|
+
engage: async (_id) => self.engage(),
|
|
202
|
+
disengage: async (_id) => self.disengage(),
|
|
203
|
+
courseCurrentPoint: async (_id) => self.setNavMode(),
|
|
204
|
+
courseNextPoint: async (_id) => {
|
|
205
|
+
throw new Error("Not implemented");
|
|
206
|
+
},
|
|
207
|
+
tack: async (direction, _id) => self.tack(direction),
|
|
208
|
+
gybe: async (_direction, _id) => {
|
|
209
|
+
throw new Error("Not implemented");
|
|
210
|
+
},
|
|
211
|
+
dodge: async (value, _id) => {
|
|
212
|
+
if (value)
|
|
213
|
+
await self.dodge(value);
|
|
214
|
+
else
|
|
215
|
+
throw new Error("Not implemented");
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.AutopilotProvider = AutopilotProvider;
|