@h2loop/uav 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/.hydron-plugin/plugin.json +3 -0
- package/README.md +281 -0
- package/USAGE.md +172 -0
- package/agents/uav-compare.md +36 -0
- package/agents/uav-investigate.md +60 -0
- package/agents/uav-verify.md +45 -0
- package/bridge/README.md +163 -0
- package/bridge/requirements.txt +2 -0
- package/bridge/test_bridge.py +153 -0
- package/bridge/uav_mavlink_bridge.py +799 -0
- package/commands/uav-batch-report.md +20 -0
- package/commands/uav-compare.md +14 -0
- package/commands/uav-preflight.md +12 -0
- package/commands/uav-save-finding.md +25 -0
- package/commands/uav-tune.md +12 -0
- package/data/apm-params.json +1 -0
- package/data/uav-format-converters.example.json +17 -0
- package/mcp.json +15 -0
- package/package.json +43 -0
- package/plugins/index.js +46 -0
- package/plugins/worker/worker-entry.js +11 -0
- package/skills/uav/SKILL.md +101 -0
- package/skills/uav/references/checks.md +174 -0
- package/skills/uav/references/compare.md +49 -0
- package/skills/uav/references/grounding.md +94 -0
- package/skills/uav/references/project-config.md +27 -0
- package/skills/uav/references/signals.md +60 -0
- package/skills/uav/references/tuning-methodology.md +79 -0
- package/skills/uav/references/tuning.md +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# @h2loop/uav — Hydron UAV Domain Pack
|
|
2
|
+
|
|
3
|
+
ArduPilot/PX4 flight-log intelligence for [Hydron](../hydron-cli): decode a `.bin`, run
|
|
4
|
+
deterministic system checks, persist the observation locally, and compare against the
|
|
5
|
+
airframe's prior flight. Built as an **external Hydron plugin** (no core rebuild).
|
|
6
|
+
|
|
7
|
+
**→ [USAGE.md](USAGE.md) — install, validate, and everyday use** (start here). Also:
|
|
8
|
+
[CHANGELOG.md](CHANGELOG.md) · [PACKAGING.md](PACKAGING.md) (distribution/IP).
|
|
9
|
+
|
|
10
|
+
Design docs: `internal-docs/wiki/engineering/features/domain-plugins/uav/`.
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
This cycle = **P1 + P1b + P2** (verified against the real sample log `17-03-2026 Sortie 4.bin`):
|
|
15
|
+
|
|
16
|
+
- ✅ **Decode** — pure-TS FMT-driven DataFlash decoder, no runtime deps, validated
|
|
17
|
+
field-by-field against the 73 oracle CSVs (`src/decode/`). TimeUS kept as `bigint`.
|
|
18
|
+
- ✅ **Decode dual-stack** — TS `.bin`; pymavlink `.tlog`/`.log`; **PX4 `.ulog`** (pyulog).
|
|
19
|
+
Same `DecodedLog` → the same checks run on PX4.
|
|
20
|
+
- ✅ **Check** — **23 system checks + param audit** over in-memory `bun:sqlite`: power
|
|
21
|
+
(cell-V, sag, current, internal-resistance, brownout), control (attitude, rate-tracking,
|
|
22
|
+
hover, rcou/rcin, thrust-margin), sensors (vibration **+ alignment/degradation classifier**,
|
|
23
|
+
imu, mag/offsets, gps, **EKF/EKF2 velocity+position innovations**, variance), ESC, and
|
|
24
|
+
ERR/MODE event findings. Deterministic; missing tables → `SKIP`.
|
|
25
|
+
- ✅ **Param audit + grounding** — safety/range/consistency over PARM, ranges grounded in
|
|
26
|
+
ArduPilot's **`apm.pdef.xml`** metadata (3419 params bundled; KG path for online).
|
|
27
|
+
- ✅ **Tuning (USP)** — `uav_tune`: FFT → dominant peak → recommended `INS_HNTCH_*` notch params.
|
|
28
|
+
- ✅ **MAVLink param link** — bridge Tier-C `param_get/set` (gated `--allow-param-write`) for
|
|
29
|
+
the push + retest loop; flight C2 intentionally absent.
|
|
30
|
+
- ✅ **Persist + run-tracking** — local episode store + keyed diff; `uav_tag` run labels +
|
|
31
|
+
airframe-profile baseline (`diffAgainstProfile`).
|
|
32
|
+
- ✅ **PlotJuggler handoff** — `uav_export` writes one CSV per message type.
|
|
33
|
+
- ✅ **Control plane** — on-demand `uav` skill + references, the `uav-investigate` /
|
|
34
|
+
`uav-compare` / `uav-verify` subagents (spawned automatically), `/uav-*` commands, built-in
|
|
35
|
+
threshold / param-rule defaults (overridable per project via `.hydron/uav-thresholds.yaml` /
|
|
36
|
+
`param-rules.yaml`), bundled `apm-params.json`. **No dedicated UAV mode** — the domain rides
|
|
37
|
+
in the normal modes.
|
|
38
|
+
|
|
39
|
+
## Layout
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/
|
|
43
|
+
types.ts # LOCKED shared contracts (DecodedLog, Finding, Report, EpisodeDocument, Thresholds)
|
|
44
|
+
index.ts # plugin entry — registers uav_analyze, uav_query, uav_param_audit
|
|
45
|
+
decode/ # FMT-driven .bin decoder (TS port of split_dataflash_log.py) + tests
|
|
46
|
+
harness/
|
|
47
|
+
analyze.ts # the per-flight pipeline orchestrator
|
|
48
|
+
loadtables.ts # DecodedLog -> in-memory SQLite
|
|
49
|
+
checks.ts # the 21 system checks + param audit -> Finding[]
|
|
50
|
+
thresholds.ts # uav-thresholds.yaml loader (+ built-in v1 defaults)
|
|
51
|
+
extract.ts # DecodedLog -> entity/episode/params/events/measurements
|
|
52
|
+
store/
|
|
53
|
+
store.ts # episode JSON + index.jsonl (system of record)
|
|
54
|
+
diff.ts # keyed param/finding diff
|
|
55
|
+
script/analyze.ts # standalone smoke harness (no hydron runtime needed)
|
|
56
|
+
agents/ # subagents (uav-investigate / uav-compare / uav-verify)
|
|
57
|
+
commands/ # /uav-* slash commands
|
|
58
|
+
skills/uav/ # on-demand `uav` skill + references
|
|
59
|
+
data/ # bundled apm-params.json + built-in defaults
|
|
60
|
+
bridge/ # optional MAVLink→MCP Python bridge
|
|
61
|
+
plugins/ # BUILT bundle (index.js + worker/) — generated
|
|
62
|
+
mcp.json # exa + mavlink MCP servers (disabled by default)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Run standalone
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bun install
|
|
69
|
+
bun run script/analyze.ts "/path/to/flight.bin" --project /path/to/workdir
|
|
70
|
+
bun test # 29 tests: decoder vs oracle, checks, store/diff
|
|
71
|
+
bun run typecheck
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Knowledge grounding (the retrieval ladder)
|
|
75
|
+
|
|
76
|
+
Verdicts are always deterministic (harness vs bound). Sourcing a bound's value + citation
|
|
77
|
+
walks a ladder, first hit wins, never invents:
|
|
78
|
+
|
|
79
|
+
1. **`uav-thresholds.yaml`** — tuned / customer-stated bounds (local, authoritative).
|
|
80
|
+
2. **`query_hydron_project`** — ingested OEM datasheets in the project KG/RAG (cite citation).
|
|
81
|
+
3. **Exa.ai web MCP** (`exa_web_search_exa` / `exa_web_fetch_exa`) — datasheets / rules-of-thumb
|
|
82
|
+
**not in the KG**; cite the URL. Pluggable: swap for another search MCP. These tools are
|
|
83
|
+
available in the normal modes; the UAV subagents allow-list them so they don't prompt per call.
|
|
84
|
+
|
|
85
|
+
## Verified sample result (`17-03-2026 Sortie 4.bin`)
|
|
86
|
+
|
|
87
|
+
ArduCopter V4.6.3, ~48 min, 1306 params. **battery CRITICAL** (2× ERR 6/1 failsafes),
|
|
88
|
+
**vibration HIGH** (VibeZ 40.4 > 30), **hover HIGH** (avg ThH 0.17 < 0.25 band); attitude/
|
|
89
|
+
altitude/rcou/rcin/mag/ekf clean. Decode ~0.4 s.
|
|
90
|
+
|
|
91
|
+
## RAM / large-file scaling (P0 — design-partner blocker)
|
|
92
|
+
|
|
93
|
+
> **AirBound requires 1.5–2 GB files and ~4-flight compares; we'd only tested to ~60 MB,
|
|
94
|
+
> and big files froze the machine.** This is the top priority. See
|
|
95
|
+
> `internal-docs/.../uav/11-airbound-scale-and-deep-integration.md`.
|
|
96
|
+
|
|
97
|
+
> **✅ Landed (2026-06-29) — P-scale steps 1–5 (streaming decode → on-disk SQLite).**
|
|
98
|
+
> `analyzeFlight`/`uav_query` now stream `.bin` rows straight into a throwaway **on-disk**
|
|
99
|
+
> SQLite (`harness/db.ts` `openAnalysisDb`/`createSqliteSink`; `decode/dataflash.ts`
|
|
100
|
+
> `decodeDataflashInto`; `harness/load.ts` `decodeToDb`), retaining only the low-rate
|
|
101
|
+
> tables `extract.ts`/`vehicle.ts` read in JS (`RETAIN_TABLES = PARM/MSG/GPS/ERR/MODE`);
|
|
102
|
+
> high-rate aggregates moved to SQL in `extract.ts`. The 24 checks were already SQL-only.
|
|
103
|
+
> Verified by `src/harness/streaming.test.ts` (synthetic `.bin` parity/concurrency/cleanup +
|
|
104
|
+
> a real-bin smoke). **Synthetic benchmark (115 MB / 5M-row log):** peak RSS **781 MB → 282 MB
|
|
105
|
+
> (~2.8×)** — the eliminated part is the JS row graph (the term that scales linearly and
|
|
106
|
+
> freezes at GB scale).
|
|
107
|
+
>
|
|
108
|
+
> **✅ P1b landed — chunked file read.** `decode/dataflash.ts` `decodeCore` now reads the file
|
|
109
|
+
> in 4 MiB windows and carries cross-boundary messages, instead of `arrayBuffer()`-ing the whole
|
|
110
|
+
> file. **Peak RSS is now independent of file size.**
|
|
111
|
+
>
|
|
112
|
+
> **Real benchmark (the 1.69 GB golden `2026-04-07 15-20-25.bin`):** streaming `analyze`
|
|
113
|
+
> completes at **~332 MB peak RSS in ~79 s** (down from ~1.08 GB pre-P1b; the old full path
|
|
114
|
+
> would be 10 GB+ and freeze). Retained JS rows tiny (PARM 1397 / MSG 117 / ERR 2). **AirBound's
|
|
115
|
+
> 1.5 GB gate is met, RSS flat in file size.** `loadTables` (`:memory:`) and `decodeAuto` are
|
|
116
|
+
> unchanged for `uav_tune`/`export`/`plot`.
|
|
117
|
+
>
|
|
118
|
+
> **Test hygiene:** the golden flight log lives outside the repo and changes; `src/test-fixture.ts`
|
|
119
|
+
> centralizes it. Full-decode / pymavlink / CSV-oracle tests `skipIf(!goldenFullDecodeSafe())`
|
|
120
|
+
> (skip when the golden is absent or >500 MB — they'd OOM on a GB log); the memory-safe
|
|
121
|
+
> streaming path is exercised on the real golden via `hasGolden()`. Suite: **172 pass / 16 skip
|
|
122
|
+
> / 0 fail**.
|
|
123
|
+
|
|
124
|
+
## CPU: off-thread decode + duty-cycle throttle — ✅ landed
|
|
125
|
+
|
|
126
|
+
The streaming decode trades memory for CPU (millions of batched inserts + parse of tens of
|
|
127
|
+
millions of messages on one thread). Two **cross-platform** controls (no `nice`/OS-priority):
|
|
128
|
+
- **Off the main thread (#3):** `analyze`/`uav_query` run the decode in a **Worker**
|
|
129
|
+
(`harness/worker-entry.ts` + `worker-client.ts`), so Hydron's main loop stays responsive and
|
|
130
|
+
the OS schedules the work on another core. **The full on-disk SQLite is still built inside the
|
|
131
|
+
worker** — later `uav_query`/root-cause keep full row specificity. If a Worker can't run, it
|
|
132
|
+
**falls back to in-thread** (the feature never breaks).
|
|
133
|
+
- **CPU ceiling (#4):** set `UAV_DECODE_MAX_CPU` (e.g. `0.6` ≈ 60% of a core) to duty-cycle the
|
|
134
|
+
decode (`harness/throttle.ts`, applied per-chunk in `decodeCore`). Unset = full speed. Trades
|
|
135
|
+
wall-clock for a CPU cap — cross-platform (timer-based). Verified in
|
|
136
|
+
`src/harness/throttle-worker.test.ts` (strict worker path matches in-thread; throttle idles).
|
|
137
|
+
|
|
138
|
+
## Multi-flight compare at GB scale (P-multicompare) — ✅ landed
|
|
139
|
+
|
|
140
|
+
`uav_compare` compares **2+ already-analyzed flights at once** — finding-severity trend, key-
|
|
141
|
+
metric trend (vibe_peak, hover_thr, …), and parameter drift across the runs. It diffs the small
|
|
142
|
+
stored per-flight **EpisodeDocuments** (≈100 KB each), never the raw logs, so **comparing four
|
|
143
|
+
1 GB flights costs ~four 100 KB docs, not 4 GB** (analyze each first — each streams at ~flat
|
|
144
|
+
RAM). New: `store/diff.ts` `diffEpisodesN` (N-way), `store/store.ts` `readEpisodeById`,
|
|
145
|
+
`harness/compare.ts` `compareFlights`, and the `uav_compare` tool. Tested in
|
|
146
|
+
`src/harness/compare.test.ts`.
|
|
147
|
+
|
|
148
|
+
Root cause (in this code): `uav_analyze` holds **three concurrent copies** of the log —
|
|
149
|
+
the whole file buffer, **every row of every message type as boxed JS arrays** in
|
|
150
|
+
`log.tables` (`decode/dataflash.ts`), and a full copy again in an **`:memory:`** SQLite
|
|
151
|
+
(`harness/loadtables.ts`). The JS row graph dominates and OOMs on big logs. The 24 checks
|
|
152
|
+
already run as **pure SQL aggregates** over the DB, so the fix is structural:
|
|
153
|
+
|
|
154
|
+
- **P0:** `loadtables.ts` → on-disk throwaway SQLite (not `:memory:`) + `journal_mode=OFF` /
|
|
155
|
+
`synchronous=OFF` / bounded `cache_size`. (Partial — removes one copy.)
|
|
156
|
+
- **P1 (the real fix):** stream decode rows directly into the on-disk DB in batches; retain
|
|
157
|
+
in JS only the low-rate tables `extract.ts`/`vehicle.ts` iterate (PARM/MSG/MODE/ERR/GPS);
|
|
158
|
+
move `extract.ts`'s duration/vibe_peak/hover aggregates to SQL. High-rate tables
|
|
159
|
+
(IMU/RATE/ATT/VIBE/BAT/CTUN) then live only on disk; RAM is bounded by `cache_size`.
|
|
160
|
+
- **P1b:** read the file in 1 MB chunks (reuse the `contentHash` sliding-buffer) instead of
|
|
161
|
+
`arrayBuffer()`; **P2:** size-based "large-file mode", per-table row caps with `partial`
|
|
162
|
+
flags, optional child-process decode so a blowup can't freeze the host.
|
|
163
|
+
- **Multi-file:** the streaming/on-disk design must hold for **4 concurrent flights**
|
|
164
|
+
(`uav-compare`), each its own on-disk DB, diffed via SQL joins.
|
|
165
|
+
|
|
166
|
+
## Known limitations / next
|
|
167
|
+
|
|
168
|
+
- **Multidisciplinary root cause (P1, AirBound litmus test)** — beyond the current metric
|
|
169
|
+
checks, rank + eliminate competing hypotheses (aero × firmware × electronics) and cite
|
|
170
|
+
each; first demo target: **improperly tuned pitch-gain filter** (RATE oscillation ×
|
|
171
|
+
VIBE/FFT × `ATC_RAT_PIT_*`/`INS_GYRO_FILTER` params).
|
|
172
|
+
- **Pluggable diverse ingest (P1)** — weather-station CSV, custom params, external sensor
|
|
173
|
+
logs, time-aligned with the flight (self-registering handler layer).
|
|
174
|
+
- **Deep UAV-tool integration (the moat)** — SITL replay to confirm a hypothesis, deeper
|
|
175
|
+
pymavlink/MAVProxy + param-metadata + firmware code-context grounding.
|
|
176
|
+
- **Correlation/trend tier** (oscillation FFT, compass-motor interference, EKF drift over
|
|
177
|
+
time) and **param airframe-profile diff** (`auditAgainstProfile` stub) are not yet built.
|
|
178
|
+
- **PX4 `.ulog`** isn't wired yet; the pymavlink path is tested on the ArduPilot `.bin`.
|
|
179
|
+
- Live `uav_watch`/`uav_stream` and the `uav-diagnose` orchestrator (core-gated) are future.
|
|
180
|
+
- `uav-thresholds.yaml` overlay parsing is minimal (built-in v1 defaults are authoritative
|
|
181
|
+
until the YAML parse is fleshed out).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Install (plugin system)
|
|
186
|
+
|
|
187
|
+
The pack is now a proper **Hydron plugin bundle**. Install it with the `hydron plugin` CLI:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Global (available in every project on this machine):
|
|
191
|
+
hydron plugin add @h2loop/uav
|
|
192
|
+
|
|
193
|
+
# Project-scoped (committed to repo, shared with the team):
|
|
194
|
+
hydron plugin add @h2loop/uav --project
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`hydron plugin add` validates the bundle, then COPIES it into the plugins root (global →
|
|
198
|
+
`~/.config/hydron-cli/plugins/uav`; `--project` → `<repo>/.hydron/plugins/uav`). A local-path
|
|
199
|
+
install (`hydron plugin add /path/to/uav-pack [--project]`) is also supported for dev. Once
|
|
200
|
+
installed, agents, commands, skills, tools, MCP servers, and the ambient domain context are
|
|
201
|
+
all discovered and loaded automatically — no manual config wiring needed.
|
|
202
|
+
|
|
203
|
+
**Scope & persistence.** The install persists across sessions — install once, it's picked up
|
|
204
|
+
at every future startup. Global = "this machine does UAV work" (available in every repo);
|
|
205
|
+
`--project` = "this repo is UAV work" (committed with the repo, shared with the team). A
|
|
206
|
+
project's local `uav-thresholds.yaml` still overrides the global one.
|
|
207
|
+
|
|
208
|
+
Uninstall with `hydron plugin remove uav` (add `--project` for the project-scoped install).
|
|
209
|
+
Other commands: `hydron plugin list` (installed plugins + status), `hydron plugin enable
|
|
210
|
+
<name>` / `disable <name>`, `hydron plugin update <name>`, `hydron plugin validate <path>`
|
|
211
|
+
(static analysis before installing).
|
|
212
|
+
|
|
213
|
+
After install or uninstall, **fully quit and relaunch Hydron** — the plugin loads once per
|
|
214
|
+
process (a window reload isn't enough).
|
|
215
|
+
|
|
216
|
+
**UAV is a domain LAYER, not a mode.** Every mode (Ask / Architect / Code / Debug) becomes
|
|
217
|
+
UAV-aware via a short always-on signpost (the `experimental.chat.system.transform` hook) plus
|
|
218
|
+
the on-demand `uav` skill. There is **no dedicated UAV agent or mode**; the `uav-investigate`,
|
|
219
|
+
`uav-compare`, and `uav-verify` subagents handle isolated multi-step loops and are spawned
|
|
220
|
+
automatically, not selected by the user.
|
|
221
|
+
|
|
222
|
+
## MCP prerequisites
|
|
223
|
+
|
|
224
|
+
The plugin ships two MCP servers (both **disabled by default**). Enable them per-project in
|
|
225
|
+
your Hydron config:
|
|
226
|
+
|
|
227
|
+
### Exa (web grounding)
|
|
228
|
+
|
|
229
|
+
Provides `exa_web_search_exa` / `exa_web_fetch_exa` for OEM datasheet limits not in the
|
|
230
|
+
project KG.
|
|
231
|
+
|
|
232
|
+
**Requires:** `EXA_API_KEY` environment variable (get one at https://exa.ai).
|
|
233
|
+
|
|
234
|
+
### MAVLink bridge (live link)
|
|
235
|
+
|
|
236
|
+
A local Python stdio server. Read-only tools: `read_params`, `param_get`, `param_get_all`,
|
|
237
|
+
`get_messages`, `tail_telemetry`, `get_flight_mode`, `get_status`. The write tier
|
|
238
|
+
(`param_set` / `param_set_batch`) is **off by default** — see below.
|
|
239
|
+
|
|
240
|
+
**Requires:**
|
|
241
|
+
- Python ≥ 3.10 with `pymavlink>=2.4.49` and `mcp[cli]>=1.0.0` installed. The pack does **not**
|
|
242
|
+
auto-provision a venv — install the deps yourself: `pip install -r bridge/requirements.txt`
|
|
243
|
+
(ideally into a venv), and point the command's `python3` at that interpreter.
|
|
244
|
+
- A reachable MAVLink endpoint (e.g. `udp:127.0.0.1:14550` from SITL or a telemetry radio),
|
|
245
|
+
or a `.tlog`/`.log` file for offline replay.
|
|
246
|
+
- The `bridge/` directory ships with the plugin (the `mcp.json` references it).
|
|
247
|
+
|
|
248
|
+
Enable per-project in your Hydron config. **Param writes are OFF by default** (the bridge
|
|
249
|
+
defaults to `--read-only`). To enable the tune→push→retest write tier, the command must pass
|
|
250
|
+
**both** `--no-read-only` **and** `--allow-param-write`. ⚠ this lets the agent write params to
|
|
251
|
+
a **live vehicle** — prefer SITL/bench.
|
|
252
|
+
```jsonc
|
|
253
|
+
"mcp": {
|
|
254
|
+
"mavlink": {
|
|
255
|
+
"enabled": true,
|
|
256
|
+
"command": ["python3", "<plugin-path>/bridge/uav_mavlink_bridge.py",
|
|
257
|
+
"--connect", "udp:127.0.0.1:14550",
|
|
258
|
+
"--no-read-only", "--allow-param-write"]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Connect-target forms: `udp:HOST:PORT` (default `udp:127.0.0.1:14550` = SITL/MAVProxy) |
|
|
264
|
+
`tcp:HOST:PORT` | `serial:/dev/ttyUSB0:BAUD` | `/path/log.tlog` (offline replay); can also be
|
|
265
|
+
set via the `MAVLINK_CONNECT` env on the server. **"Connected" ≠ a live vehicle:** the panel
|
|
266
|
+
shows Connected once the bridge process starts, but tools only return data while something is
|
|
267
|
+
actually publishing MAVLink at the connect target; otherwise calls time out (`-32001`). Plain
|
|
268
|
+
`.bin` log analysis via `uav_analyze` does **not** need the bridge.
|
|
269
|
+
|
|
270
|
+
## Known limitations
|
|
271
|
+
|
|
272
|
+
1. **Loader collision (host bug) — FIXED.** Hydron's plugin dedup used to key on the hook
|
|
273
|
+
module's basename (`index`), so two plugins each shipping a `plugins/index.ts` hook could
|
|
274
|
+
collide and one would be silently dropped. This is now resolved in hydron-cli:
|
|
275
|
+
`getPluginName` keys on the plugin install directory, not the basename, so multiple
|
|
276
|
+
hook-bearing plugins coexist.
|
|
277
|
+
|
|
278
|
+
2. **Local-path install copies everything:** `hydron plugin add ./path` does a raw `fs.cp`
|
|
279
|
+
with no `files`/`.npmignore` filtering. If you ran `bun install` or `bun run build` locally,
|
|
280
|
+
the copy includes `node_modules` + built artifacts + source. **Publish to npm and install via
|
|
281
|
+
spec** (`hydron plugin add @h2loop/uav`) for a clean install — npm respects `files`.
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @h2loop/uav — Install, Validate & Use
|
|
2
|
+
|
|
3
|
+
The UAV domain pack for Hydron: point it at an ArduPilot/PX4 flight log and get grounded
|
|
4
|
+
analysis, tuning suggestions, in-editor charts, and (optionally) a live MAVLink link. This guide
|
|
5
|
+
covers getting it running on **macOS, Linux, and Windows**, validating it works, and everyday use.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Prerequisites (all platforms)
|
|
10
|
+
|
|
11
|
+
- **Hydron** installed (the host — CLI and/or VS Code extension). Required.
|
|
12
|
+
- **Bun** — *only* needed to build the bundle from source or run the pack's tests; **not**
|
|
13
|
+
needed to use a published pack.
|
|
14
|
+
- **Python ≥ 3.10** — *only* for the optional live MAVLink bridge (`pymavlink>=2.4.49` +
|
|
15
|
+
`mcp[cli]>=1.0.0`). The pack does **not** auto-provision a venv; you install the deps
|
|
16
|
+
yourself (see §6). Plain `.bin` log analysis needs no Python.
|
|
17
|
+
- *Optional:* `export EXA_API_KEY=…` for web grounding of bounds.
|
|
18
|
+
- macOS/Linux are native; on **Windows** use **WSL2** for the bridge's Python (the bridge is
|
|
19
|
+
POSIX-oriented).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Install per OS
|
|
24
|
+
|
|
25
|
+
Install with the **`hydron plugin`** CLI — a cross-platform command (no bash wrapper, no PATH
|
|
26
|
+
patching). `hydron plugin add` COPIES the whole bundle into the plugins root, so the source
|
|
27
|
+
location no longer matters after install.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Global — available in every project on this machine:
|
|
31
|
+
hydron plugin add @h2loop/uav
|
|
32
|
+
|
|
33
|
+
# Project-scoped — committed with the repo, shared with the team:
|
|
34
|
+
hydron plugin add @h2loop/uav --project
|
|
35
|
+
|
|
36
|
+
# Local path (dev, from a checkout):
|
|
37
|
+
hydron plugin add /path/to/uav-pack # add --project to scope it to a repo
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### macOS / Linux
|
|
41
|
+
Native. Nothing extra needed to use a published pack. Python ≥ 3.10 is only required for the
|
|
42
|
+
optional MAVLink bridge (§6).
|
|
43
|
+
|
|
44
|
+
### Windows — use WSL2 for the bridge
|
|
45
|
+
The `hydron plugin add` command itself is cross-platform. The MAVLink bridge, however, is
|
|
46
|
+
POSIX-oriented Python, so run it (and ideally Hydron) inside **WSL2** (Ubuntu):
|
|
47
|
+
```powershell
|
|
48
|
+
wsl --install -d Ubuntu # PowerShell (admin), one-time; then open the Ubuntu shell
|
|
49
|
+
```
|
|
50
|
+
Use the **VS Code "WSL" remote** (or Hydron's CLI inside WSL) so Hydron and the bridge share
|
|
51
|
+
the same environment.
|
|
52
|
+
|
|
53
|
+
### Uninstall (any OS)
|
|
54
|
+
```bash
|
|
55
|
+
hydron plugin remove uav # add --project for the project-scoped install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After install (or uninstall), **fully quit + relaunch Hydron** — the plugin loads once at
|
|
59
|
+
process start (a window reload isn't enough).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Validate it works
|
|
64
|
+
|
|
65
|
+
### Install checks (plugin CLI)
|
|
66
|
+
| Check | Command | Expect |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Installed & enabled | `hydron plugin list` | `uav` listed as enabled |
|
|
69
|
+
| Bundle valid | `hydron plugin validate /path/to/uav-pack` | passes static analysis (before installing) |
|
|
70
|
+
| Removed | `hydron plugin remove uav` | `uav` no longer in `hydron plugin list` |
|
|
71
|
+
|
|
72
|
+
### Behavior (in a Hydron session, after restart)
|
|
73
|
+
| # | Test | Prompt / action | Expect |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| 1 | Tools load | "what UAV tools do you have?" | `uav_analyze, uav_plot, uav_tune, uav_finding, uav_query, uav_python, …` |
|
|
76
|
+
| 2 | Analyze + vehicle type | "analyze `/path/flight.bin`" | ranked anomaly report; detects multirotor / fixed-wing / VTOL |
|
|
77
|
+
| 3 | Graph labeling | "plot VibeZ over time" | chart **plus** a note: *VIBE = airframe vibration at the IMU, not per-motor* |
|
|
78
|
+
| 4 | Tuning | "run uav_tune on this flight" | `INS_HNTCH_*` notch params (+ `INS_HNTC2_*` if a distinct 2nd peak) |
|
|
79
|
+
| 5 | Per-airframe bounds | drop `<project>/.hydron/uav-thresholds.yaml` with `vibration: { max_ms2: 60 }`, re-analyze | vibration judged against 60, not the default 30 |
|
|
80
|
+
|
|
81
|
+
Automated tests: `bun test`. Real-log tests run only when `UAV_GOLDEN_BIN=/path/to.bin` is set.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4. Everyday use
|
|
86
|
+
|
|
87
|
+
Point Hydron at a `.bin`/`.tlog`/`.ulog` and talk to it in any mode — the UAV layer rides along.
|
|
88
|
+
|
|
89
|
+
- **Triage:** "analyze `/path/flight.bin` and rank the anomalies."
|
|
90
|
+
- **Visualize:** "plot desired-vs-actual roll tracking error"; "FFT the gyro"; "x=RCOU.C1 y=VIBE.VibeZ".
|
|
91
|
+
- **Investigate (parallel fan-out):** "what caused the EKF spike at 800s, walk the altitude excursion, and cross-check the IMUs."
|
|
92
|
+
- **Tune:** "`/uav-tune /path/flight.bin`" → notch + mode recommendation with valid ranges.
|
|
93
|
+
- **Save a conclusion:** "save that as a finding" (or `/uav-save-finding`).
|
|
94
|
+
- **Compare / batch:** "`/uav-compare flightA flightB`"; "`/uav-batch-report /path/to/logs`".
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 5. Per-lab config (pluggable, no plugin change)
|
|
99
|
+
|
|
100
|
+
Drop these into your project's `.hydron/` — the pack ships generic defaults; your specifics override:
|
|
101
|
+
|
|
102
|
+
- **`uav-thresholds.yaml`** — your airframe's bounds. Deep-merges over defaults, so list only the
|
|
103
|
+
keys you change (e.g. `vibration: { max_ms2: 60 }`). Name your profile with `ruleset_version`.
|
|
104
|
+
- **`uav-format-converters.json`** — register a converter for a non-standard log format.
|
|
105
|
+
|
|
106
|
+
See `skills/uav/references/project-config.md` for the full list (`uav-thresholds.yaml`,
|
|
107
|
+
`param-rules.yaml`, `apm-params.json`, `uav-format-converters.json`) and merge semantics.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 6. Live MAVLink bridge (the `mavlink` MCP)
|
|
112
|
+
|
|
113
|
+
A **MAVLink→MCP bridge**: a local Python stdio server Hydron spawns so the agent can query a
|
|
114
|
+
live vehicle / SITL / log replay. Read-only tools: `get_status`, `get_flight_mode`,
|
|
115
|
+
`read_params`, `param_get`, `param_get_all`, `get_messages`, `tail_telemetry`. The **write**
|
|
116
|
+
tools `param_set`/`param_set_batch` are **off by default** (see the ⚠ note below).
|
|
117
|
+
|
|
118
|
+
### Install the Python deps yourself
|
|
119
|
+
The pack does **not** auto-provision a venv. The bridge needs Python ≥ 3.10 with
|
|
120
|
+
`pymavlink>=2.4.49` and `mcp[cli]>=1.0.0`:
|
|
121
|
+
```bash
|
|
122
|
+
pip install -r bridge/requirements.txt # ideally into a venv
|
|
123
|
+
```
|
|
124
|
+
Then point the `mavlink` command's `python3` at that interpreter.
|
|
125
|
+
|
|
126
|
+
### Enable and configure (per-project in opencode.jsonc)
|
|
127
|
+
The `mavlink` server ships in `mcp.json` **disabled by default**. Enable it per-project:
|
|
128
|
+
```jsonc
|
|
129
|
+
"mcp": {
|
|
130
|
+
"mavlink": {
|
|
131
|
+
"enabled": true,
|
|
132
|
+
"command": ["python3", "<plugin-path>/bridge/uav_mavlink_bridge.py",
|
|
133
|
+
"--connect", "udp:127.0.0.1:14550"]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
Connect-target forms: `udp:HOST:PORT` (default `udp:127.0.0.1:14550` = SITL/MAVProxy) |
|
|
138
|
+
`tcp:HOST:PORT` | `serial:/dev/ttyUSB0:BAUD` | `/path/log.tlog` (offline replay). Can also be
|
|
139
|
+
set via the `MAVLINK_CONNECT` env on the server.
|
|
140
|
+
|
|
141
|
+
> **"Connected" ≠ a live vehicle.** The panel shows `mavlink Connected` once the bridge *process*
|
|
142
|
+
> starts. The tools only return data while something is **actually publishing MAVLink** at the
|
|
143
|
+
> connect target. With no source, calls return empty / time out (`-32001`). Start your SITL/vehicle
|
|
144
|
+
> (or leave it — log analysis via `uav_analyze` doesn't need the live bridge).
|
|
145
|
+
|
|
146
|
+
> **Param writes are OFF by default.** The bridge defaults to `--read-only`. To enable the
|
|
147
|
+
> tune→push→retest write tier (`param_set` / `param_set_batch`), the command must pass **both**
|
|
148
|
+
> `--no-read-only` **and** `--allow-param-write`:
|
|
149
|
+
> ```jsonc
|
|
150
|
+
> "command": ["python3", "<plugin-path>/bridge/uav_mavlink_bridge.py",
|
|
151
|
+
> "--connect", "udp:127.0.0.1:14550",
|
|
152
|
+
> "--no-read-only", "--allow-param-write"]
|
|
153
|
+
> ```
|
|
154
|
+
> ⚠ this lets the agent **write parameters to a live vehicle** — prefer SITL/bench, and point
|
|
155
|
+
> `--connect` at a real airframe only when you intend that.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 7. Troubleshooting
|
|
160
|
+
|
|
161
|
+
- **UAV tools don't appear** → you didn't *fully* restart Hydron (a window reload isn't enough); the
|
|
162
|
+
plugin loads once per process. Confirm it's installed with `hydron plugin list`. On Windows make
|
|
163
|
+
sure Hydron is running in the **same WSL env** you're using.
|
|
164
|
+
- **`mavlink` shows Disabled** → the `python3` in the `mavlink` command lacks `pymavlink`+`mcp`, or
|
|
165
|
+
it isn't Python ≥ 3.10. Install the deps (`pip install -r bridge/requirements.txt`, ideally into a
|
|
166
|
+
venv) and point the command at that interpreter.
|
|
167
|
+
- **Every mavlink tool → `MCP error -32001: Request timed out`** → the bridge is Connected but **no
|
|
168
|
+
MAVLink source is live** at the connect target. Start SITL / point `--connect` at a live link.
|
|
169
|
+
- **`MCP error -32000: Connection closed`** → the bridge crashed at startup: usually the wrong Python
|
|
170
|
+
(no deps — see Disabled above) or a **port clash** (only one Hydron instance can bind the bridge's
|
|
171
|
+
UDP port at a time — don't run two).
|
|
172
|
+
- **See also:** `PACKAGING.md`, `CHANGELOG.md`, `bridge/README.md`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
mode: subagent
|
|
3
|
+
displayName: UAV Compare
|
|
4
|
+
description: >-
|
|
5
|
+
Cross-flight regression for ONE airframe over its stored history (not just a pair).
|
|
6
|
+
Loops over the airframe's episodes, diffs consecutive flights, and returns only the
|
|
7
|
+
narrated trend — the multi-flight decode, the per-table SQL, and the per-step diffs
|
|
8
|
+
stay in this subagent. This is a genuine loop (many flights, many diffs); that is why
|
|
9
|
+
it is a subagent rather than a one-shot tool call.
|
|
10
|
+
permission:
|
|
11
|
+
# deny-all FIRST, then allow specifics: PermissionNext resolves with findLast (last
|
|
12
|
+
# matching rule wins), so "*": deny MUST precede the allows or it overrides them all.
|
|
13
|
+
"*": deny
|
|
14
|
+
bash: allow
|
|
15
|
+
"uav_analyze": allow
|
|
16
|
+
"uav_query": allow
|
|
17
|
+
skill: { "*": deny, "uav": allow }
|
|
18
|
+
---
|
|
19
|
+
You are a UAV compare subagent. You produce a cross-flight regression for one airframe over
|
|
20
|
+
its history, keeping the multi-flight decode and the per-table SQL out of the primary window.
|
|
21
|
+
|
|
22
|
+
Do this:
|
|
23
|
+
1. Determine the airframe's flight set. If given an explicit pair, use it; otherwise load the
|
|
24
|
+
airframe's stored episodes in order (`uav_query` over the local store) and walk the history.
|
|
25
|
+
2. For each consecutive pair, fetch the server-side param + finding diff. Accumulate the
|
|
26
|
+
trend — a param that drifts the same direction across several flights matters more than a
|
|
27
|
+
one-flight blip.
|
|
28
|
+
3. Return ONLY a narrated regression: the params and findings that shifted, the direction and
|
|
29
|
+
magnitude of each shift (and whether it is a one-flight jump or a sustained trend), and
|
|
30
|
+
which guardrail each relates to — citing the bound and its source. Ground every shift
|
|
31
|
+
against an absolute bound, not just "moved a lot relative to last flight".
|
|
32
|
+
|
|
33
|
+
In the tuning phase the reference is the *last* flight (track pattern shifts); for a
|
|
34
|
+
production vehicle the reference is the *fixed* guardrail. Rank the shifts by
|
|
35
|
+
significance and surface them — do not close the verdict. Leave that to the engineer.
|
|
36
|
+
See the `uav` skill's compare reference for how to narrate the diff.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
mode: subagent
|
|
3
|
+
displayName: UAV Investigate
|
|
4
|
+
description: >-
|
|
5
|
+
Drill into ONE open-ended question about a flight (e.g. "what caused the EKF jump at
|
|
6
|
+
~800s?") and return a single grounded brief. The multi-step loop — analyze, repeated
|
|
7
|
+
uav_query, uav_python, web-grounding — and all its intermediate output stay in this
|
|
8
|
+
subagent; only the ranked conclusion comes back. Use for investigations, NOT for a
|
|
9
|
+
one-shot uav_analyze (call that tool directly from the parent — it already isolates the
|
|
10
|
+
decode and per-table SQL).
|
|
11
|
+
permission:
|
|
12
|
+
# deny-all FIRST, then allow specifics: PermissionNext resolves with findLast (last
|
|
13
|
+
# matching rule wins), so "*": deny MUST precede the allows or it overrides them all.
|
|
14
|
+
"*": deny
|
|
15
|
+
bash: allow
|
|
16
|
+
read: allow
|
|
17
|
+
"uav_analyze": allow
|
|
18
|
+
"uav_query": allow
|
|
19
|
+
"uav_python": allow
|
|
20
|
+
"uav_param_audit": allow
|
|
21
|
+
"uav_finding": allow
|
|
22
|
+
"exa_web_search_exa": allow
|
|
23
|
+
"exa_web_fetch_exa": allow
|
|
24
|
+
"query_hydron_project": allow
|
|
25
|
+
skill: { "*": deny, "uav": allow }
|
|
26
|
+
---
|
|
27
|
+
You are a UAV investigation subagent. You exist because a real investigation is a LOOP — it
|
|
28
|
+
takes many tool calls and produces a lot of intermediate output (SQL rows, Python summaries,
|
|
29
|
+
dead ends). All of that stays in your window; only your final brief returns to the parent.
|
|
30
|
+
|
|
31
|
+
If the task is a single one-shot decode-and-check, you are the wrong tool — say so; the parent
|
|
32
|
+
should call `uav_analyze` directly (the tool already keeps the decode and per-table SQL out of
|
|
33
|
+
context). You earn your keep only on open-ended questions.
|
|
34
|
+
|
|
35
|
+
How to investigate (reason, don't jump to a verdict):
|
|
36
|
+
1. Establish the baseline with `uav_analyze` if not already done — what fired, what didn't.
|
|
37
|
+
2. Form a hypothesis, then TEST it with evidence: `uav_query` for the relevant rows,
|
|
38
|
+
`uav_python` (numpy/pandas) for aggregates, correlations, per-window stats, spectra. Quote
|
|
39
|
+
the exact window/timestamp your numbers come from; do not eyeball.
|
|
40
|
+
3. Ground every claim against an ABSOLUTE bound, never relative-to-peers. "X is 6× its
|
|
41
|
+
neighbour" is NOT a fault unless X also clears its absolute guardrail (uav-thresholds.yaml,
|
|
42
|
+
then query_hydron_project, then a cited web source). If you can only show a relative
|
|
43
|
+
difference, say "within absolute tolerance — relative only" and do not flag it.
|
|
44
|
+
4. Iterate until the chain holds or the evidence runs out. State what you could NOT confirm.
|
|
45
|
+
|
|
46
|
+
Persisting a conclusion is a REQUIRED final step — for EACH finding that cleared an absolute
|
|
47
|
+
bound in step 3, actually call the tool; do not just describe it in your brief:
|
|
48
|
+
- FIRST `uav_finding op:"list" flight_id:<id>` — read what is already stored (deterministic +
|
|
49
|
+
earlier agent findings).
|
|
50
|
+
- DECIDE whether your finding is already represented — same root cause counts, even if worded
|
|
51
|
+
differently or covered by a deterministic check (e.g. don't add "IMU2 disagreement" when
|
|
52
|
+
`imu_consistency` already reports it). This is a judgment, not a string match.
|
|
53
|
+
- Only if it is genuinely new, `uav_finding op:"add"` with a clear title, severity, the measured
|
|
54
|
+
value, the absolute threshold_used, and a detail citing the bound + its source. Pass
|
|
55
|
+
supersedes_id to replace a stale one. Never add a finding you could only support relative-to-peers.
|
|
56
|
+
|
|
57
|
+
Return: the ranked findings (most significant first), each with its measured value, the time
|
|
58
|
+
window it came from, the absolute bound it breached and that bound's source, which ones you
|
|
59
|
+
persisted (and which you skipped as already-stored), and a one-paragraph synthesis. No raw row
|
|
60
|
+
dumps, no full FFT bins. Surface and rank; leave the final verdict to the engineer.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
mode: subagent
|
|
3
|
+
displayName: UAV Verify
|
|
4
|
+
description: >-
|
|
5
|
+
Adversarial check on ONE proposed flag. Given a claimed anomaly, decide whether it clears
|
|
6
|
+
an ABSOLUTE grounded bound or is merely relative-to-peers / eyeballed. Returns a verdict:
|
|
7
|
+
GROUNDED (cite the bound + source + margin) or RELATIVE-ONLY (not a fault). Use this before
|
|
8
|
+
reporting a flag the deterministic checks did NOT already produce.
|
|
9
|
+
permission:
|
|
10
|
+
# deny-all FIRST, then allow specifics: PermissionNext resolves with findLast (last
|
|
11
|
+
# matching rule wins), so "*": deny MUST precede the allows or it overrides them all.
|
|
12
|
+
"*": deny
|
|
13
|
+
bash: allow
|
|
14
|
+
read: allow
|
|
15
|
+
"uav_query": allow
|
|
16
|
+
"uav_python": allow
|
|
17
|
+
"query_hydron_project": allow
|
|
18
|
+
"exa_web_search_exa": allow
|
|
19
|
+
"exa_web_fetch_exa": allow
|
|
20
|
+
skill: { "*": deny, "uav": allow }
|
|
21
|
+
---
|
|
22
|
+
You are an adversarial verifier. Your DEFAULT is to refute the flag. Your only job: decide
|
|
23
|
+
whether a proposed anomaly is grounded in an absolute bound, or is an artifact of relative
|
|
24
|
+
comparison. You do not investigate broadly and you do not author new findings — you judge one.
|
|
25
|
+
|
|
26
|
+
You are given: a claimed flag (signal, measured value, the reasoning behind it).
|
|
27
|
+
|
|
28
|
+
Test it:
|
|
29
|
+
1. Is there an ABSOLUTE bound for this signal? Look in this order: uav-thresholds.yaml →
|
|
30
|
+
query_hydron_project (ingested datasheets) → a cited web source (ArduPilot health docs,
|
|
31
|
+
OEM datasheet). If no absolute bound exists, the flag is NOT grounded — say so.
|
|
32
|
+
2. Does the measured value actually breach that absolute bound? Recompute from the data with
|
|
33
|
+
`uav_query` / `uav_python` if needed; quote the window. "N× the other instances", "highest
|
|
34
|
+
of the three", "looks elevated" are RELATIVE signals and never, by themselves, a fault — an
|
|
35
|
+
IMU at 0.20 m/s² inter-instance spread is fine against a 0.7 m/s² bound even if it is 6× its
|
|
36
|
+
neighbours.
|
|
37
|
+
3. Check the bound applies to this vehicle type and regime before accepting it.
|
|
38
|
+
|
|
39
|
+
Return ONE verdict, nothing else:
|
|
40
|
+
- `GROUNDED` — measured value, the absolute bound it breaches, the bound's source, and the
|
|
41
|
+
margin by which it breaches.
|
|
42
|
+
- `RELATIVE-ONLY` — the absolute bound (or "none exists"), the value's margin INSIDE it, and a
|
|
43
|
+
one-line reason the original flag was relative/eyeballed.
|
|
44
|
+
Bias toward RELATIVE-ONLY when uncertain. A flag survives only if you can name the absolute
|
|
45
|
+
bound it breaks.
|