signalk-pypilot-newui 1.0.0 → 2.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 +91 -0
- package/README.md +45 -5
- package/dist/index.js +130 -43
- package/dist/publisher.js +72 -226
- package/dist/pypilot-client.js +25 -9
- package/package.json +1 -1
- package/public/app.js +224 -18
- package/public/index.html +72 -4
- package/public/style.css +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0 — 2026-08-20 — Rev63
|
|
4
|
+
|
|
5
|
+
**Breaking release addressing feedback from Sean D'Epagnier (pypilot author,
|
|
6
|
+
[#1](https://github.com/Aitonos/signalk-pypilot-newui/issues/1) and
|
|
7
|
+
[#2](https://github.com/Aitonos/signalk-pypilot-newui/issues/2)).**
|
|
8
|
+
|
|
9
|
+
### Breaking - path standardisation (issue #1)
|
|
10
|
+
|
|
11
|
+
Every pypilot key now maps 1:1 to `steering.autopilot.pypilot.<key>`
|
|
12
|
+
verbatim. The old `FIXED_MAPPINGS` table with hand-picked renames and
|
|
13
|
+
C -> K / deg -> rad conversions is gone. Signal K infers units cleanly
|
|
14
|
+
enough from the path type, consumers pick their display unit, and we
|
|
15
|
+
surface pypilot's own `meta.units` verbatim.
|
|
16
|
+
|
|
17
|
+
**If you have KIP widgets or WilhelmSK dashboards wired to 1.0.0 paths,
|
|
18
|
+
rewire them per the table below**:
|
|
19
|
+
|
|
20
|
+
| Old (1.0.0) | New (2.0.0) |
|
|
21
|
+
|-------------------------------------------------------------------|----------------------------------------------------------|
|
|
22
|
+
| `steering.autopilot.pypilot.availableModes` | `steering.autopilot.pypilot.ap.modes` |
|
|
23
|
+
| `steering.autopilot.pypilot.pilot` | `steering.autopilot.pypilot.ap.pilot` |
|
|
24
|
+
| `steering.autopilot.pypilot.gains.<pilot>.<gain>` | `steering.autopilot.pypilot.ap.pilot.<pilot>.<gain>` |
|
|
25
|
+
| `steering.autopilot.pypilot.servo.controllerTemperature` (K) | `steering.autopilot.pypilot.servo.controller_temp` (C) |
|
|
26
|
+
| `steering.autopilot.pypilot.servo.motorTemperature` (K) | `steering.autopilot.pypilot.servo.motor_temp` (C) |
|
|
27
|
+
| `steering.autopilot.pypilot.servo.ampHours` | `steering.autopilot.pypilot.servo.amp_hours` |
|
|
28
|
+
| `steering.autopilot.pypilot.calibration.imuHeadingOffset` (rad) | `steering.autopilot.pypilot.imu.heading_offset` (deg) |
|
|
29
|
+
| `steering.autopilot.pypilot.calibration.rudderRange` (rad) | `steering.autopilot.pypilot.rudder.range` (deg) |
|
|
30
|
+
| `steering.autopilot.pypilot.calibration.rudderOffset` | `steering.autopilot.pypilot.rudder.offset` |
|
|
31
|
+
| `steering.autopilot.pypilot.calibration.rudderScale` | `steering.autopilot.pypilot.rudder.scale` |
|
|
32
|
+
| `steering.autopilot.pypilot.calibration.rudderNonlinearity` | `steering.autopilot.pypilot.rudder.nonlinearity` |
|
|
33
|
+
| `steering.autopilot.pypilot.calibration.state` | `steering.autopilot.pypilot.rudder.calibration_state` |
|
|
34
|
+
| `steering.autopilot.pypilot.tack.state` | `steering.autopilot.pypilot.ap.tack.state` |
|
|
35
|
+
| `steering.autopilot.pypilot.tack.direction` | `steering.autopilot.pypilot.ap.tack.direction` |
|
|
36
|
+
| `steering.autopilot.pypilot.tack.timeout` | `steering.autopilot.pypilot.ap.tack.timeout` |
|
|
37
|
+
| `steering.autopilot.pypilot.errors.imu` | `steering.autopilot.pypilot.imu.error` |
|
|
38
|
+
| `steering.autopilot.pypilot.warnings.imu` | `steering.autopilot.pypilot.imu.warning` |
|
|
39
|
+
| `steering.autopilot.pypilot.errors.controller` | `steering.autopilot.pypilot.servo.controller` |
|
|
40
|
+
|
|
41
|
+
`steering.autopilot.pypilot.profile`, `.profiles`, `.availablePilots`,
|
|
42
|
+
`.version`, `.runtime`, `.servo.voltage`, `.servo.current`,
|
|
43
|
+
`.servo.engaged`, `.servo.flags`, `.servo.controller` are unchanged
|
|
44
|
+
(their pypilot keys already matched the SK-side name).
|
|
45
|
+
|
|
46
|
+
### Fixed (issue #2) - `pypilot_values` is not one-shot
|
|
47
|
+
|
|
48
|
+
Sean noted that `pypilot_values` can arrive multiple times, and on
|
|
49
|
+
subsequent deliveries it carries only the newly-appeared keys (typically
|
|
50
|
+
during boot while calibration is still loading). Our code assumed
|
|
51
|
+
one-shot AND replaced `this.catalog` wholesale on each delivery.
|
|
52
|
+
|
|
53
|
+
- Comment in `pypilot-client.ts` fixed.
|
|
54
|
+
- Catalog is now MERGED, not replaced.
|
|
55
|
+
- `catalog` event carries `{ isDelta, newKeys }` so downstream handlers
|
|
56
|
+
can register put-handlers only for the new keys and avoid re-emitting
|
|
57
|
+
meta for paths already published.
|
|
58
|
+
|
|
59
|
+
### Added - Debug console in Setup
|
|
60
|
+
|
|
61
|
+
New closed-whitelist SSH endpoint `POST /plugins/*/debug-cmd` accepting
|
|
62
|
+
one of the following presets (nothing arbitrary):
|
|
63
|
+
|
|
64
|
+
- `logs.pypilot` - last 200 lines of `journalctl -u pypilot`
|
|
65
|
+
- `logs.pypilot_web` - last 200 lines of the web unit
|
|
66
|
+
- `logs.follow` - last 30 s (polled every 3 s from the UI when
|
|
67
|
+
"Follow logs" is on)
|
|
68
|
+
- `dmesg.tail` - last 100 lines of `dmesg`
|
|
69
|
+
- `top.snapshot` - one-shot `top -bn1`
|
|
70
|
+
- `uptime`, `df`, `pypilot.version`
|
|
71
|
+
- `restart.web` - restart only `pypilot_web` (level 1 of the triad)
|
|
72
|
+
- `reboot.pi` - full `sudo reboot` (level 3 of the triad)
|
|
73
|
+
|
|
74
|
+
UI in Setup: preset buttons, scrollable output textarea, follow-logs
|
|
75
|
+
toggle (3 s polling), copy-to-clipboard, share-to-anywhere (Web Share
|
|
76
|
+
API), clear.
|
|
77
|
+
|
|
78
|
+
### Changed - three escalated restart levels in Setup / Emergency
|
|
79
|
+
|
|
80
|
+
The old single "RESTART pypilot" button becomes a triad, colour-coded
|
|
81
|
+
from green (safest) to red (loudest):
|
|
82
|
+
|
|
83
|
+
- **1. restart pypilot_web** (~3-5 s) - AP keeps steering.
|
|
84
|
+
- **2. RESTART pypilot** (~10-15 s) - the previous behaviour: AP drops
|
|
85
|
+
the heading briefly and re-engages after reconnect.
|
|
86
|
+
- **3. reboot Pi** (~35-60 s) - full Raspberry Pi reboot, no AP for
|
|
87
|
+
almost a minute.
|
|
88
|
+
|
|
89
|
+
All three go through a shared **helm-manned confirmation modal** that
|
|
90
|
+
states the outage duration and scope in the current UI language and
|
|
91
|
+
asks the operator to confirm someone is at the helm before executing.
|
|
92
|
+
Cancel button, click outside and Escape all decline safely.
|
|
93
|
+
|
|
3
94
|
## 1.0.0 — 2026-08-18 — Rev32..Rev62 first public release
|
|
4
95
|
|
|
5
96
|
The webapp went from a minimum viable control panel to a full-blown touch
|
package/README.md
CHANGED
|
@@ -9,6 +9,24 @@ and switches out of the box.
|
|
|
9
9
|
Runs alongside [`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)
|
|
10
10
|
by Panaaj — see [Working alongside pypilot-autopilot-provider](#working-alongside-pypilot-autopilot-provider).
|
|
11
11
|
|
|
12
|
+
## Upgrading from 1.0.0 to 2.0.0
|
|
13
|
+
|
|
14
|
+
2.0.0 is a **breaking release** addressing feedback from Sean D'Epagnier
|
|
15
|
+
(pypilot author). Every pypilot key now maps 1:1 to
|
|
16
|
+
`steering.autopilot.pypilot.<key>` verbatim - no more hand-picked renames
|
|
17
|
+
or C -> K / deg -> rad conversions. If you have KIP widgets or WilhelmSK
|
|
18
|
+
dashboards wired to 1.0.0 paths, see the full migration table in
|
|
19
|
+
[`CHANGELOG.md`](./CHANGELOG.md) - most notable examples:
|
|
20
|
+
|
|
21
|
+
- `.servo.controllerTemperature` (K) -> `.servo.controller_temp` (C)
|
|
22
|
+
- `.calibration.rudderRange` (rad) -> `.rudder.range` (deg)
|
|
23
|
+
- `.gains.<pilot>.<gain>` -> `.ap.pilot.<pilot>.<gain>`
|
|
24
|
+
|
|
25
|
+
Also new in 2.0.0: three escalated **restart / reboot levels** in
|
|
26
|
+
Setup -> Emergency, all guarded by a helm-manned confirmation modal, and
|
|
27
|
+
a **Debug console** with an SSH-based whitelist of preset diagnostic
|
|
28
|
+
commands (logs, dmesg, top, uptime, df, pypilot version).
|
|
29
|
+
|
|
12
30
|
## What it does
|
|
13
31
|
|
|
14
32
|
**Touch webapp** (mobile / tablet / chart-plotter, dark theme):
|
|
@@ -98,13 +116,34 @@ register the same ID and behaviour is undefined). Valid setups:
|
|
|
98
116
|
|
|
99
117
|
Absorb mode lives in **Setup → Autopilot Provider (one-socket mode)**.
|
|
100
118
|
|
|
101
|
-
## Remote
|
|
119
|
+
## Remote restart / reboot triad + Debug console
|
|
102
120
|
|
|
103
121
|
When the autopilot process on the TinyPilot gets wedged you don't need to open
|
|
104
122
|
an SSH session by hand. In **Setup → Emergency**, save the TinyPilot's SSH user
|
|
105
|
-
+ password once
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
+ password once. From then on you get three escalated actions, colour-coded
|
|
124
|
+
from safest to loudest, each guarded by a helm-manned confirmation modal that
|
|
125
|
+
asks whether someone is at the helm before executing:
|
|
126
|
+
|
|
127
|
+
1. **restart pypilot_web** (~3-5 s) - only the web server restarts. The AP
|
|
128
|
+
core keeps steering. Use when the visor gets stuck but the pilot is fine.
|
|
129
|
+
2. **RESTART pypilot** (~10-15 s) - restarts pypilot core + web. The AP drops
|
|
130
|
+
the heading briefly and re-engages when it comes back up.
|
|
131
|
+
3. **reboot Pi** (~35-60 s) - full `sudo reboot` of the Raspberry Pi. No
|
|
132
|
+
autopilot for almost a minute. Last-resort when the box is in a bad state.
|
|
133
|
+
|
|
134
|
+
Underneath the triad there is a **Debug console** that runs a closed
|
|
135
|
+
whitelist of preset diagnostic commands over the same SSH session:
|
|
136
|
+
`logs pypilot`, `logs pypilot_web`, `dmesg`, `top`, `uptime`, `df`,
|
|
137
|
+
`pypilot --version`. Output goes into a scrollable textarea, a "Follow
|
|
138
|
+
logs" toggle polls `journalctl --since '30 s ago'` every 3 seconds, and
|
|
139
|
+
Copy / Share buttons let you paste the dump straight into a GitHub
|
|
140
|
+
issue or a WhatsApp / email conversation with your friendly rigger.
|
|
141
|
+
|
|
142
|
+
The endpoint (`POST /plugins/*/debug-cmd`) refuses any command that is
|
|
143
|
+
not in the whitelist, so a leaked JWT cannot be used to run arbitrary
|
|
144
|
+
shell on the TinyPilot.
|
|
145
|
+
|
|
146
|
+
Signal K stores plugin configs in plain text under
|
|
108
147
|
`~/.signalk/plugin-config-data/` so use credentials that are only valid for
|
|
109
148
|
that isolated TinyPilot.
|
|
110
149
|
|
|
@@ -117,7 +156,8 @@ that isolated TinyPilot.
|
|
|
117
156
|
| `GET` | `/plugins/signalk-pypilot-newui/paths` | Live list of published SK paths with GET / PUT URLs and units |
|
|
118
157
|
| `GET` | `/plugins/signalk-pypilot-newui/catalog` | Raw pypilot catalog (all values + metadata) |
|
|
119
158
|
| `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 |
|
|
159
|
+
| `POST` | `/plugins/signalk-pypilot-newui/restart-pypilot` | SSH remote restart (level 2 of the triad), uses `sshUser` + `sshPassword` from plugin config |
|
|
160
|
+
| `POST` | `/plugins/signalk-pypilot-newui/debug-cmd` | Run one of a closed whitelist of preset diagnostic / restart commands over SSH (levels 1 and 3 of the triad + logs / dmesg / top / etc). Body: `{ "preset": "<name>" }` |
|
|
121
161
|
| `POST` | `/plugins/signalk-pypilot-newui/pause` | Disconnect local socket (for scripts) |
|
|
122
162
|
| `POST` | `/plugins/signalk-pypilot-newui/resume` | Reconnect local socket (for scripts) |
|
|
123
163
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const scanner_1 = require("./scanner");
|
|
|
6
6
|
const autopilot_provider_1 = require("./autopilot-provider");
|
|
7
7
|
// Rev counter bumped on every build so the user can distinguish deploys
|
|
8
8
|
// from the webapp header (feedback_revision_bump_each_build).
|
|
9
|
-
const PLUGIN_REVISION = "
|
|
9
|
+
const PLUGIN_REVISION = "Rev63";
|
|
10
10
|
// Rev59: read package.json once at load time so /status can report the
|
|
11
11
|
// npm package version alongside the internal Rev counter.
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -164,13 +164,23 @@ module.exports = function (app) {
|
|
|
164
164
|
client.on("pong", (latency) => {
|
|
165
165
|
lastPingLatencyMs = latency;
|
|
166
166
|
});
|
|
167
|
-
client.on("catalog", (catalog) => {
|
|
167
|
+
client.on("catalog", (catalog, info) => {
|
|
168
|
+
const isDelta = !!info?.isDelta;
|
|
168
169
|
lastCatalog = catalog;
|
|
169
|
-
|
|
170
|
+
// Rev63 / 2.0.0 (issue #2): only reset the meta-sent tracker on the
|
|
171
|
+
// INITIAL catalog. A delta catalog carries only newly-appeared keys
|
|
172
|
+
// (typically calibration finishing to load a few seconds after
|
|
173
|
+
// core), and we do NOT want to re-emit meta for the paths we
|
|
174
|
+
// already published - it would flood the SK bus with duplicate
|
|
175
|
+
// meta blobs. `metaSent` naturally excludes the new keys because
|
|
176
|
+
// they were never added to it, so those will emit meta on first
|
|
177
|
+
// publish as expected.
|
|
178
|
+
if (!isDelta)
|
|
179
|
+
metaSent = new Set();
|
|
170
180
|
const catalogKeys = Object.keys(catalog);
|
|
171
181
|
const gainKeys = catalogKeys.filter((k) => k.startsWith("ap.pilot.") && catalog[k].AutopilotGain);
|
|
172
182
|
const apKeys = catalogKeys.filter((k) => k.startsWith("ap.pilot."));
|
|
173
|
-
app.setPluginStatus(`${PLUGIN_REVISION} - connected, catalog ${catalogKeys.length} vars (ap.pilots.*=${apKeys.length}, AutopilotGain=${gainKeys.length})`);
|
|
183
|
+
app.setPluginStatus(`${PLUGIN_REVISION} - connected, catalog ${catalogKeys.length} vars (ap.pilots.*=${apKeys.length}, AutopilotGain=${gainKeys.length})${isDelta ? " [+" + info.newKeys.length + " new]" : ""}`);
|
|
174
184
|
setupWatches(client, catalog);
|
|
175
185
|
registerPutHandlers(catalog);
|
|
176
186
|
publishCatalogDerived(catalog);
|
|
@@ -290,15 +300,7 @@ module.exports = function (app) {
|
|
|
290
300
|
for (const [name, meta] of Object.entries(lastCatalog)) {
|
|
291
301
|
if (publisher_1.RESERVED_PYPILOT_KEYS.has(name))
|
|
292
302
|
continue;
|
|
293
|
-
|
|
294
|
-
if (!mapping)
|
|
295
|
-
mapping = (0, publisher_1.mapDynamicName)(name, lastCatalog);
|
|
296
|
-
if (!mapping) {
|
|
297
|
-
if (props.publishUnmapped)
|
|
298
|
-
mapping = (0, publisher_1.autoMap)(name);
|
|
299
|
-
else
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
303
|
+
const mapping = (0, publisher_1.mappingFor)(name, meta);
|
|
302
304
|
items.push({
|
|
303
305
|
pypilotName: name,
|
|
304
306
|
skPath: mapping.skPath,
|
|
@@ -634,6 +636,106 @@ module.exports = function (app) {
|
|
|
634
636
|
return res.status(500).json({ error: e?.message || String(e) });
|
|
635
637
|
}
|
|
636
638
|
});
|
|
639
|
+
// Rev63 / 2.0.0: Debug console. A whitelist of preset commands
|
|
640
|
+
// executed over the same ssh2 session as /restart-pypilot. The
|
|
641
|
+
// whitelist is CLOSED - the endpoint refuses any command not in
|
|
642
|
+
// DEBUG_PRESETS below, so a leaked JWT cannot be used to run
|
|
643
|
+
// arbitrary shell on the TinyPilot. Requires allowWrites.
|
|
644
|
+
const DEBUG_PRESETS = {
|
|
645
|
+
"logs.pypilot": "journalctl -u pypilot -n 200 --no-pager 2>&1 || (test -f /var/log/pypilot.log && tail -n 200 /var/log/pypilot.log) || echo 'no journal / no logfile'",
|
|
646
|
+
"logs.pypilot_web": "journalctl -u pypilot_web -n 200 --no-pager 2>&1 || echo 'no pypilot_web unit'",
|
|
647
|
+
"logs.follow": "journalctl -u pypilot --since '30 seconds ago' --no-pager 2>&1",
|
|
648
|
+
"dmesg.tail": "dmesg 2>&1 | tail -100",
|
|
649
|
+
"top.snapshot": "top -bn1 2>&1 | head -20",
|
|
650
|
+
"uptime": "uptime && cat /proc/loadavg",
|
|
651
|
+
"df": "df -h 2>&1",
|
|
652
|
+
"pypilot.version": "pypilot --version 2>&1 || (test -f /home/tc/pypilot/version && cat /home/tc/pypilot/version) || echo 'unknown'",
|
|
653
|
+
"restart.web": "sudo systemctl restart pypilot_web 2>&1 && echo OK",
|
|
654
|
+
"reboot.pi": "sudo reboot",
|
|
655
|
+
};
|
|
656
|
+
router.post("/debug-cmd", async (req, res) => {
|
|
657
|
+
if (!props.allowWrites)
|
|
658
|
+
return res.status(403).json({ error: "allowWrites disabled" });
|
|
659
|
+
const preset = (req.body && req.body.preset) || "";
|
|
660
|
+
const cmd = DEBUG_PRESETS[preset];
|
|
661
|
+
if (!cmd) {
|
|
662
|
+
return res.status(400).json({
|
|
663
|
+
error: "unknown preset",
|
|
664
|
+
available: Object.keys(DEBUG_PRESETS),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
const sshUser = props.sshUser || "tc";
|
|
668
|
+
const sshPassword = props.sshPassword || "";
|
|
669
|
+
if (!sshPassword) {
|
|
670
|
+
return res.status(202).json({
|
|
671
|
+
ok: false,
|
|
672
|
+
preset,
|
|
673
|
+
hint: "SSH password not set - open Setup > Emergency and save it.",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
678
|
+
const { Client } = require("ssh2");
|
|
679
|
+
const conn = new Client();
|
|
680
|
+
const t0 = Date.now();
|
|
681
|
+
const result = await new Promise((resolve) => {
|
|
682
|
+
let settled = false;
|
|
683
|
+
const finish = (r) => {
|
|
684
|
+
if (settled)
|
|
685
|
+
return;
|
|
686
|
+
settled = true;
|
|
687
|
+
try {
|
|
688
|
+
conn.end();
|
|
689
|
+
}
|
|
690
|
+
catch { }
|
|
691
|
+
resolve(r);
|
|
692
|
+
};
|
|
693
|
+
conn.on("ready", () => {
|
|
694
|
+
conn.exec(cmd, { pty: true }, (err, stream) => {
|
|
695
|
+
if (err) {
|
|
696
|
+
finish({ ok: false, stdout: err.message });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
let out = "";
|
|
700
|
+
stream.on("close", (code) => finish({ ok: code === 0, stdout: out, code }));
|
|
701
|
+
stream.on("data", (data) => { out += data.toString(); });
|
|
702
|
+
stream.stderr.on("data", (data) => { out += data.toString(); });
|
|
703
|
+
setTimeout(() => { try {
|
|
704
|
+
stream.write(sshPassword + "\n");
|
|
705
|
+
}
|
|
706
|
+
catch { } }, 300);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
conn.on("error", (err) => finish({ ok: false, stdout: `ssh: ${err?.message || err}` }));
|
|
710
|
+
conn.on("timeout", () => finish({ ok: false, stdout: "ssh timeout" }));
|
|
711
|
+
try {
|
|
712
|
+
conn.connect({
|
|
713
|
+
host: props.host, port: 22,
|
|
714
|
+
username: sshUser, password: sshPassword,
|
|
715
|
+
readyTimeout: 8000, tryKeyboard: true,
|
|
716
|
+
});
|
|
717
|
+
conn.on("keyboard-interactive", (_n, _i, _l, _p, finish2) => {
|
|
718
|
+
finish2([sshPassword]);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
finish({ ok: false, stdout: e?.message || String(e) });
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
// For reboot.pi the ssh session drops as the box goes down, so we
|
|
726
|
+
// report success even on a non-zero exit if we at least connected.
|
|
727
|
+
return res.json({
|
|
728
|
+
ok: preset === "reboot.pi" ? true : result.ok,
|
|
729
|
+
preset,
|
|
730
|
+
elapsedMs: Date.now() - t0,
|
|
731
|
+
code: result.code,
|
|
732
|
+
stdout: result.stdout.slice(0, 32000),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
catch (e) {
|
|
736
|
+
return res.status(500).json({ error: e?.message || String(e), preset });
|
|
737
|
+
}
|
|
738
|
+
});
|
|
637
739
|
},
|
|
638
740
|
};
|
|
639
741
|
// ---- helpers ----
|
|
@@ -805,17 +907,11 @@ module.exports = function (app) {
|
|
|
805
907
|
return;
|
|
806
908
|
}
|
|
807
909
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
return;
|
|
814
|
-
mapping = (0, publisher_1.autoMap)(name);
|
|
815
|
-
}
|
|
816
|
-
if (mapping.reserved)
|
|
817
|
-
return;
|
|
818
|
-
const skValue = (0, publisher_1.convertForSK)(mapping, value);
|
|
910
|
+
// Rev63 / 2.0.0: every pypilot key derives to steering.autopilot.pypilot.<key>
|
|
911
|
+
// verbatim (Sean D'Epagnier's suggestion in #1). No conversions, no rename,
|
|
912
|
+
// no hardcoded table - Signal K + downstream consumers handle units.
|
|
913
|
+
const mapping = (0, publisher_1.mappingFor)(name, lastCatalog[name]);
|
|
914
|
+
const skValue = value;
|
|
819
915
|
const updateEntry = {
|
|
820
916
|
$source: SOURCE_LABEL,
|
|
821
917
|
timestamp: new Date().toISOString(),
|
|
@@ -891,13 +987,10 @@ module.exports = function (app) {
|
|
|
891
987
|
if (!name) {
|
|
892
988
|
return { state: "COMPLETED", statusCode: 404, message: "unknown path" };
|
|
893
989
|
}
|
|
894
|
-
//
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
pypilotValue = value * 180 / Math.PI;
|
|
899
|
-
}
|
|
900
|
-
client.set(name, pypilotValue);
|
|
990
|
+
// Rev63 / 2.0.0: values are surfaced verbatim, so PUT writes verbatim
|
|
991
|
+
// too. No reverse conversion needed - what the consumer sends is what
|
|
992
|
+
// pypilot receives.
|
|
993
|
+
client.set(name, value);
|
|
901
994
|
return { state: "COMPLETED", statusCode: 200 };
|
|
902
995
|
};
|
|
903
996
|
try {
|
|
@@ -908,22 +1001,16 @@ module.exports = function (app) {
|
|
|
908
1001
|
app.debug(`[put] register failed for ${skPath}: ${e?.message || e}`);
|
|
909
1002
|
}
|
|
910
1003
|
};
|
|
911
|
-
for
|
|
912
|
-
|
|
1004
|
+
// Register a PUT handler for every catalog key we consider writeable.
|
|
1005
|
+
// With verbatim mapping (Rev63) this collapses to a single loop.
|
|
1006
|
+
for (const [name, meta] of Object.entries(catalog)) {
|
|
1007
|
+
if (publisher_1.RESERVED_PYPILOT_KEYS.has(name))
|
|
913
1008
|
continue;
|
|
914
|
-
|
|
1009
|
+
const mapping = (0, publisher_1.mappingFor)(name, meta);
|
|
1010
|
+
if (mapping.putKind !== "plain")
|
|
915
1011
|
continue;
|
|
916
1012
|
registerOne(mapping.skPath);
|
|
917
1013
|
}
|
|
918
|
-
// Gains: register handler for each discovered ap.pilots.<pilot>.<gain> that
|
|
919
|
-
// catalog exposes as AutopilotGain.
|
|
920
|
-
for (const [name, meta] of Object.entries(catalog)) {
|
|
921
|
-
if (!meta.AutopilotGain)
|
|
922
|
-
continue;
|
|
923
|
-
const m = (0, publisher_1.mapDynamicName)(name, catalog);
|
|
924
|
-
if (m)
|
|
925
|
-
registerOne(m.skPath);
|
|
926
|
-
}
|
|
927
1014
|
}
|
|
928
1015
|
// Rev24: dedicated ACTION paths for KIP-friendly PUT buttons.
|
|
929
1016
|
// KIP has widgets that PUT a fixed value on click - perfect for +10/+1/AP/Tack buttons.
|