@h2loop/uav 1.2.0 → 1.2.1
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/README.md +17 -16
- package/USAGE.md +47 -30
- package/bridge/requirements.txt +5 -1
- package/mcp.json +1 -7
- package/package.json +1 -1
- package/plugins/index.js +26 -26
- package/skills/uav/SKILL.md +4 -1
- package/skills/uav/references/tuning.md +4 -2
package/README.md
CHANGED
|
@@ -59,7 +59,9 @@ skills/uav/ # on-demand `uav` skill + references
|
|
|
59
59
|
data/ # bundled apm-params.json + built-in defaults
|
|
60
60
|
bridge/ # optional MAVLink→MCP Python bridge
|
|
61
61
|
plugins/ # BUILT bundle (index.js + worker/) — generated
|
|
62
|
-
mcp.json # exa
|
|
62
|
+
mcp.json # exa MCP server (disabled by default); mavlink is registered
|
|
63
|
+
# from src/index.ts's config hook — it needs a resolved
|
|
64
|
+
# interpreter + an ABSOLUTE script path, which static JSON can't give
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
## Run standalone
|
|
@@ -235,30 +237,29 @@ project KG.
|
|
|
235
237
|
|
|
236
238
|
A local Python stdio server. Read-only tools: `read_params`, `param_get`, `param_get_all`,
|
|
237
239
|
`get_messages`, `tail_telemetry`, `get_flight_mode`, `get_status`. The write tier
|
|
238
|
-
(`param_set` / `param_set_batch`) is **
|
|
240
|
+
(`param_set` / `param_set_batch`) is **on by default** so tune→push→retest works out of the
|
|
241
|
+
box — see below.
|
|
239
242
|
|
|
240
243
|
**Requires:**
|
|
241
|
-
- Python ≥ 3.10 with `pymavlink>=2.4.49` and `mcp[cli]>=1.0.0` installed. The pack does **not**
|
|
244
|
+
- Python ≥ 3.10 with `pymavlink>=2.4.49` and `mcp[cli]>=1.0.0,<2` installed. The pack does **not**
|
|
242
245
|
auto-provision a venv — install the deps yourself: `pip install -r bridge/requirements.txt`
|
|
243
246
|
(ideally into a venv), and point the command's `python3` at that interpreter.
|
|
244
247
|
- A reachable MAVLink endpoint (e.g. `udp:127.0.0.1:14550` from SITL or a telemetry radio),
|
|
245
248
|
or a `.tlog`/`.log` file for offline replay.
|
|
246
249
|
- The `bridge/` directory ships with the plugin (the `mcp.json` references it).
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"--connect", "udp:127.0.0.1:14550",
|
|
258
|
-
"--no-read-only", "--allow-param-write"]
|
|
259
|
-
}
|
|
260
|
-
}
|
|
251
|
+
Ships **enabled**, with param writes **on** — the command passes `--no-read-only`
|
|
252
|
+
`--allow-param-write`, so `param_set` / `param_set_batch` are registered and the tuning loop
|
|
253
|
+
works with no configuration. ⚠ this lets the agent write params to a **live vehicle** — prefer
|
|
254
|
+
SITL/bench. To ship read-only, drop both flags.
|
|
255
|
+
|
|
256
|
+
No `--connect` is passed, so the target comes from the **`MAVLINK_CONNECT`** env var, falling
|
|
257
|
+
back to `udp:127.0.0.1:14550`:
|
|
258
|
+
```bash
|
|
259
|
+
export MAVLINK_CONNECT=serial:/dev/ttyUSB0:57600 # COM3:57600 on Windows
|
|
261
260
|
```
|
|
261
|
+
Don't put it in the server's `environment` block — Hydron spreads that *over* `process.env`, so
|
|
262
|
+
it would shadow your export instead of defaulting to it.
|
|
262
263
|
|
|
263
264
|
Connect-target forms: `udp:HOST:PORT` (default `udp:127.0.0.1:14550` = SITL/MAVProxy) |
|
|
264
265
|
`tcp:HOST:PORT` | `serial:/dev/ttyUSB0:BAUD` | `/path/log.tlog` (offline replay); can also be
|
package/USAGE.md
CHANGED
|
@@ -12,7 +12,7 @@ covers getting it running on **macOS, Linux, and Windows**, validating it works,
|
|
|
12
12
|
- **Bun** — *only* needed to build the bundle from source or run the pack's tests; **not**
|
|
13
13
|
needed to use a published pack.
|
|
14
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
|
|
15
|
+
`mcp[cli]>=1.0.0,<2`). The pack does **not** auto-provision a venv; you install the deps
|
|
16
16
|
yourself (see §6). Plain `.bin` log analysis needs no Python.
|
|
17
17
|
- *Optional:* `export EXA_API_KEY=…` for web grounding of bounds.
|
|
18
18
|
- **macOS, Linux, and Windows are all native** — no WSL required. The pack resolves the Python
|
|
@@ -127,50 +127,67 @@ See `skills/uav/references/project-config.md` for the full list (`uav-thresholds
|
|
|
127
127
|
A **MAVLink→MCP bridge**: a local Python stdio server Hydron spawns so the agent can query a
|
|
128
128
|
live vehicle / SITL / log replay. Read-only tools: `get_status`, `get_flight_mode`,
|
|
129
129
|
`read_params`, `param_get`, `param_get_all`, `get_messages`, `tail_telemetry`. The **write**
|
|
130
|
-
tools `param_set`/`param_set_batch` are **
|
|
130
|
+
tools `param_set`/`param_set_batch` are **on by default**, so tune→push→retest works out of
|
|
131
|
+
the box (see the ⚠ note below to turn them off).
|
|
131
132
|
|
|
132
|
-
### Install the Python deps
|
|
133
|
+
### Install the Python deps (the only manual step)
|
|
133
134
|
The pack does **not** auto-provision a venv. The bridge needs Python ≥ 3.10 with
|
|
134
|
-
`pymavlink>=2.4.49` and `mcp[cli]>=1.0.0`:
|
|
135
|
+
`pymavlink>=2.4.49` and `mcp[cli]>=1.0.0,<2`:
|
|
135
136
|
```bash
|
|
136
137
|
pip install -r bridge/requirements.txt # ideally into a venv
|
|
137
138
|
```
|
|
138
|
-
|
|
139
|
-
the
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
> The `<2` bound is required, not cosmetic: the MCP Python SDK **removed `mcp.server.fastmcp`
|
|
140
|
+
> in 2.x**, so the bridge fails at import on `mcp>=2`. Verified working on 1.29.0.
|
|
141
|
+
|
|
142
|
+
You do **not** need to edit any command. The plugin registers the `mavlink` server itself at
|
|
143
|
+
startup, resolving:
|
|
144
|
+
|
|
145
|
+
- **the interpreter** — via the same probe-by-execution resolver the `.tlog`/`.ulog` decoders
|
|
146
|
+
use, so `python3` / `python` / the Windows `py -3` launcher are all handled and Windows'
|
|
147
|
+
Microsoft-Store alias stubs are rejected. Pin a specific one with `UAV_PYTHON` (e.g. a venv:
|
|
148
|
+
`export UAV_PYTHON=/path/.venv/bin/python`, or `…\Scripts\python.exe` on Windows).
|
|
149
|
+
- **the script path** — absolute, inside the installed plugin bundle. This matters: Hydron
|
|
150
|
+
spawns local MCP servers with the cwd set to *your project*, so a bundle-relative path can
|
|
151
|
+
never resolve. That is why the server is no longer declared in `mcp.json`.
|
|
152
|
+
|
|
153
|
+
If no usable Python is found the server is not registered at all; if Python is found but can't
|
|
154
|
+
`import mcp, pymavlink`, it is registered **disabled** — so you see it listed rather than
|
|
155
|
+
getting a failed server on every launch. Check with `hydron mcp list`.
|
|
156
|
+
|
|
157
|
+
### Point it at your vehicle
|
|
158
|
+
The `mavlink` server is registered **enabled**, with param writes on, so nothing needs enabling.
|
|
159
|
+
The one thing you may need to change is the connect target.
|
|
160
|
+
|
|
161
|
+
The command passes **no** `--connect`, so the bridge falls back to its own default —
|
|
162
|
+
which reads the **`MAVLINK_CONNECT`** environment variable, then `udp:127.0.0.1:14550`. So for
|
|
163
|
+
anything other than local SITL/MAVProxy, just export the target:
|
|
164
|
+
```bash
|
|
165
|
+
export MAVLINK_CONNECT=serial:/dev/ttyUSB0:57600 # or COM3:57600 on Windows
|
|
153
166
|
```
|
|
154
167
|
Connect-target forms: `udp:HOST:PORT` (default `udp:127.0.0.1:14550` = SITL/MAVProxy) |
|
|
155
168
|
`tcp:HOST:PORT` | `serial:/dev/ttyUSB0:BAUD` (on Windows a COM port, e.g. `COM3` or
|
|
156
|
-
`COM3:57600`) | `/path/log.tlog` (offline replay).
|
|
157
|
-
|
|
169
|
+
`COM3:57600`) | `/path/log.tlog` (offline replay).
|
|
170
|
+
|
|
171
|
+
> Set it in the environment, **not** in the server's `environment` block: Hydron spawns local
|
|
172
|
+
> MCP servers with `{...process.env, ...environment}`, so a server-level entry *shadows* your
|
|
173
|
+
> export rather than acting as a default. That is why the plugin sets neither.
|
|
158
174
|
|
|
159
175
|
> **"Connected" ≠ a live vehicle.** The panel shows `mavlink Connected` once the bridge *process*
|
|
160
176
|
> starts. The tools only return data while something is **actually publishing MAVLink** at the
|
|
161
177
|
> connect target. With no source, calls return empty / time out (`-32001`). Start your SITL/vehicle
|
|
162
178
|
> (or leave it — log analysis via `uav_analyze` doesn't need the live bridge).
|
|
163
179
|
|
|
164
|
-
>
|
|
165
|
-
> tune→push→retest write tier (`param_set` /
|
|
166
|
-
>
|
|
167
|
-
>
|
|
168
|
-
>
|
|
169
|
-
>
|
|
170
|
-
>
|
|
180
|
+
> **⚠ Param writes are ON by default.** The shipped command passes `--no-read-only`
|
|
181
|
+
> `--allow-param-write`, which registers the tune→push→retest write tier (`param_set` /
|
|
182
|
+
> `param_set_batch`). This lets the agent **write parameters to a live vehicle**. Prefer
|
|
183
|
+
> SITL/bench, and point `MAVLINK_CONNECT` at a real airframe only when you intend that.
|
|
184
|
+
>
|
|
185
|
+
> To ship read-only, remove both flags from the `config` hook in `src/index.ts` — the bridge's own defaults are
|
|
186
|
+
> `read_only=True` / `allow_param_write=False`, and the write tools are then never registered:
|
|
187
|
+
> ```ts
|
|
188
|
+
> command: [...py, script], // no --no-read-only / --allow-param-write
|
|
171
189
|
> ```
|
|
172
|
-
>
|
|
173
|
-
> `--connect` at a real airframe only when you intend that.
|
|
190
|
+
> Reads (`param_get`, `param_get_all`, `read_params`, telemetry) are unaffected either way.
|
|
174
191
|
|
|
175
192
|
---
|
|
176
193
|
|
package/bridge/requirements.txt
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
pymavlink>=2.4.49
|
|
2
|
-
|
|
2
|
+
# Pinned below 2.0: the MCP Python SDK removed `mcp.server.fastmcp` in 2.x (FastMCP is gone,
|
|
3
|
+
# replaced by a different MCPServer API), so this bridge fails at import on mcp>=2. The
|
|
4
|
+
# unpinned `>=1.0.0` silently floated into that breaking major. Porting to the 2.x server API
|
|
5
|
+
# is a separate change; until then this range is the supported one (verified on 1.29.0).
|
|
6
|
+
mcp[cli]>=1.0.0,<2
|
package/mcp.json
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
"url": "https://mcp.exa.ai/mcp",
|
|
5
5
|
"headers": { "x-api-key": "${EXA_API_KEY}" },
|
|
6
6
|
"enabled": false
|
|
7
|
-
},
|
|
8
|
-
"mavlink": {
|
|
9
|
-
"type": "local",
|
|
10
|
-
"enabled": false,
|
|
11
|
-
"command": ["python3", "./bridge/uav_mavlink_bridge.py", "--connect", "udp:127.0.0.1:14550"],
|
|
12
|
-
"environment": { "MAVLINK_DIALECT": "ardupilotmega" },
|
|
13
|
-
"timeout": 10000
|
|
14
7
|
}
|
|
15
8
|
}
|
|
9
|
+
|
package/package.json
CHANGED
package/plugins/index.js
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{readdir as
|
|
3
|
-
`))!==-1){let
|
|
4
|
-
`))!==-1)V(H.slice(0,O)),H=H.slice(O+1)}H+=U.decode();let
|
|
2
|
+
import{readdir as Q0,readFile as X0,writeFile as x6,mkdir as m6}from"fs/promises";import{dirname as d6,join as g}from"path";import{fileURLToPath as p6}from"url";import{tool as D}from"@kilocode/plugin";import{readdir as p0,readFile as l0}from"fs/promises";import{join as Q$}from"path";import{open as L0}from"fs/promises";function dZ(){let Z=process.env.UAV_DECODE_MAX_CPU;if(!Z)return;let $=Number(Z);return Number.isFinite($)&&$>0&&$<1?$:void 0}var H0=(Z)=>new Promise(($)=>setTimeout($,Z));async function pZ(Z,$){if(!$||Z<=0)return;let Q=Math.min(Z*(1/$-1),250);if(Q>=1)await H0(Q)}var lZ={a:{kind:"str",width:64},b:{kind:"i8",width:1},B:{kind:"u8",width:1},h:{kind:"i16",width:2},H:{kind:"u16",width:2},i:{kind:"i32",width:4},I:{kind:"u32",width:4},f:{kind:"f32",width:4},d:{kind:"f64",width:8},n:{kind:"str",width:4},N:{kind:"str",width:16},Z:{kind:"str",width:64},c:{kind:"i16",width:2,scale:0.01},C:{kind:"u16",width:2,scale:0.01},e:{kind:"i32",width:4,scale:0.01},E:{kind:"u32",width:4,scale:0.01},L:{kind:"i32",width:4,scale:0.0000001},M:{kind:"u8",width:1},q:{kind:"i64",width:8},Q:{kind:"u64",width:8}};var O0=163,A0=149,q0=128,nZ=86;function F0(Z){return($,Q)=>{let J="",X=Q+Z;for(let Y=Q;Y<X;Y++){let K=$.getUint8(Y);if(K===0)break;J+=String.fromCharCode(K)}return J}}function E0(Z){let $=Z.scale;switch(Z.kind){case"i8":return $===void 0?(Q,J)=>Q.getInt8(J):(Q,J)=>Q.getInt8(J)*$;case"u8":return $===void 0?(Q,J)=>Q.getUint8(J):(Q,J)=>Q.getUint8(J)*$;case"i16":return $===void 0?(Q,J)=>Q.getInt16(J,!0):(Q,J)=>Q.getInt16(J,!0)*$;case"u16":return $===void 0?(Q,J)=>Q.getUint16(J,!0):(Q,J)=>Q.getUint16(J,!0)*$;case"i32":return $===void 0?(Q,J)=>Q.getInt32(J,!0):(Q,J)=>Q.getInt32(J,!0)*$;case"u32":return $===void 0?(Q,J)=>Q.getUint32(J,!0):(Q,J)=>Q.getUint32(J,!0)*$;case"f32":return(Q,J)=>Q.getFloat32(J,!0);case"f64":return(Q,J)=>Q.getFloat64(J,!0);case"i64":return(Q,J)=>Q.getBigInt64(J,!0);case"u64":return(Q,J)=>Q.getBigUint64(J,!0);case"str":return F0(Z.width)}}function T0(Z,$){let Q=[],J=[];for(let Y of Z){let K=lZ[Y];if(!K)return{readers:[],widths:[],ok:!1,timeIdx:-1};Q.push(E0(K)),J.push(K.width)}let X=$.indexOf("TimeUS");return{readers:Q,widths:J,ok:!0,timeIdx:X}}async function cZ(Z,$,Q){let J=Q??dZ(),X=new Map,Y={},K=!1,W=null,G=null,z=4194304,B=await L0(Z,"r");try{let V=Buffer.allocUnsafe(z),U=new Uint8Array(0),H=0,F=!1;while(!F){let{bytesRead:T}=await B.read(V,0,z,H);H+=T,F=T===0;let L;if(T===0)L=U;else if(U.length===0)L=V.subarray(0,T);else L=new Uint8Array(U.length+T),L.set(U,0),L.set(V.subarray(0,T),U.length);let A=new DataView(L.buffer,L.byteOffset,L.byteLength),O=L.length,E=F,N=performance.now(),_=0;while(_+3<=O){if(L[_]!==O0||L[_+1]!==A0){_+=1;continue}let R=L[_+2],w=_+3;if(R===q0){if(w+nZ>O){if(E)K=!0;break}let v=L[w],mZ=L[w+1],s=qZ(L,w+2,4),W0=qZ(L,w+6,16),AZ=qZ(L,w+22,64).split(",").filter((U0)=>U0.length>0),{readers:G0,widths:z0,ok:B0,timeIdx:V0}=T0(W0,AZ);X.set(v,{name:s,length:mZ,columns:AZ,ok:B0,payloadLen:mZ-3,readers:G0,widths:z0,timeIdx:V0}),$.onFmt(s,AZ),Y[s]=(Y[s]??0)+1,_+=3+nZ;continue}let M=X.get(R);if(M===void 0){_+=1;continue}let y=M.payloadLen;if(w+y>O){if(E)K=!0;break}if(Y[M.name]=(Y[M.name]??0)+1,!M.ok){_+=3+y;continue}let{readers:LZ,widths:K0}=M,OZ=Array(LZ.length),xZ=w;for(let v=0;v<LZ.length;v++)OZ[v]=LZ[v](A,xZ),xZ+=K0[v];if($.onRow(M.name,OZ),M.timeIdx>=0){let v=OZ[M.timeIdx];if(typeof v==="bigint")G=v}_+=3+y}if(U=L.slice(_),await pZ(performance.now()-N,J),F)break}}finally{await B.close()}if(K)W=G;return{partial:K,decodedUntil:W,messageCounts:Y}}function iZ(Z,$,Q){let J=Z[$];if(!J)Z[$]={columns:Q,rows:[]};else J.columns=Q}async function i(Z){let $={},Q=await cZ(Z,{onFmt:(J,X)=>iZ($,J,X),onRow:(J,X)=>{$[J].rows.push(X)}});return{partial:Q.partial,decodedUntil:Q.decodedUntil,tables:$,messageCounts:Q.messageCounts}}async function oZ(Z,$,Q={}){let J=Q.retain??new Set,X=5000,Y={},K=new Map,W=await cZ(Z,{onFmt:(G,z)=>{if(iZ(Y,G,z),$.table(G,z),!K.has(G))K.set(G,[])},onRow:(G,z)=>{let B=K.get(G);if(B){if(B.push(z),B.length>=5000)$.rows(G,B),B.length=0}if(J.has(G))Y[G].rows.push(z)}});for(let[G,z]of K)if(z.length)$.rows(G,z);return{partial:W.partial,decodedUntil:W.decodedUntil,tables:Y,messageCounts:W.messageCounts}}function qZ(Z,$,Q){let J="",X=$+Q;for(let Y=$;Y<X;Y++){let K=Z[Y];if(K===0)break;J+=String.fromCharCode(K)}return J}import{fileURLToPath as R0}from"url";var N0="no working Python 3 interpreter found. Install Python >= 3.8 and make sure `python3` "+"(macOS/Linux) or `python` (Windows \u2014 tick \u201CAdd python.exe to PATH\u201D in the installer) runs "+"from a terminal, or set UAV_PYTHON to the interpreter's full path.",FZ=null,rZ=!1;function _0(){let Z=process.env.UAV_PYTHON?.trim();if(Z)return[[Z]];return process.platform==="win32"?[["py","-3"],["python"],["python3"]]:[["python3"],["python"]]}async function D0(Z){try{return await Bun.spawn([...Z,"-c","import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)"],{stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited===0}catch{return!1}}async function EZ(){if(rZ)return FZ;for(let Z of _0())if(await D0(Z)){FZ=Z;break}return rZ=!0,FZ}async function x(){let Z=await EZ();if(!Z)throw Error(N0);return Z}var j0=R0(new URL("./mavdump.py",import.meta.url)),P0=new Set(["TimeUS","TimeMS"]);function aZ(Z){return typeof Z==="object"&&Z!==null&&"_summary"in Z}function I0(Z){if(typeof Z==="bigint")return Z;if(typeof Z==="number"&&Number.isFinite(Z))return BigInt(Math.trunc(Z));if(typeof Z==="string"&&/^-?\d+$/.test(Z))return BigInt(Z);return 0n}async function m(Z){let $=await x();if(await Bun.spawn([...$,"-c","import pymavlink"],{stdout:"ignore",stderr:"ignore"}).exited!==0)throw Error(`pymavlink not found \u2014 ${$.join(" ")} -m pip install pymavlink`);let J=Bun.spawn([...$,j0,Z],{stdout:"pipe",stderr:"pipe"}),X=new Map,Y={},K=!1,W=null,G=null,z=!1;function B(A){let O=X.get(A);if(!O)O={columns:[],colIndex:new Map,rows:[]},X.set(A,O);return O}function V(A){let O=A.t,E=B(O),N=A.f;for(let R of Object.keys(N))if(!E.colIndex.has(R))E.colIndex.set(R,E.columns.length),E.columns.push(R);let _=Array(E.columns.length).fill(null);for(let R of Object.keys(N)){let w=E.colIndex.get(R),M=N[R],y=P0.has(R)?I0(M):M;if(_[w]=y,R==="TimeUS"&&typeof y==="bigint")G=y}E.rows.push(_)}let U=new TextDecoder,H="";for await(let A of J.stdout){H+=U.decode(A,{stream:!0});let O;while((O=H.indexOf(`
|
|
3
|
+
`))!==-1){let E=H.slice(0,O);if(H=H.slice(O+1),E.length===0)continue;let N;try{N=JSON.parse(E)}catch{K=!0;continue}if(aZ(N)){z=!0;for(let[_,R]of Object.entries(N._summary.counts))Y[_]=R;if(N._summary.partial)K=!0}else V(N)}}H+=U.decode();let F=H.trim();if(F.length>0)try{let A=JSON.parse(F);if(aZ(A)){z=!0;for(let[O,E]of Object.entries(A._summary.counts))Y[O]=E;if(A._summary.partial)K=!0}else V(A)}catch{K=!0}let T=await J.exited;if(T!==0){if(!z){let A=(await new Response(J.stderr).text()).trim();throw Error(`pymavlink decode failed (exit ${T})${A?`: ${A}`:""}`)}K=!0}if(!z)K=!0;if(K)W=G;let L={};for(let[A,O]of X)L[A]={columns:O.columns,rows:O.rows};for(let[A,O]of X)if(Y[A]===void 0)Y[A]=O.rows.length;return{partial:K,decodedUntil:W,tables:L,messageCounts:Y}}import{fileURLToPath as C0}from"url";var M0=C0(new URL("./ulog.py",import.meta.url)),S0=new Set(["timestamp","timestamp_sample"]);function w0(Z){return typeof Z==="object"&&Z!==null&&"_summary"in Z}function y0(Z){if(typeof Z==="bigint")return Z;if(typeof Z==="number"&&Number.isFinite(Z))return BigInt(Math.trunc(Z));if(typeof Z==="string"&&/^-?\d+$/.test(Z))return BigInt(Z);return 0n}async function k0(Z){let $=()=>Bun.spawn([...Z,"-c","import pyulog"],{stdout:"ignore",stderr:"ignore"}).exited;if(await $()===0)return;if(await Bun.spawn([...Z,"-m","pip","install","--quiet","pyulog"],{stdout:"ignore",stderr:"ignore"}).exited,await $()!==0)throw Error(`pyulog not found \u2014 ${Z.join(" ")} -m pip install pyulog`)}async function t(Z){let $=await x();await k0($);let Q=Bun.spawn([...$,M0,Z],{stdout:"pipe",stderr:"pipe"}),J=new Map,X={},Y=!1,K=null,W=null,G=!1;function z(A){let O=J.get(A);if(!O)O={columns:[],colIndex:new Map,rows:[]},J.set(A,O);return O}function B(A){let O=A.t,E=z(O),N=A.f;for(let R of Object.keys(N))if(!E.colIndex.has(R))E.colIndex.set(R,E.columns.length),E.columns.push(R);let _=Array(E.columns.length).fill(null);for(let R of Object.keys(N)){let w=E.colIndex.get(R),M=N[R],y=S0.has(R)?y0(M):M;if(_[w]=y,R==="timestamp"&&typeof y==="bigint")W=y}E.rows.push(_)}function V(A){if(A.length===0)return;let O;try{O=JSON.parse(A)}catch{Y=!0;return}if(w0(O)){G=!0;for(let[E,N]of Object.entries(O._summary.counts))X[E]=N;if(O._summary.partial)Y=!0}else B(O)}let U=new TextDecoder,H="";for await(let A of Q.stdout){H+=U.decode(A,{stream:!0});let O;while((O=H.indexOf(`
|
|
4
|
+
`))!==-1)V(H.slice(0,O)),H=H.slice(O+1)}H+=U.decode();let F=H.trim();if(F.length>0)V(F);let T=await Q.exited;if(T!==0){if(!G){let A=(await new Response(Q.stderr).text()).trim();throw Error(`ulog decode failed (exit ${T})${A?`: ${A}`:""}`)}Y=!0}if(!G)Y=!0;if(Y)K=W;let L={};for(let[A,O]of J)L[A]={columns:O.columns,rows:O.rows};for(let[A,O]of J)if(X[A]===void 0)X[A]=O.rows.length;return{partial:Y,decodedUntil:K,tables:L,messageCounts:X}}import{existsSync as sZ,readFileSync as v0}from"fs";import{mkdtemp as h0,rm as b0}from"fs/promises";import{tmpdir as f0}from"os";import{basename as TZ,join as NZ}from"path";function e(Z){if(!Z)return[];let $=NZ(Z,".hydron","uav-format-converters.json");if(!sZ($))return[];try{let Q=JSON.parse(v0($,"utf8"));return Array.isArray(Q.converters)?Q.converters.filter((J)=>J&&J.match&&J.command):[]}catch{return[]}}function ZZ(Z,$){for(let Q of $)try{if(new RegExp(Q.match,"i").test(Z))return Q}catch{}return null}async function $Z(Z,$){let Q=await h0(NZ(f0(),"uav-convert-")),J=TZ(Z).replace(/\.[^.]+$/,""),X=NZ(Q,J+($.produces??".bin")),Y=$.command.replaceAll("{input}",Z).replaceAll("{output}",X),K=()=>b0(Q,{recursive:!0,force:!0});try{let W=process.platform==="win32"?["cmd.exe","/d","/s","/c",Y]:["bash","-lc",Y],G=Bun.spawn(W,{stdout:"pipe",stderr:"pipe"}),z=await G.exited;if(z!==0){let B=(await new Response(G.stderr).text()).trim().slice(0,300);throw Error(`format converter failed (exit ${z}) for ${TZ(Z)}: ${B}`)}if(!sZ(X))throw Error(`format converter produced no output for ${TZ(Z)}`);return{out:X,cleanup:K}}catch(W){throw await K(),W}}import{Database as g0}from"bun:sqlite";import{tmpdir as u0}from"os";import{join as x0}from"path";import{existsSync as tZ,unlinkSync as eZ}from"fs";var m0=0;function $$(Z){let $=x0(u0(),`uav-${Z}-${process.pid}-${m0++}.sqlite`);try{if(tZ($))eZ($)}catch{}let Q=new g0($);return Q.exec("PRAGMA page_size = 8192"),Q.query("PRAGMA journal_mode = MEMORY").get(),Q.exec("PRAGMA synchronous = OFF"),Q.exec("PRAGMA temp_store = FILE"),Q.exec("PRAGMA cache_size = -65536"),{db:Q,cleanup(){try{Q.close()}catch{}try{if(tZ($))eZ($)}catch{}}}}var _Z=(Z)=>`"${Z.replace(/"/g,'""')}"`,Z$=5000;function DZ(Z){let $=new Map,Q=new Map;return{table(J,X){if($.has(J)||X.length===0)return;Z.run(`CREATE TABLE ${_Z(J)} (${X.map(_Z).join(", ")})`);let Y=X.map(()=>"?").join(", ");$.set(J,Z.query(`INSERT INTO ${_Z(J)} VALUES (${Y})`)),Q.set(J,X.length)},rows(J,X){let Y=$.get(J),K=Q.get(J);if(!Y||K===void 0||X.length===0)return;let W=Z.transaction((G)=>{for(let z of G){let B=Array(K);for(let V=0;V<K;V++){let U=z[V];B[V]=U===void 0?null:U}Y.run(...B)}});for(let G=0;G<X.length;G+=Z$)W(X.slice(G,G+Z$))}}}function J$(Z,$){let Q=DZ(Z);for(let[J,X]of Object.entries($.tables)){if(X.columns.length===0)continue;if(Q.table(J,X.columns),X.rows.length)Q.rows(J,X.rows)}}var d0=new Set(["PARM","MSG","GPS","ERR","MODE"]);async function o(Z,$="flight",Q){let J=ZZ(Z,e(Q?.configDir));if(J){let{out:K,cleanup:W}=await $Z(Z,J);try{return await o(K,$)}finally{await W()}}let{db:X,cleanup:Y}=$$($);try{let K=Z.toLowerCase();if(K.endsWith(".bin"))return{log:await oZ(Z,DZ(X),{retain:d0}),db:X,cleanup:Y};let W;if(K.endsWith(".ulog"))W=await t(Z);else if(K.endsWith(".tlog")||K.endsWith(".log"))W=await m(Z);else try{W=await i(Z)}catch{W=await m(Z)}return J$(X,W),{log:W,db:X,cleanup:Y}}catch(K){throw Y(),K}}var JZ=1e4;async function n0(Z,$,Q){let J=Q$(Z,".hydron","observations",$),X;try{X=await p0(J)}catch{throw Error(`No observation store at ${J}. Run uav_analyze first.`)}for(let Y of X){let K=Q$(J,Y,`${Q}.json`);try{let G=JSON.parse(await l0(K,"utf8"))?.episode?.attrs?.source_path;if(typeof G==="string"&&G)return G;throw Error(`Episode ${Q} has no source_path; re-run uav_analyze to record it.`)}catch(W){if(W.code==="ENOENT")continue;throw W}}throw Error(`flight_id ${Q} not found in the observation store. Run uav_analyze first.`)}function c0(Z){let $=Z.trim();$=$.replace(/^```(?:sql)?\s*/i,"").replace(/\s*```$/,"").trim();for(;;){let X=$;if($=$.replace(/^--[^\n]*\n?/,"").replace(/^\/\*[\s\S]*?\*\//,"").trimStart(),$===X)break}if($=$.replace(/;\s*$/,""),$.includes(";"))throw Error("Only a single statement is allowed (no ';').");let Q=$.replace(/^\(+\s*/,"");if(!(/^\s*(select|with|values)\b/i.test(Q)||/^\s*explain\b/i.test($)||/^\s*pragma\s+(table_info|table_list|table_xinfo|index_list|index_info|index_xinfo|database_list|collation_list|function_list|pragma_list)\b/i.test($)))throw Error("uav_query is read-only: use SELECT/WITH/VALUES (comments & ```fences ok), EXPLAIN, or a read PRAGMA (table_info/table_list/index_*). For heavier analysis use Python via bash, not SQL.");if(/\b(insert|update|delete|drop|create|alter|attach|detach|replace|reindex|vacuum)\b/i.test($))throw Error("Write/DDL keyword detected \u2014 uav_query is read-only.");if(!/\blimit\b/i.test($))$+=` LIMIT ${JZ}`;return $}async function X$(Z){let $=c0(Z.query),Q=await n0(Z.projectDir,Z.workspace,Z.flight_id),{db:J,cleanup:X}=await o(Q,Z.flight_id);try{J.exec?.("PRAGMA query_only = ON");let K=J.query($).all(),W=K.length?Object.keys(K[0]):[],z=K.slice(0,JZ).map((B)=>W.map((V)=>B[V]));return{columns:W,rows:z,truncated:K.length>JZ}}finally{X()}}function Y$(Z){if(!Z.columns.length)return"(no rows)";let $=Z.columns.join(" | "),Q=Z.rows.slice(0,50).map((X)=>X.map((Y)=>typeof Y==="bigint"?Y.toString():String(Y)).join(" | ")).join(`
|
|
5
5
|
`),J=Z.rows.length>50?`
|
|
6
6
|
\u2026 ${Z.rows.length-50} more rows`:Z.truncated?`
|
|
7
7
|
(capped at ${JZ} rows)`:"";return`${$}
|
|
8
|
-
${Q}${J}`}import{createHash as W6}from"crypto";import{open as g$,stat as z6}from"fs/promises";import{resolve as B6}from"path";function RZ(Z,$){let Q=Z.tables.PARM;if(!Q)return null;let J=Q.columns.findIndex((K)=>K.toLowerCase()==="name"),X=Q.columns.findIndex((K)=>K.toLowerCase()==="value");if(J<0||X<0)return null;let Y=$.toLowerCase();for(let K of Q.rows)if(String(K[J]).toLowerCase()===Y){let G=Number(K[X]);return Number.isFinite(G)?G:null}return null}function n0(Z){let $=Z.tables.MSG;if(!$)return"";let Q=$.columns.findIndex((J)=>/message|msg/i.test(J));if(Q<0)return"";for(let J of $.rows){let X=String(J[Q]??"");if(/Ardu(Copter|Plane|Sub|Rover)|PX4/i.test(X))return X}return""}function Y$(Z){let $=RZ(Z,"Q_ENABLE");if($!==null&&$>=1||Z.tables.QUIK||Z.tables.AETR){if(Z.tables.MOTB||RZ(Z,"FRAME_CLASS")!==null||Z.tables.ARSP)return"vtol"}if($!==null&&$>=1)return"vtol";let Q=n0(Z);if(/ArduPlane/i.test(Q))return"fixedwing";if(/ArduCopter/i.test(Q))return"multirotor";if(RZ(Z,"FRAME_CLASS")!==null)return"multirotor";if(Z.tables.ARSP&&!Z.tables.MOTB)return"fixedwing";if(Z.tables.MOTB||Z.tables.RATE)return"multirotor";return"unknown"}function QZ(Z){return Z===void 0||Z==="multirotor"||Z==="vtol"||Z==="unknown"}function K$(Z){return Z==="fixedwing"||Z==="vtol"}function S(Z){return`"${Z.replace(/"/g,'""')}"`}function j(Z,$){let Q=Z.db.query($).get();if(!Q)return null;let J=Object.values(Q)[0];if(J===null||J===void 0)return null;return Number(J)}function k(Z,$){try{let Q=Z.db.query(`PRAGMA table_info(${S($)})`).all();return new Set(Q.map((J)=>J.name))}catch{return new Set}}function C(Z,$,Q){return k(Z,$).has(Q)}function W$(Z,$){for(let Q of $)if(Z.hasTable(Q))return Q;return null}function q(Z,$,Q){return{check:Z,severity:"OK",value:null,threshold_used:null,ruleset_version:$.ruleset_version,verdict:"SKIP",tier:"system",...Q?{detail:Q}:{}}}function P(Z){return{check:Z.check,severity:Z.failed?Z.failSeverity:"OK",value:Z.value,threshold_used:Z.threshold_used,ruleset_version:Z.thresholds.ruleset_version,verdict:Z.failed?"FAIL":"PASS",tier:"system",...Z.source?{source:Z.source}:{},...Z.detail?{detail:Z.detail}:{}}}var c0=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("ERR"))return q("battery",Z.thresholds);if(!C(Z,"ERR","Subsys")||!C(Z,"ERR","ECode"))return q("battery",Z.thresholds,"ERR present but Subsys/ECode columns absent");let Q=$?.err_subsys??6,J=$?.err_ecode??1,X=j(Z,`SELECT COUNT(*) FROM "ERR" WHERE "Subsys"=${Q} AND "ECode"=${J}`)??0;return P({check:"battery",thresholds:Z.thresholds,failed:X>0,failSeverity:"CRITICAL",value:X,threshold_used:0,source:$?.source,detail:X>0?`${X} battery failsafe ERR event(s) (Subsys=${Q} ECode=${J})`:void 0})},i0=(Z)=>{let $=Z.thresholds.attitude;if(!Z.hasTable("ATT"))return q("attitude",Z.thresholds);for(let K of["DesRoll","Roll","DesPitch","Pitch"])if(!C(Z,"ATT",K))return q("attitude",Z.thresholds,`ATT missing column ${K}`);let Q=$?.max_deg??5,J=j(Z,'SELECT MAX(ABS("DesRoll"-"Roll")) FROM "ATT"'),X=j(Z,'SELECT MAX(ABS("DesPitch"-"Pitch")) FROM "ATT"'),Y=Math.max(J??0,X??0);return P({check:"attitude",thresholds:Z.thresholds,failed:Y>Q,failSeverity:"HIGH",value:Y,threshold_used:Q,source:$?.source,detail:`peak roll err ${(J??0).toFixed(2)}\xB0, pitch err ${(X??0).toFixed(2)}\xB0`})},o0=(Z)=>{let $=Z.thresholds.altitude;if(!Z.hasTable("CTUN"))return q("altitude",Z.thresholds);if(!C(Z,"CTUN","DAlt")||!C(Z,"CTUN","Alt"))return q("altitude",Z.thresholds,"CTUN missing DAlt/Alt columns");let Q=$?.max_m??2,J=j(Z,'SELECT MAX(ABS("DAlt"-"Alt")) FROM "CTUN"');return P({check:"altitude",thresholds:Z.thresholds,failed:(J??0)>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source})},r0=(Z)=>{let $=Z.thresholds.hover;if(!QZ(Z.vehicleType))return q("hover",Z.thresholds,`n/a for ${Z.vehicleType} (no hover throttle)`);if(!Z.hasTable("CTUN"))return q("hover",Z.thresholds);if(!C(Z,"CTUN","ThH"))return q("hover",Z.thresholds,"CTUN missing ThH column");let Q=$?.band??[0.25,0.75],[J,X]=Q,Y=j(Z,'SELECT AVG("ThH") FROM "CTUN"'),K=Y===null?!1:Y<J||Y>X;return P({check:"hover",thresholds:Z.thresholds,failed:K,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`mean hover throttle ${(Y??0).toFixed(3)} vs band [${J}, ${X}]`})},G$=2000,a0=(Z)=>{if(!QZ(Z.vehicleType))return q("rcou",Z.thresholds,`n/a for ${Z.vehicleType} (control surfaces, not motor mix)`);if(!Z.hasTable("RCOU"))return q("rcou",Z.thresholds);let $=k(Z,"RCOU"),Q=[];for(let Y=1;Y<=8;Y++)if($.has(`C${Y}`))Q.push(`C${Y}`);if(Q.length===0)return q("rcou",Z.thresholds,"RCOU has no C1..C8 channels");let J=-1/0,X=[];for(let Y of Q){let K=j(Z,`SELECT MAX(${S(Y)}) FROM "RCOU"`);if(K===null)continue;if(K>J)J=K;if(K>=G$)X.push(`${Y}=${K}`)}if(J===-1/0)J=0;return P({check:"rcou",thresholds:Z.thresholds,failed:X.length>0,failSeverity:"HIGH",value:J,threshold_used:G$,detail:X.length>0?`motor output(s) pegged at max: ${X.join(", ")}`:void 0})},DZ=1000,jZ=2000,s0=(Z)=>{if(!Z.hasTable("RCIN"))return q("rcin",Z.thresholds);if(!C(Z,"RCIN","C3"))return q("rcin",Z.thresholds,"RCIN missing C3 column");let $=j(Z,'SELECT MIN("C3") FROM "RCIN"'),Q=j(Z,'SELECT MAX("C3") FROM "RCIN"'),J=$!==null&&$<DZ||Q!==null&&Q>jZ,X=Q;if($!==null&&$<DZ)X=$;return P({check:"rcin",thresholds:Z.thresholds,failed:J,failSeverity:"HIGH",value:X,threshold_used:jZ,detail:`RCIN C3 range [${$??"\u2014"}, ${Q??"\u2014"}] vs [${DZ}, ${jZ}]`})},t0=(Z)=>{let $=Z.thresholds.vibration;if(!Z.hasTable("VIBE"))return q("vibration",Z.thresholds);let Q=k(Z,"VIBE"),J=["VibeX","VibeY","VibeZ"].filter((V)=>Q.has(V));if(J.length===0)return q("vibration",Z.thresholds,"VIBE has no VibeX/Y/Z columns");let X=$?.max_ms2??30,Y={},K=-1/0;for(let V of J){let H=j(Z,`SELECT MAX(${S(V)}) FROM "VIBE"`)??0;if(Y[V]=H,H>K)K=H}if(K===-1/0)K=0;let G=["Clip","Clip0","Clip1","Clip2"].filter((V)=>Q.has(V)),W=0;for(let V of G){let U=j(Z,`SELECT MAX(${S(V)}) FROM "VIBE"`);if(U!==null&&U>W)W=U}let z=G.length>0?` | clipping MAX=${W}${W>0?" (CLIP EVENTS)":""}`:"",B=e0(Y,K,W);return P({check:"vibration",thresholds:Z.thresholds,failed:K>=X,failSeverity:"HIGH",value:K,threshold_used:X,source:$?.source,detail:`peak vibe ${K} m/s\xB2 (axes ${J.join("/")})${z}${B}`})};function e0(Z,$,Q){let J=Object.values(Z);if($<=0||J.length===0)return"";let X=[...J].sort((z,B)=>B-z),Y=X[0],K=X.length>1?X[1]:0;if(X.length>=2&&K>0&&Y>=2*K)return" | cause: likely sensor/mounting alignment (one axis dominates)";let W=X.length>=2&&Y<=1.5*(X[X.length-1]||Y);if(Q>0||W)return" | cause: likely mechanical degradation (props/bearings/frame)";return""}var Z8=(Z)=>{let $=Z.thresholds.mag;if(!Z.hasTable("MAG"))return q("mag",Z.thresholds);if(!C(Z,"MAG","Health"))return q("mag",Z.thresholds,"MAG missing Health column");let Q=$?.min??1,J=j(Z,'SELECT MIN("Health") FROM "MAG"');return P({check:"mag",thresholds:Z.thresholds,failed:J!==null&&J<Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source})},$8=(Z)=>{let $=Z.thresholds.ekf,Q=W$(Z,["XKF3","NKF3"]);if(!Q)return q("ekf",Z.thresholds);let J=k(Z,Q),X=["IVN","IVE","IVD","IVT","IPN","IPE","IPD"].filter((G)=>J.has(G));if(X.length===0)return q("ekf",Z.thresholds,`${Q} has no IVN/IVE/IVD/IVT/IPN/IPE/IPD columns`);let Y=$?.max_ratio??0.5,K=-1/0;for(let G of X){let W=j(Z,`SELECT MAX(ABS(${S(G)})) FROM ${S(Q)}`);if(W!==null&&W>K)K=W}if(K===-1/0)K=0;return P({check:"ekf",thresholds:Z.thresholds,failed:K>Y,failSeverity:"HIGH",value:K,threshold_used:Y,source:$?.source,detail:`peak innovation ${K} (${Q}: ${X.join("/")})`})},J8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("min_cell_voltage",Z.thresholds);if(!C(Z,"BAT","Volt"))return q("min_cell_voltage",Z.thresholds,"BAT missing Volt column");let Q=$?.min_cell_v??3.3,J=j(Z,'SELECT MAX("Volt") FROM "BAT"'),X=j(Z,'SELECT MIN("Volt") FROM "BAT"');if(J===null||X===null||J<=0)return q("min_cell_voltage",Z.thresholds,"BAT.Volt has no usable values");let Y=Math.max(1,Math.round(J/4.2)),K=X/Y;return P({check:"min_cell_voltage",thresholds:Z.thresholds,failed:K<Q,failSeverity:"CRITICAL",value:K,threshold_used:Q,source:$?.source,detail:`${Y}S pack (MAX pack ${J.toFixed(2)}V), min cell ${K.toFixed(3)}V vs floor ${Q}V`})},Q8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("voltage_sag",Z.thresholds);if(!C(Z,"BAT","VoltR")||!C(Z,"BAT","Volt"))return q("voltage_sag",Z.thresholds,"BAT missing VoltR/Volt columns");let Q=$?.max_sag_v??2,J=j(Z,'SELECT MAX("VoltR"-"Volt") FROM "BAT"');return P({check:"voltage_sag",thresholds:Z.thresholds,failed:J!==null&&J>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source,detail:`peak sag ${(J??0).toFixed(2)}V (VoltR-Volt) vs ${Q}V`})},X8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("over_current",Z.thresholds);if(!C(Z,"BAT","Curr"))return q("over_current",Z.thresholds,"BAT missing Curr column");let Q=$?.max_current_a??200,J=j(Z,'SELECT MAX("Curr") FROM "BAT"');return P({check:"over_current",thresholds:Z.thresholds,failed:J!==null&&J>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source,detail:`peak current ${(J??0).toFixed(1)}A vs ${Q}A`})},Y8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("internal_resistance",Z.thresholds);if(!C(Z,"BAT","Res"))return q("internal_resistance",Z.thresholds,"BAT missing Res column");let Q=j(Z,'SELECT MAX("Res") FROM "BAT"');if(Q===null||Q===0)return q("internal_resistance",Z.thresholds,"BAT.Res all zero/absent (not reported)");let J=$?.res_min_current_a??5,Y=C(Z,"BAT","Curr")?j(Z,`SELECT MAX("Res") FROM "BAT" WHERE "Curr" >= ${J}`):Q;if(Y===null)return q("internal_resistance",Z.thresholds,`no BAT samples at Curr \u2265 ${J} A \u2014 Res only valid under load (max raw ${Q.toFixed(3)}\u03A9 is a low-current artifact)`);let K=$?.max_res_ohm??0.03;return P({check:"internal_resistance",thresholds:Z.thresholds,failed:Y>K,failSeverity:"HIGH",value:Y,threshold_used:K,source:$?.source,detail:`peak internal resistance ${Y.toFixed(4)}\u03A9 (at Curr \u2265 ${J} A) vs ${K}\u03A9`})},K8=(Z)=>{let $=Z.thresholds.power;if(!Z.hasTable("POWR"))return q("board_brownout",Z.thresholds);if(!C(Z,"POWR","Vcc"))return q("board_brownout",Z.thresholds,"POWR missing Vcc column");let Q=$?.min_vcc_v??4.5,J=j(Z,'SELECT MIN("Vcc") FROM "POWR"');return P({check:"board_brownout",thresholds:Z.thresholds,failed:J!==null&&J<Q,failSeverity:"CRITICAL",value:J,threshold_used:Q,source:$?.source,detail:`min board Vcc ${(J??0).toFixed(3)}V vs floor ${Q}V`})},G8=(Z)=>{let $=Z.thresholds.rate;if(!Z.hasTable("RATE"))return q("rate_tracking",Z.thresholds);let Q=k(Z,"RATE"),X=[["RDes","R"],["PDes","P"],["YDes","Y"]].filter(([W,z])=>Q.has(W)&&Q.has(z));if(X.length===0)return q("rate_tracking",Z.thresholds,"RATE has no RDes/R, PDes/P, YDes/Y pairs");let Y=$?.max_err_dps??50,K=-1/0,G="";for(let[W,z]of X){let B=j(Z,`SELECT MAX(ABS(${S(W)}-${S(z)})) FROM "RATE"`);if(B!==null&&B>K)K=B,G=W.replace("Des","")}if(K===-1/0)K=0;return P({check:"rate_tracking",thresholds:Z.thresholds,failed:K>Y,failSeverity:"HIGH",value:K,threshold_used:Y,source:$?.source,detail:`peak rate-tracking error ${K.toFixed(2)} deg/s (axis ${G||"?"}) vs ${Y} deg/s`})},W8=(Z)=>{let $=Z.thresholds.gps;if(!Z.hasTable("GPA"))return q("gps_accuracy",Z.thresholds);if(!C(Z,"GPA","HAcc"))return q("gps_accuracy",Z.thresholds,"GPA missing HAcc column");let Q=$?.hacc_max_m??5,J=j(Z,'SELECT MAX("HAcc") FROM "GPA"');return P({check:"gps_accuracy",thresholds:Z.thresholds,failed:J!==null&&J>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source,detail:`peak horizontal accuracy ${(J??0).toFixed(2)}m vs ${Q}m`})},z8=(Z)=>{let $=Z.thresholds.ekf,Q=W$(Z,["XKF4","NKF4"]);if(!Q)return q("ekf_variance",Z.thresholds);let J=k(Z,Q),X=["SV","SP","SH","SM"].filter((B)=>J.has(B));if(X.length===0)return q("ekf_variance",Z.thresholds,`${Q} has no SV/SP/SH/SM columns`);let Y=$?.max_variance??1,K=-1/0,G="";for(let B of X){let V=j(Z,`SELECT MAX(${S(B)}) FROM ${S(Q)}`);if(V!==null&&V>K)K=V,G=B}if(K===-1/0)K=0;let W=J.has("FS")?j(Z,`SELECT MAX("FS") FROM ${S(Q)}`):null,z=W!==null&&W>0?` | EKF failsafe set (FS MAX=${W})`:"";return P({check:"ekf_variance",thresholds:Z.thresholds,failed:K>Y,failSeverity:"HIGH",value:K,threshold_used:Y,source:$?.source,detail:`peak EKF variance ${K.toFixed(3)} (${Q}: ${G||X.join("/")}) vs ${Y}${z}`})},B8=(Z)=>{let $=Z.thresholds.mag;if(!Z.hasTable("MAG"))return q("mag_offsets",Z.thresholds);let Q=k(Z,"MAG"),J=["OfsX","OfsY","OfsZ"].filter((G)=>Q.has(G));if(J.length===0)return q("mag_offsets",Z.thresholds,"MAG has no OfsX/OfsY/OfsZ columns");let X=$?.max_offset??600,Y=-1/0,K="";for(let G of J){let W=j(Z,`SELECT MAX(ABS(${S(G)})) FROM "MAG"`);if(W!==null&&W>Y)Y=W,K=G}if(Y===-1/0)Y=0;return P({check:"mag_offsets",thresholds:Z.thresholds,failed:Y>X,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`peak mag offset ${Y.toFixed(0)} (${K||J.join("/")}) vs ${X}`})},V8=(Z)=>{let $=Z.thresholds.esc;if(!Z.hasTable("ESC"))return q("esc_health",Z.thresholds);let Q=k(Z,"ESC"),J=["Temp","MotTemp"].filter((V)=>Q.has(V));if(J.length===0&&!Q.has("Err"))return q("esc_health",Z.thresholds,"ESC has no Temp/MotTemp/Err columns");let X=$?.max_temp_c??90,Y=-1/0,K="";for(let V of J){let U=j(Z,`SELECT MAX(${S(V)}) FROM "ESC"`);if(U!==null&&U>Y)Y=U,K=V}if(Y===-1/0)Y=0;let G=Q.has("Err")?j(Z,'SELECT MAX("Err") FROM "ESC"')??0:0,W=J.length>0&&Y>X,z=G>0,B=z?` | ESC Err MAX=${G}`:"";return P({check:"esc_health",thresholds:Z.thresholds,failed:W||z,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`peak ESC temp ${Y.toFixed(1)}\xB0C (${K||"n/a"}) vs ${X}\xB0C${B}`})},U8=(Z)=>{if(!QZ(Z.vehicleType))return q("thrust_margin",Z.thresholds,`n/a for ${Z.vehicleType} (lift/T:W is a rotorcraft metric)`);if(!Z.hasTable("MOTB"))return q("thrust_margin",Z.thresholds);let $=k(Z,"MOTB");if(!$.has("FailFlags")&&!($.has("ThrOut")&&$.has("LiftMax")))return q("thrust_margin",Z.thresholds,"MOTB missing FailFlags and ThrOut/LiftMax columns");let Q=$.has("FailFlags")?j(Z,'SELECT MAX("FailFlags") FROM "MOTB"')??0:0,J=null;if($.has("ThrOut")&&$.has("LiftMax")){let G=j(Z,'SELECT AVG("ThrOut") FROM "MOTB"'),W=j(Z,'SELECT AVG("LiftMax") FROM "MOTB"');if(G!==null&&W!==null&&W>0)J=G/W}let X=J!==null&&J>0.5,Y=Q>0,K=`MOTB FailFlags MAX=${Q}`+(J!==null?`, mean ThrOut/LiftMax ${J.toFixed(2)} (low-margin > 0.5)`:"")+(Y?" \u2014 THRUST-LOSS flagged":X?" \u2014 low thrust margin":"");return P({check:"thrust_margin",thresholds:Z.thresholds,failed:Y||X,failSeverity:"HIGH",value:J,threshold_used:0.5,detail:K})},H8=(Z)=>{let $=Z.thresholds.imu;if(!Z.hasTable("IMU"))return q("imu_health",Z.thresholds);let Q=k(Z,"IMU"),J=["EG","EA"].filter((G)=>Q.has(G));if(J.length===0)return q("imu_health",Z.thresholds,"IMU has no EG/EA error-count columns");let X=$?.max_err??0,Y=-1/0,K="";for(let G of J){let W=j(Z,`SELECT MAX(${S(G)}) FROM "IMU"`);if(W!==null&&W>Y)Y=W,K=G}if(Y===-1/0)Y=0;return P({check:"imu_health",thresholds:Z.thresholds,failed:Y>X,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`max IMU error count ${Y} (${K||J.join("/")}) vs ${X}`})},L8=(Z)=>{let $=Z.thresholds.imu;if(!Z.hasTable("IMU"))return q("imu_consistency",Z.thresholds);let Q=k(Z,"IMU");if(!Q.has("I"))return q("imu_consistency",Z.thresholds,"IMU has no instance column I");let J=["AccX","AccY","AccZ"].filter((N)=>Q.has(N)),X=["GyrX","GyrY","GyrZ"].filter((N)=>Q.has(N));if(J.length===0&&X.length===0)return q("imu_consistency",Z.thresholds,"IMU has no Acc*/Gyr* columns");let Y=[...J,...X].map((N)=>`AVG(${S(N)}) ${S(N)}`).join(", "),K=Z.db.query(`SELECT "I" AS "I", ${Y} FROM "IMU" GROUP BY "I"`).all();if(K.length<2)return q("imu_consistency",Z.thresholds,`only ${K.length} IMU instance \u2014 nothing to cross-check`);let G=(N)=>{let _=K.map((D)=>Number(D[N])).filter((D)=>Number.isFinite(D));return _.length>=2?Math.max(..._)-Math.min(..._):0},W=0,z="";for(let N of J){let _=G(N);if(_>W)W=_,z=N}let B=0,V="";for(let N of X){let _=G(N);if(_>B)B=_,V=N}let U=$?.accel_consistency_max??0.7,H=$?.gyro_consistency_max??0.3,E=W>U,T=B>H,L=E||T,A=W/U>=B/H?W:B,O=W/U>=B/H?U:H,F=L?`INCONSISTENT \u2014 ${E?`accel ${z} spread ${W.toFixed(3)} m/s\xB2 > ${U}`:""}${E&&T?"; ":""}${T?`gyro ${V} spread ${B.toFixed(3)} rad/s > ${H}`:""}`:`IMUs consistent \u2014 max inter-instance bias spread: accel ${W.toFixed(3)} m/s\xB2 (<${U}), gyro ${B.toFixed(3)} rad/s (<${H}); relative differences between instances are within healthy tolerance`;return P({check:"imu_consistency",thresholds:Z.thresholds,failed:L,failSeverity:"HIGH",value:A,threshold_used:O,source:{kind:"rule-of-thumb",ref:"IMU inconsistency tolerance (~ArduPilot arming limits)"},detail:F})},O8=(Z)=>{if(!Z.hasTable("ERR"))return q("err_events",Z.thresholds);if(!C(Z,"ERR","Subsys")||!C(Z,"ERR","ECode"))return q("err_events",Z.thresholds,"ERR present but Subsys/ECode columns absent");let $=Z.thresholds.battery?.err_subsys??6,Q=Z.db.query(`SELECT "Subsys" AS subsys, "ECode" AS ecode, COUNT(*) AS n FROM "ERR" WHERE "ECode" <> 0 AND "Subsys" <> ${$} GROUP BY "Subsys", "ECode" ORDER BY "Subsys", "ECode"`).all(),J=0,X=[];for(let Y of Q)J+=Number(Y.n),X.push(`Subsys=${Y.subsys} ECode=${Y.ecode} \xD7${Y.n}`);return P({check:"err_events",thresholds:Z.thresholds,failed:J>0,failSeverity:"HIGH",value:J,threshold_used:0,detail:J>0?`${J} non-battery ERR event(s): ${X.join(", ")}`:"no non-battery ERR events"})},A8=new Set([6,9]),q8=(Z)=>{if(!Z.hasTable("MODE"))return q("mode_changes",Z.thresholds);if(!C(Z,"MODE","ModeNum"))return q("mode_changes",Z.thresholds,"MODE missing ModeNum column");let $=Z.db.query('SELECT "ModeNum" AS modenum FROM "MODE"').all(),Q=$.length,J=Q>0?Q-1:0,X=new Map,Y=!1;for(let G of $){let W=Number(G.modenum);if(X.set(W,(X.get(W)??0)+1),A8.has(W))Y=!0}let K=[...X.entries()].sort((G,W)=>G[0]-W[0]).map(([G,W])=>`Mode${G}\xD7${W}`).join(", ");return P({check:"mode_changes",thresholds:Z.thresholds,failed:Y,failSeverity:"HIGH",value:J,threshold_used:0,detail:`${J} mode change(s); timeline: ${K||"\u2014"}`+(Y?" \u2014 emergency mode (RTL/LAND) entered":"")})},E8=(Z)=>{let $=Z.thresholds.airspeed;if(!K$(Z.vehicleType))return q("airspeed",Z.thresholds,`n/a for ${Z.vehicleType} (no fixed-wing airspeed)`);if(!Z.hasTable("ARSP"))return q("airspeed",Z.thresholds,"no ARSP (airspeed) data");if(!C(Z,"ARSP","Airspeed"))return q("airspeed",Z.thresholds,"ARSP missing Airspeed column");let Q=$?.stall_min_ms??8,J={kind:"rule-of-thumb",ref:"default stall floor"};if(Z.hasTable("PARM")){let Y=j(Z,`SELECT "Value" FROM "PARM" WHERE "Name"='ARSPD_FBW_MIN' LIMIT 1`);if(Y!==null&&Y>0)Q=Y,J={kind:"datasheet",ref:"ARSPD_FBW_MIN"}}let X=j(Z,'SELECT MIN("Airspeed") FROM "ARSP" WHERE "Airspeed" > 2');if(X===null)return q("airspeed",Z.thresholds,"no airborne airspeed samples");return P({check:"airspeed",thresholds:Z.thresholds,failed:X<Q,failSeverity:"HIGH",value:X,threshold_used:Q,source:J,detail:`min in-flight airspeed ${X.toFixed(1)} m/s vs stall floor ${Q} m/s`})},F8=[{id:"battery",run:c0},{id:"min_cell_voltage",run:J8},{id:"voltage_sag",run:Q8},{id:"over_current",run:X8},{id:"internal_resistance",run:Y8},{id:"board_brownout",run:K8},{id:"attitude",run:i0},{id:"rate_tracking",run:G8},{id:"altitude",run:o0},{id:"hover",run:r0},{id:"rcou",run:a0},{id:"rcin",run:s0},{id:"thrust_margin",run:U8},{id:"vibration",run:t0},{id:"imu_health",run:H8},{id:"imu_consistency",run:L8},{id:"mag",run:Z8},{id:"mag_offsets",run:B8},{id:"gps_accuracy",run:W8},{id:"ekf",run:$8},{id:"ekf_variance",run:z8},{id:"esc_health",run:V8},{id:"airspeed",run:E8},{id:"err_events",run:O8},{id:"mode_changes",run:q8}];function z$(Z,$,Q){let J=new Set;try{let K=Z.query("SELECT name FROM sqlite_master WHERE type='table'").all();for(let G of K)J.add(G.name)}catch{}let X={db:Z,thresholds:$,hasTable:(K)=>J.has(K),vehicleType:Q?.vehicleType},Y=[];for(let{id:K,run:G}of F8)try{Y.push(G(X))}catch(W){let z=W instanceof Error?W.message:String(W);Y.push(q(K,$,`check error: ${z}`))}return Y}import{existsSync as j8,readFileSync as P8}from"fs";import{join as I8}from"path";import{existsSync as T8,readFileSync as N8}from"fs";import{dirname as _8,join as PZ}from"path";import{fileURLToPath as R8}from"url";var B$=_8(R8(import.meta.url)),D8=[PZ(B$,"..","data","apm-params.json"),PZ(B$,"..","..","data","apm-params.json")];function U$(Z){if(Z){let $=PZ(Z,".hydron","apm-params.json"),Q=V$($);if(Q&&Q.size>0)return Q}for(let $ of D8){let Q=V$($);if(Q)return Q}return new Map}function H$(Z,$){return $.get(Z.toUpperCase())}function V$(Z){if(!T8(Z))return;try{let $=N8(Z,"utf8"),Q=JSON.parse($);if(!Q||typeof Q!=="object")return;let J=new Map;for(let[X,Y]of Object.entries(Q)){if(!X||X.startsWith("_")||typeof Y!=="object"||Y===null)continue;let K={name:X};if(typeof Y.min==="number"&&Number.isFinite(Y.min))K.min=Y.min;if(typeof Y.max==="number"&&Number.isFinite(Y.max))K.max=Y.max;if(typeof Y.units==="string"&&Y.units)K.units=Y.units;J.set(X.toUpperCase(),K)}return J}catch{return}}function O$(){return{ruleset_version:"v1",safety:{ARMING_CHECK:{min:1},FS_THR_ENABLE:{min:1},FENCE_ENABLE:{min:1}},range:{INS_GYRO_FILTER:{min:5,max:80,severity:"HIGH"},ATC_RAT_RLL_P:{min:0,max:0.5,severity:"HIGH"},ATC_RAT_PIT_P:{min:0,max:0.5,severity:"HIGH"},ATC_RAT_YAW_P:{min:0,max:1,severity:"HIGH"},MOT_SPIN_ARM:{min:0,max:0.2,severity:"HIGH"},MOT_SPIN_MIN:{min:0,max:0.3,severity:"HIGH"},RTL_ALT:{min:200,max:1e4,severity:"HIGH"},WPNAV_SPEED:{min:50,max:2000,severity:"HIGH"}}}}function C8(Z){let $=O$(),Q=I8(Z,".hydron","param-rules.yaml");if(!j8(Q))return $;try{let J=P8(Q,"utf8");return M8($,J)}catch{return $}}function M8(Z,$){let Q={ruleset_version:Z.ruleset_version,safety:{...Z.safety},range:{...Z.range}},J=null;for(let X of $.split(/\r?\n/)){let K=X.replace(/#.*$/,"").trim();if(!K||K.startsWith("-"))continue;let G=/^ruleset_version\s*:\s*(.+)$/.exec(K);if(G){Q.ruleset_version=G[1].trim().replace(/^["']|["']$/g,""),J=null;continue}if(/^safety\s*:\s*$/.test(K)){J="safety";continue}if(/^range\s*:\s*$/.test(K)){J="range";continue}let W=/^([A-Za-z0-9_]+)\s*:\s*\{(.+)\}\s*$/.exec(K);if(W&&J){let z=W[1],B=S8(W[2]);if(J==="safety"){let V=B.min;if(typeof V==="number")Q.safety[z]={min:V}}else{let{min:V,max:U,severity:H}=B;if(typeof V==="number"&&typeof U==="number")Q.range[z]={min:V,max:U,severity:H==="CRITICAL"?"CRITICAL":"HIGH"}}}}return Q}function S8(Z){let $={};for(let Q of Z.split(",")){let J=/^\s*([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$/.exec(Q);if(!J)continue;let X=J[1],Y=J[2].replace(/^["']|["']$/g,""),K=Number(Y);$[X]=Number.isFinite(K)&&Y!==""?K:Y}return $}function w8(Z){let $=new Map;for(let Q of Z)$.set(Q.name.toUpperCase(),Q.value);return $}function b(Z){return{check:Z.check,severity:Z.severity,value:Z.value,threshold_used:Z.threshold_used,ruleset_version:Z.ruleset_version,verdict:Z.verdict,tier:"system",source:Z.source,detail:Z.detail}}function XZ(Z,$={}){let Q=$.configDir?C8($.configDir):O$(),J=$.ruleset_version??Q.ruleset_version,X=w8(Z),Y=U$($.configDir);return[...y8(X,Q,J),...k8(X,J),...h8(X,Q,J,Y),...b8(Z,null)]}function y8(Z,$,Q){let J=[],X={kind:"rule-of-thumb"},Y=Z.get("ARMING_CHECK");if(Y===void 0)J.push(g("param.safety.arming",Q,"ARMING_CHECK absent"));else{let B=Y!==0;J.push(b({check:"param.safety.arming",severity:B?"OK":"CRITICAL",value:Y,threshold_used:$.safety.ARMING_CHECK?.min??1,verdict:B?"PASS":"FAIL",ruleset_version:Q,detail:B?"arming checks enabled":"ARMING_CHECK=0 \u2014 ALL pre-arm safety checks are disabled",source:X}))}let K=Z.get("BATT_LOW_VOLT"),G=Z.get("BATT_CRT_VOLT");if(K===void 0&&G===void 0)J.push(g("param.safety.battery_failsafe",Q,"BATT_LOW_VOLT/BATT_CRT_VOLT absent"));else{let B=(K??0)===0&&(G??0)===0;J.push(b({check:"param.safety.battery_failsafe",severity:B?"CRITICAL":"OK",value:B?0:G??K??0,threshold_used:0,verdict:B?"FAIL":"PASS",ruleset_version:Q,detail:B?"battery failsafe not configured (BATT_LOW_VOLT and BATT_CRT_VOLT both 0)":"battery failsafe configured",source:X}))}let W=Z.get("FS_THR_ENABLE");if(W===void 0)J.push(g("param.safety.radio_failsafe",Q,"FS_THR_ENABLE absent"));else{let B=W!==0;J.push(b({check:"param.safety.radio_failsafe",severity:B?"OK":"HIGH",value:W,threshold_used:$.safety.FS_THR_ENABLE?.min??1,verdict:B?"PASS":"FAIL",ruleset_version:Q,detail:B?"radio failsafe enabled":"FS_THR_ENABLE=0 \u2014 radio/throttle failsafe is OFF",source:X}))}let z=Z.get("FENCE_ENABLE");if(z===void 0)J.push(g("param.safety.fence",Q,"FENCE_ENABLE absent"));else{let B=z!==0;J.push(b({check:"param.safety.fence",severity:B?"OK":"HIGH",value:z,threshold_used:$.safety.FENCE_ENABLE?.min??1,verdict:B?"PASS":"FAIL",ruleset_version:Q,detail:B?"geofence enabled":"FENCE_ENABLE=0 \u2014 geofence disabled (advisory)",source:X}))}return J}function k8(Z,$){let Q=[],J={kind:"rule-of-thumb"},X=Z.get("BATT_CRT_VOLT"),Y=v8(Z);if(X===void 0||X===0||Y===null||Y<=0)Q.push(g("param.consistency.battery_cells",$,"BATT_CRT_VOLT and a cell-count source required"));else{let z=X/Y,B=z>=3&&z<=3.7;Q.push(b({check:"param.consistency.battery_cells",severity:B?"OK":"HIGH",value:IZ(z,3),threshold_used:3,verdict:B?"PASS":"FAIL",ruleset_version:$,detail:B?`per-cell critical voltage ${IZ(z,2)}V across ${Y}S looks sane`:`per-cell critical voltage ${IZ(z,2)}V (${Y}S) outside [3.0,3.7] \u2014 cell count vs BATT_CRT_VOLT inconsistent`,source:J}))}let K=Z.get("FRAME_CLASS"),G=Z.get("FRAME_TYPE");if(K===void 0&&G===void 0)Q.push(g("param.consistency.frame",$,"FRAME_CLASS/FRAME_TYPE absent"));else{let z=(K??0)!==0&&(G??0)!==0;Q.push(b({check:"param.consistency.frame",severity:z?"OK":"HIGH",value:K??null,threshold_used:1,verdict:z?"PASS":"FAIL",ruleset_version:$,detail:z?`frame configured (CLASS=${K}, TYPE=${G})`:`frame not fully configured (CLASS=${K??"?"}, TYPE=${G??"?"}) \u2014 must be non-zero for a copter`,source:J}))}let W=Z.get("INS_HNTCH_ENABLE");if(W===void 0)Q.push(g("param.consistency.notch_filter",$,"INS_HNTCH_ENABLE absent"));else Q.push(b({check:"param.consistency.notch_filter",severity:"OK",value:W,threshold_used:null,verdict:"PASS",ruleset_version:$,detail:W===0?"harmonic notch filter disabled (INS_HNTCH_ENABLE=0) \u2014 advisory; tune if vibration is high":"harmonic notch filter enabled",source:J}));return Q}function v8(Z){for(let Q of["BATT_CELLS","BATT_N_CELLS","BATT_CELL_COUNT"]){let J=Z.get(Q);if(J!==void 0&&J>0)return Math.round(J)}let $=Z.get("BATT_ARM_VOLT");if($!==void 0&&$>0)return Math.round($/3.6);return null}function h8(Z,$,Q,J){let X=[];for(let[Y,K]of Object.entries($.range)){let G=Z.get(Y.toUpperCase()),W=`param.range.${Y}`;if(G===void 0){X.push(g(W,Q,`${Y} absent`));continue}let z=H$(Y,J),B=z!==void 0&&(typeof z.min==="number"||typeof z.max==="number"),V=B&&typeof z.min==="number"?Math.max(K.min,z.min):K.min,U=B&&typeof z.max==="number"?Math.min(K.max,z.max):K.max,H=B&&typeof z.min==="number"?z.min:K.min,E=B&&typeof z.max==="number"?z.max:K.max,T=B?{kind:"datasheet",ref:"apm.pdef.xml"}:{kind:"tuned"},L=G>=V&&G<=U;X.push(b({check:W,severity:L?"OK":K.severity,value:G,threshold_used:L?E:G<V?H:E,verdict:L?"PASS":"FAIL",ruleset_version:Q,detail:L?`${Y}=${G} within [${H},${E}]${B?" (apm.pdef.xml)":""}`:`${Y}=${G} outside [${H},${E}]${B?" (apm.pdef.xml)":""}`,source:T}))}return X}function b8(Z,$){return[]}function g(Z,$,Q){return b({check:Z,severity:"OK",value:null,threshold_used:null,verdict:"SKIP",ruleset_version:$,detail:Q})}function IZ(Z,$){let Q=10**$;return Math.round(Z*Q)/Q}function A$(Z){if(Z.length===0)return"Parameter audit: no findings.";let $=(W)=>{if(W.verdict==="SKIP")return"\u2013";if(W.verdict==="PASS")return"\u2713";return W.severity==="CRITICAL"?"\u2717 CRIT":"\u2717 HIGH"},Q=Z.filter((W)=>W.verdict==="FAIL"),J=Z.filter((W)=>W.verdict==="PASS"),X=Z.filter((W)=>W.verdict==="SKIP"),Y=Q.filter((W)=>W.severity==="CRITICAL").length,K=[];K.push(`Parameter audit \u2014 ${Z.length} checks: ${J.length} pass, ${Q.length} fail (${Y} critical), ${X.length} skipped`);let G=[...Z].sort((W,z)=>L$(W)-L$(z));for(let W of G){let z=W.detail?` \u2014 ${W.detail}`:"";K.push(` ${$(W).padEnd(7)} ${W.check}${z}`)}return K.join(`
|
|
9
|
-
`)}function
|
|
10
|
-
`)){let X=J.trim();if(!X)continue;try{Q.push(JSON.parse(X))}catch{}}return Q}async function
|
|
8
|
+
${Q}${J}`}import{createHash as B6}from"crypto";import{open as u$,stat as V6}from"fs/promises";import{resolve as U6}from"path";function RZ(Z,$){let Q=Z.tables.PARM;if(!Q)return null;let J=Q.columns.findIndex((K)=>K.toLowerCase()==="name"),X=Q.columns.findIndex((K)=>K.toLowerCase()==="value");if(J<0||X<0)return null;let Y=$.toLowerCase();for(let K of Q.rows)if(String(K[J]).toLowerCase()===Y){let W=Number(K[X]);return Number.isFinite(W)?W:null}return null}function i0(Z){let $=Z.tables.MSG;if(!$)return"";let Q=$.columns.findIndex((J)=>/message|msg/i.test(J));if(Q<0)return"";for(let J of $.rows){let X=String(J[Q]??"");if(/Ardu(Copter|Plane|Sub|Rover)|PX4/i.test(X))return X}return""}function K$(Z){let $=RZ(Z,"Q_ENABLE");if($!==null&&$>=1||Z.tables.QUIK||Z.tables.AETR){if(Z.tables.MOTB||RZ(Z,"FRAME_CLASS")!==null||Z.tables.ARSP)return"vtol"}if($!==null&&$>=1)return"vtol";let Q=i0(Z);if(/ArduPlane/i.test(Q))return"fixedwing";if(/ArduCopter/i.test(Q))return"multirotor";if(RZ(Z,"FRAME_CLASS")!==null)return"multirotor";if(Z.tables.ARSP&&!Z.tables.MOTB)return"fixedwing";if(Z.tables.MOTB||Z.tables.RATE)return"multirotor";return"unknown"}function QZ(Z){return Z===void 0||Z==="multirotor"||Z==="vtol"||Z==="unknown"}function W$(Z){return Z==="fixedwing"||Z==="vtol"}function S(Z){return`"${Z.replace(/"/g,'""')}"`}function j(Z,$){let Q=Z.db.query($).get();if(!Q)return null;let J=Object.values(Q)[0];if(J===null||J===void 0)return null;return Number(J)}function k(Z,$){try{let Q=Z.db.query(`PRAGMA table_info(${S($)})`).all();return new Set(Q.map((J)=>J.name))}catch{return new Set}}function C(Z,$,Q){return k(Z,$).has(Q)}function z$(Z,$){for(let Q of $)if(Z.hasTable(Q))return Q;return null}function q(Z,$,Q){return{check:Z,severity:"OK",value:null,threshold_used:null,ruleset_version:$.ruleset_version,verdict:"SKIP",tier:"system",...Q?{detail:Q}:{}}}function P(Z){return{check:Z.check,severity:Z.failed?Z.failSeverity:"OK",value:Z.value,threshold_used:Z.threshold_used,ruleset_version:Z.thresholds.ruleset_version,verdict:Z.failed?"FAIL":"PASS",tier:"system",...Z.source?{source:Z.source}:{},...Z.detail?{detail:Z.detail}:{}}}var o0=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("ERR"))return q("battery",Z.thresholds);if(!C(Z,"ERR","Subsys")||!C(Z,"ERR","ECode"))return q("battery",Z.thresholds,"ERR present but Subsys/ECode columns absent");let Q=$?.err_subsys??6,J=$?.err_ecode??1,X=j(Z,`SELECT COUNT(*) FROM "ERR" WHERE "Subsys"=${Q} AND "ECode"=${J}`)??0;return P({check:"battery",thresholds:Z.thresholds,failed:X>0,failSeverity:"CRITICAL",value:X,threshold_used:0,source:$?.source,detail:X>0?`${X} battery failsafe ERR event(s) (Subsys=${Q} ECode=${J})`:void 0})},r0=(Z)=>{let $=Z.thresholds.attitude;if(!Z.hasTable("ATT"))return q("attitude",Z.thresholds);for(let K of["DesRoll","Roll","DesPitch","Pitch"])if(!C(Z,"ATT",K))return q("attitude",Z.thresholds,`ATT missing column ${K}`);let Q=$?.max_deg??5,J=j(Z,'SELECT MAX(ABS("DesRoll"-"Roll")) FROM "ATT"'),X=j(Z,'SELECT MAX(ABS("DesPitch"-"Pitch")) FROM "ATT"'),Y=Math.max(J??0,X??0);return P({check:"attitude",thresholds:Z.thresholds,failed:Y>Q,failSeverity:"HIGH",value:Y,threshold_used:Q,source:$?.source,detail:`peak roll err ${(J??0).toFixed(2)}\xB0, pitch err ${(X??0).toFixed(2)}\xB0`})},a0=(Z)=>{let $=Z.thresholds.altitude;if(!Z.hasTable("CTUN"))return q("altitude",Z.thresholds);if(!C(Z,"CTUN","DAlt")||!C(Z,"CTUN","Alt"))return q("altitude",Z.thresholds,"CTUN missing DAlt/Alt columns");let Q=$?.max_m??2,J=j(Z,'SELECT MAX(ABS("DAlt"-"Alt")) FROM "CTUN"');return P({check:"altitude",thresholds:Z.thresholds,failed:(J??0)>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source})},s0=(Z)=>{let $=Z.thresholds.hover;if(!QZ(Z.vehicleType))return q("hover",Z.thresholds,`n/a for ${Z.vehicleType} (no hover throttle)`);if(!Z.hasTable("CTUN"))return q("hover",Z.thresholds);if(!C(Z,"CTUN","ThH"))return q("hover",Z.thresholds,"CTUN missing ThH column");let Q=$?.band??[0.25,0.75],[J,X]=Q,Y=j(Z,'SELECT AVG("ThH") FROM "CTUN"'),K=Y===null?!1:Y<J||Y>X;return P({check:"hover",thresholds:Z.thresholds,failed:K,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`mean hover throttle ${(Y??0).toFixed(3)} vs band [${J}, ${X}]`})},G$=2000,t0=(Z)=>{if(!QZ(Z.vehicleType))return q("rcou",Z.thresholds,`n/a for ${Z.vehicleType} (control surfaces, not motor mix)`);if(!Z.hasTable("RCOU"))return q("rcou",Z.thresholds);let $=k(Z,"RCOU"),Q=[];for(let Y=1;Y<=8;Y++)if($.has(`C${Y}`))Q.push(`C${Y}`);if(Q.length===0)return q("rcou",Z.thresholds,"RCOU has no C1..C8 channels");let J=-1/0,X=[];for(let Y of Q){let K=j(Z,`SELECT MAX(${S(Y)}) FROM "RCOU"`);if(K===null)continue;if(K>J)J=K;if(K>=G$)X.push(`${Y}=${K}`)}if(J===-1/0)J=0;return P({check:"rcou",thresholds:Z.thresholds,failed:X.length>0,failSeverity:"HIGH",value:J,threshold_used:G$,detail:X.length>0?`motor output(s) pegged at max: ${X.join(", ")}`:void 0})},jZ=1000,PZ=2000,e0=(Z)=>{if(!Z.hasTable("RCIN"))return q("rcin",Z.thresholds);if(!C(Z,"RCIN","C3"))return q("rcin",Z.thresholds,"RCIN missing C3 column");let $=j(Z,'SELECT MIN("C3") FROM "RCIN"'),Q=j(Z,'SELECT MAX("C3") FROM "RCIN"'),J=$!==null&&$<jZ||Q!==null&&Q>PZ,X=Q;if($!==null&&$<jZ)X=$;return P({check:"rcin",thresholds:Z.thresholds,failed:J,failSeverity:"HIGH",value:X,threshold_used:PZ,detail:`RCIN C3 range [${$??"\u2014"}, ${Q??"\u2014"}] vs [${jZ}, ${PZ}]`})},Z8=(Z)=>{let $=Z.thresholds.vibration;if(!Z.hasTable("VIBE"))return q("vibration",Z.thresholds);let Q=k(Z,"VIBE"),J=["VibeX","VibeY","VibeZ"].filter((V)=>Q.has(V));if(J.length===0)return q("vibration",Z.thresholds,"VIBE has no VibeX/Y/Z columns");let X=$?.max_ms2??30,Y={},K=-1/0;for(let V of J){let H=j(Z,`SELECT MAX(${S(V)}) FROM "VIBE"`)??0;if(Y[V]=H,H>K)K=H}if(K===-1/0)K=0;let W=["Clip","Clip0","Clip1","Clip2"].filter((V)=>Q.has(V)),G=0;for(let V of W){let U=j(Z,`SELECT MAX(${S(V)}) FROM "VIBE"`);if(U!==null&&U>G)G=U}let z=W.length>0?` | clipping MAX=${G}${G>0?" (CLIP EVENTS)":""}`:"",B=$8(Y,K,G);return P({check:"vibration",thresholds:Z.thresholds,failed:K>=X,failSeverity:"HIGH",value:K,threshold_used:X,source:$?.source,detail:`peak vibe ${K} m/s\xB2 (axes ${J.join("/")})${z}${B}`})};function $8(Z,$,Q){let J=Object.values(Z);if($<=0||J.length===0)return"";let X=[...J].sort((z,B)=>B-z),Y=X[0],K=X.length>1?X[1]:0;if(X.length>=2&&K>0&&Y>=2*K)return" | cause: likely sensor/mounting alignment (one axis dominates)";let G=X.length>=2&&Y<=1.5*(X[X.length-1]||Y);if(Q>0||G)return" | cause: likely mechanical degradation (props/bearings/frame)";return""}var J8=(Z)=>{let $=Z.thresholds.mag;if(!Z.hasTable("MAG"))return q("mag",Z.thresholds);if(!C(Z,"MAG","Health"))return q("mag",Z.thresholds,"MAG missing Health column");let Q=$?.min??1,J=j(Z,'SELECT MIN("Health") FROM "MAG"');return P({check:"mag",thresholds:Z.thresholds,failed:J!==null&&J<Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source})},Q8=(Z)=>{let $=Z.thresholds.ekf,Q=z$(Z,["XKF3","NKF3"]);if(!Q)return q("ekf",Z.thresholds);let J=k(Z,Q),X=["IVN","IVE","IVD","IVT","IPN","IPE","IPD"].filter((W)=>J.has(W));if(X.length===0)return q("ekf",Z.thresholds,`${Q} has no IVN/IVE/IVD/IVT/IPN/IPE/IPD columns`);let Y=$?.max_ratio??0.5,K=-1/0;for(let W of X){let G=j(Z,`SELECT MAX(ABS(${S(W)})) FROM ${S(Q)}`);if(G!==null&&G>K)K=G}if(K===-1/0)K=0;return P({check:"ekf",thresholds:Z.thresholds,failed:K>Y,failSeverity:"HIGH",value:K,threshold_used:Y,source:$?.source,detail:`peak innovation ${K} (${Q}: ${X.join("/")})`})},X8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("min_cell_voltage",Z.thresholds);if(!C(Z,"BAT","Volt"))return q("min_cell_voltage",Z.thresholds,"BAT missing Volt column");let Q=$?.min_cell_v??3.3,J=j(Z,'SELECT MAX("Volt") FROM "BAT"'),X=j(Z,'SELECT MIN("Volt") FROM "BAT"');if(J===null||X===null||J<=0)return q("min_cell_voltage",Z.thresholds,"BAT.Volt has no usable values");let Y=Math.max(1,Math.round(J/4.2)),K=X/Y;return P({check:"min_cell_voltage",thresholds:Z.thresholds,failed:K<Q,failSeverity:"CRITICAL",value:K,threshold_used:Q,source:$?.source,detail:`${Y}S pack (MAX pack ${J.toFixed(2)}V), min cell ${K.toFixed(3)}V vs floor ${Q}V`})},Y8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("voltage_sag",Z.thresholds);if(!C(Z,"BAT","VoltR")||!C(Z,"BAT","Volt"))return q("voltage_sag",Z.thresholds,"BAT missing VoltR/Volt columns");let Q=$?.max_sag_v??2,J=j(Z,'SELECT MAX("VoltR"-"Volt") FROM "BAT"');return P({check:"voltage_sag",thresholds:Z.thresholds,failed:J!==null&&J>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source,detail:`peak sag ${(J??0).toFixed(2)}V (VoltR-Volt) vs ${Q}V`})},K8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("over_current",Z.thresholds);if(!C(Z,"BAT","Curr"))return q("over_current",Z.thresholds,"BAT missing Curr column");let Q=$?.max_current_a??200,J=j(Z,'SELECT MAX("Curr") FROM "BAT"');return P({check:"over_current",thresholds:Z.thresholds,failed:J!==null&&J>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source,detail:`peak current ${(J??0).toFixed(1)}A vs ${Q}A`})},W8=(Z)=>{let $=Z.thresholds.battery;if(!Z.hasTable("BAT"))return q("internal_resistance",Z.thresholds);if(!C(Z,"BAT","Res"))return q("internal_resistance",Z.thresholds,"BAT missing Res column");let Q=j(Z,'SELECT MAX("Res") FROM "BAT"');if(Q===null||Q===0)return q("internal_resistance",Z.thresholds,"BAT.Res all zero/absent (not reported)");let J=$?.res_min_current_a??5,Y=C(Z,"BAT","Curr")?j(Z,`SELECT MAX("Res") FROM "BAT" WHERE "Curr" >= ${J}`):Q;if(Y===null)return q("internal_resistance",Z.thresholds,`no BAT samples at Curr \u2265 ${J} A \u2014 Res only valid under load (max raw ${Q.toFixed(3)}\u03A9 is a low-current artifact)`);let K=$?.max_res_ohm??0.03;return P({check:"internal_resistance",thresholds:Z.thresholds,failed:Y>K,failSeverity:"HIGH",value:Y,threshold_used:K,source:$?.source,detail:`peak internal resistance ${Y.toFixed(4)}\u03A9 (at Curr \u2265 ${J} A) vs ${K}\u03A9`})},G8=(Z)=>{let $=Z.thresholds.power;if(!Z.hasTable("POWR"))return q("board_brownout",Z.thresholds);if(!C(Z,"POWR","Vcc"))return q("board_brownout",Z.thresholds,"POWR missing Vcc column");let Q=$?.min_vcc_v??4.5,J=j(Z,'SELECT MIN("Vcc") FROM "POWR"');return P({check:"board_brownout",thresholds:Z.thresholds,failed:J!==null&&J<Q,failSeverity:"CRITICAL",value:J,threshold_used:Q,source:$?.source,detail:`min board Vcc ${(J??0).toFixed(3)}V vs floor ${Q}V`})},z8=(Z)=>{let $=Z.thresholds.rate;if(!Z.hasTable("RATE"))return q("rate_tracking",Z.thresholds);let Q=k(Z,"RATE"),X=[["RDes","R"],["PDes","P"],["YDes","Y"]].filter(([G,z])=>Q.has(G)&&Q.has(z));if(X.length===0)return q("rate_tracking",Z.thresholds,"RATE has no RDes/R, PDes/P, YDes/Y pairs");let Y=$?.max_err_dps??50,K=-1/0,W="";for(let[G,z]of X){let B=j(Z,`SELECT MAX(ABS(${S(G)}-${S(z)})) FROM "RATE"`);if(B!==null&&B>K)K=B,W=G.replace("Des","")}if(K===-1/0)K=0;return P({check:"rate_tracking",thresholds:Z.thresholds,failed:K>Y,failSeverity:"HIGH",value:K,threshold_used:Y,source:$?.source,detail:`peak rate-tracking error ${K.toFixed(2)} deg/s (axis ${W||"?"}) vs ${Y} deg/s`})},B8=(Z)=>{let $=Z.thresholds.gps;if(!Z.hasTable("GPA"))return q("gps_accuracy",Z.thresholds);if(!C(Z,"GPA","HAcc"))return q("gps_accuracy",Z.thresholds,"GPA missing HAcc column");let Q=$?.hacc_max_m??5,J=j(Z,'SELECT MAX("HAcc") FROM "GPA"');return P({check:"gps_accuracy",thresholds:Z.thresholds,failed:J!==null&&J>Q,failSeverity:"HIGH",value:J,threshold_used:Q,source:$?.source,detail:`peak horizontal accuracy ${(J??0).toFixed(2)}m vs ${Q}m`})},V8=(Z)=>{let $=Z.thresholds.ekf,Q=z$(Z,["XKF4","NKF4"]);if(!Q)return q("ekf_variance",Z.thresholds);let J=k(Z,Q),X=["SV","SP","SH","SM"].filter((B)=>J.has(B));if(X.length===0)return q("ekf_variance",Z.thresholds,`${Q} has no SV/SP/SH/SM columns`);let Y=$?.max_variance??1,K=-1/0,W="";for(let B of X){let V=j(Z,`SELECT MAX(${S(B)}) FROM ${S(Q)}`);if(V!==null&&V>K)K=V,W=B}if(K===-1/0)K=0;let G=J.has("FS")?j(Z,`SELECT MAX("FS") FROM ${S(Q)}`):null,z=G!==null&&G>0?` | EKF failsafe set (FS MAX=${G})`:"";return P({check:"ekf_variance",thresholds:Z.thresholds,failed:K>Y,failSeverity:"HIGH",value:K,threshold_used:Y,source:$?.source,detail:`peak EKF variance ${K.toFixed(3)} (${Q}: ${W||X.join("/")}) vs ${Y}${z}`})},U8=(Z)=>{let $=Z.thresholds.mag;if(!Z.hasTable("MAG"))return q("mag_offsets",Z.thresholds);let Q=k(Z,"MAG"),J=["OfsX","OfsY","OfsZ"].filter((W)=>Q.has(W));if(J.length===0)return q("mag_offsets",Z.thresholds,"MAG has no OfsX/OfsY/OfsZ columns");let X=$?.max_offset??600,Y=-1/0,K="";for(let W of J){let G=j(Z,`SELECT MAX(ABS(${S(W)})) FROM "MAG"`);if(G!==null&&G>Y)Y=G,K=W}if(Y===-1/0)Y=0;return P({check:"mag_offsets",thresholds:Z.thresholds,failed:Y>X,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`peak mag offset ${Y.toFixed(0)} (${K||J.join("/")}) vs ${X}`})},H8=(Z)=>{let $=Z.thresholds.esc;if(!Z.hasTable("ESC"))return q("esc_health",Z.thresholds);let Q=k(Z,"ESC"),J=["Temp","MotTemp"].filter((V)=>Q.has(V));if(J.length===0&&!Q.has("Err"))return q("esc_health",Z.thresholds,"ESC has no Temp/MotTemp/Err columns");let X=$?.max_temp_c??90,Y=-1/0,K="";for(let V of J){let U=j(Z,`SELECT MAX(${S(V)}) FROM "ESC"`);if(U!==null&&U>Y)Y=U,K=V}if(Y===-1/0)Y=0;let W=Q.has("Err")?j(Z,'SELECT MAX("Err") FROM "ESC"')??0:0,G=J.length>0&&Y>X,z=W>0,B=z?` | ESC Err MAX=${W}`:"";return P({check:"esc_health",thresholds:Z.thresholds,failed:G||z,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`peak ESC temp ${Y.toFixed(1)}\xB0C (${K||"n/a"}) vs ${X}\xB0C${B}`})},L8=(Z)=>{if(!QZ(Z.vehicleType))return q("thrust_margin",Z.thresholds,`n/a for ${Z.vehicleType} (lift/T:W is a rotorcraft metric)`);if(!Z.hasTable("MOTB"))return q("thrust_margin",Z.thresholds);let $=k(Z,"MOTB");if(!$.has("FailFlags")&&!($.has("ThrOut")&&$.has("LiftMax")))return q("thrust_margin",Z.thresholds,"MOTB missing FailFlags and ThrOut/LiftMax columns");let Q=$.has("FailFlags")?j(Z,'SELECT MAX("FailFlags") FROM "MOTB"')??0:0,J=null;if($.has("ThrOut")&&$.has("LiftMax")){let W=j(Z,'SELECT AVG("ThrOut") FROM "MOTB"'),G=j(Z,'SELECT AVG("LiftMax") FROM "MOTB"');if(W!==null&&G!==null&&G>0)J=W/G}let X=J!==null&&J>0.5,Y=Q>0,K=`MOTB FailFlags MAX=${Q}`+(J!==null?`, mean ThrOut/LiftMax ${J.toFixed(2)} (low-margin > 0.5)`:"")+(Y?" \u2014 THRUST-LOSS flagged":X?" \u2014 low thrust margin":"");return P({check:"thrust_margin",thresholds:Z.thresholds,failed:Y||X,failSeverity:"HIGH",value:J,threshold_used:0.5,detail:K})},O8=(Z)=>{let $=Z.thresholds.imu;if(!Z.hasTable("IMU"))return q("imu_health",Z.thresholds);let Q=k(Z,"IMU"),J=["EG","EA"].filter((W)=>Q.has(W));if(J.length===0)return q("imu_health",Z.thresholds,"IMU has no EG/EA error-count columns");let X=$?.max_err??0,Y=-1/0,K="";for(let W of J){let G=j(Z,`SELECT MAX(${S(W)}) FROM "IMU"`);if(G!==null&&G>Y)Y=G,K=W}if(Y===-1/0)Y=0;return P({check:"imu_health",thresholds:Z.thresholds,failed:Y>X,failSeverity:"HIGH",value:Y,threshold_used:X,source:$?.source,detail:`max IMU error count ${Y} (${K||J.join("/")}) vs ${X}`})},A8=(Z)=>{let $=Z.thresholds.imu;if(!Z.hasTable("IMU"))return q("imu_consistency",Z.thresholds);let Q=k(Z,"IMU");if(!Q.has("I"))return q("imu_consistency",Z.thresholds,"IMU has no instance column I");let J=["AccX","AccY","AccZ"].filter((N)=>Q.has(N)),X=["GyrX","GyrY","GyrZ"].filter((N)=>Q.has(N));if(J.length===0&&X.length===0)return q("imu_consistency",Z.thresholds,"IMU has no Acc*/Gyr* columns");let Y=[...J,...X].map((N)=>`AVG(${S(N)}) ${S(N)}`).join(", "),K=Z.db.query(`SELECT "I" AS "I", ${Y} FROM "IMU" GROUP BY "I"`).all();if(K.length<2)return q("imu_consistency",Z.thresholds,`only ${K.length} IMU instance \u2014 nothing to cross-check`);let W=(N)=>{let _=K.map((R)=>Number(R[N])).filter((R)=>Number.isFinite(R));return _.length>=2?Math.max(..._)-Math.min(..._):0},G=0,z="";for(let N of J){let _=W(N);if(_>G)G=_,z=N}let B=0,V="";for(let N of X){let _=W(N);if(_>B)B=_,V=N}let U=$?.accel_consistency_max??0.7,H=$?.gyro_consistency_max??0.3,F=G>U,T=B>H,L=F||T,A=G/U>=B/H?G:B,O=G/U>=B/H?U:H,E=L?`INCONSISTENT \u2014 ${F?`accel ${z} spread ${G.toFixed(3)} m/s\xB2 > ${U}`:""}${F&&T?"; ":""}${T?`gyro ${V} spread ${B.toFixed(3)} rad/s > ${H}`:""}`:`IMUs consistent \u2014 max inter-instance bias spread: accel ${G.toFixed(3)} m/s\xB2 (<${U}), gyro ${B.toFixed(3)} rad/s (<${H}); relative differences between instances are within healthy tolerance`;return P({check:"imu_consistency",thresholds:Z.thresholds,failed:L,failSeverity:"HIGH",value:A,threshold_used:O,source:{kind:"rule-of-thumb",ref:"IMU inconsistency tolerance (~ArduPilot arming limits)"},detail:E})},q8=(Z)=>{if(!Z.hasTable("ERR"))return q("err_events",Z.thresholds);if(!C(Z,"ERR","Subsys")||!C(Z,"ERR","ECode"))return q("err_events",Z.thresholds,"ERR present but Subsys/ECode columns absent");let $=Z.thresholds.battery?.err_subsys??6,Q=Z.db.query(`SELECT "Subsys" AS subsys, "ECode" AS ecode, COUNT(*) AS n FROM "ERR" WHERE "ECode" <> 0 AND "Subsys" <> ${$} GROUP BY "Subsys", "ECode" ORDER BY "Subsys", "ECode"`).all(),J=0,X=[];for(let Y of Q)J+=Number(Y.n),X.push(`Subsys=${Y.subsys} ECode=${Y.ecode} \xD7${Y.n}`);return P({check:"err_events",thresholds:Z.thresholds,failed:J>0,failSeverity:"HIGH",value:J,threshold_used:0,detail:J>0?`${J} non-battery ERR event(s): ${X.join(", ")}`:"no non-battery ERR events"})},F8=new Set([6,9]),E8=(Z)=>{if(!Z.hasTable("MODE"))return q("mode_changes",Z.thresholds);if(!C(Z,"MODE","ModeNum"))return q("mode_changes",Z.thresholds,"MODE missing ModeNum column");let $=Z.db.query('SELECT "ModeNum" AS modenum FROM "MODE"').all(),Q=$.length,J=Q>0?Q-1:0,X=new Map,Y=!1;for(let W of $){let G=Number(W.modenum);if(X.set(G,(X.get(G)??0)+1),F8.has(G))Y=!0}let K=[...X.entries()].sort((W,G)=>W[0]-G[0]).map(([W,G])=>`Mode${W}\xD7${G}`).join(", ");return P({check:"mode_changes",thresholds:Z.thresholds,failed:Y,failSeverity:"HIGH",value:J,threshold_used:0,detail:`${J} mode change(s); timeline: ${K||"\u2014"}`+(Y?" \u2014 emergency mode (RTL/LAND) entered":"")})},T8=(Z)=>{let $=Z.thresholds.airspeed;if(!W$(Z.vehicleType))return q("airspeed",Z.thresholds,`n/a for ${Z.vehicleType} (no fixed-wing airspeed)`);if(!Z.hasTable("ARSP"))return q("airspeed",Z.thresholds,"no ARSP (airspeed) data");if(!C(Z,"ARSP","Airspeed"))return q("airspeed",Z.thresholds,"ARSP missing Airspeed column");let Q=$?.stall_min_ms??8,J={kind:"rule-of-thumb",ref:"default stall floor"};if(Z.hasTable("PARM")){let Y=j(Z,`SELECT "Value" FROM "PARM" WHERE "Name"='ARSPD_FBW_MIN' LIMIT 1`);if(Y!==null&&Y>0)Q=Y,J={kind:"datasheet",ref:"ARSPD_FBW_MIN"}}let X=j(Z,'SELECT MIN("Airspeed") FROM "ARSP" WHERE "Airspeed" > 2');if(X===null)return q("airspeed",Z.thresholds,"no airborne airspeed samples");return P({check:"airspeed",thresholds:Z.thresholds,failed:X<Q,failSeverity:"HIGH",value:X,threshold_used:Q,source:J,detail:`min in-flight airspeed ${X.toFixed(1)} m/s vs stall floor ${Q} m/s`})},N8=[{id:"battery",run:o0},{id:"min_cell_voltage",run:X8},{id:"voltage_sag",run:Y8},{id:"over_current",run:K8},{id:"internal_resistance",run:W8},{id:"board_brownout",run:G8},{id:"attitude",run:r0},{id:"rate_tracking",run:z8},{id:"altitude",run:a0},{id:"hover",run:s0},{id:"rcou",run:t0},{id:"rcin",run:e0},{id:"thrust_margin",run:L8},{id:"vibration",run:Z8},{id:"imu_health",run:O8},{id:"imu_consistency",run:A8},{id:"mag",run:J8},{id:"mag_offsets",run:U8},{id:"gps_accuracy",run:B8},{id:"ekf",run:Q8},{id:"ekf_variance",run:V8},{id:"esc_health",run:H8},{id:"airspeed",run:T8},{id:"err_events",run:q8},{id:"mode_changes",run:E8}];function B$(Z,$,Q){let J=new Set;try{let K=Z.query("SELECT name FROM sqlite_master WHERE type='table'").all();for(let W of K)J.add(W.name)}catch{}let X={db:Z,thresholds:$,hasTable:(K)=>J.has(K),vehicleType:Q?.vehicleType},Y=[];for(let{id:K,run:W}of N8)try{Y.push(W(X))}catch(G){let z=G instanceof Error?G.message:String(G);Y.push(q(K,$,`check error: ${z}`))}return Y}import{existsSync as I8,readFileSync as C8}from"fs";import{join as M8}from"path";import{existsSync as _8,readFileSync as D8}from"fs";import{dirname as R8,join as IZ}from"path";import{fileURLToPath as j8}from"url";var V$=R8(j8(import.meta.url)),P8=[IZ(V$,"..","data","apm-params.json"),IZ(V$,"..","..","data","apm-params.json")];function H$(Z){if(Z){let $=IZ(Z,".hydron","apm-params.json"),Q=U$($);if(Q&&Q.size>0)return Q}for(let $ of P8){let Q=U$($);if(Q)return Q}return new Map}function L$(Z,$){return $.get(Z.toUpperCase())}function U$(Z){if(!_8(Z))return;try{let $=D8(Z,"utf8"),Q=JSON.parse($);if(!Q||typeof Q!=="object")return;let J=new Map;for(let[X,Y]of Object.entries(Q)){if(!X||X.startsWith("_")||typeof Y!=="object"||Y===null)continue;let K={name:X};if(typeof Y.min==="number"&&Number.isFinite(Y.min))K.min=Y.min;if(typeof Y.max==="number"&&Number.isFinite(Y.max))K.max=Y.max;if(typeof Y.units==="string"&&Y.units)K.units=Y.units;J.set(X.toUpperCase(),K)}return J}catch{return}}function A$(){return{ruleset_version:"v1",safety:{ARMING_CHECK:{min:1},FS_THR_ENABLE:{min:1},FENCE_ENABLE:{min:1}},range:{INS_GYRO_FILTER:{min:5,max:80,severity:"HIGH"},ATC_RAT_RLL_P:{min:0,max:0.5,severity:"HIGH"},ATC_RAT_PIT_P:{min:0,max:0.5,severity:"HIGH"},ATC_RAT_YAW_P:{min:0,max:1,severity:"HIGH"},MOT_SPIN_ARM:{min:0,max:0.2,severity:"HIGH"},MOT_SPIN_MIN:{min:0,max:0.3,severity:"HIGH"},RTL_ALT:{min:200,max:1e4,severity:"HIGH"},WPNAV_SPEED:{min:50,max:2000,severity:"HIGH"}}}}function S8(Z){let $=A$(),Q=M8(Z,".hydron","param-rules.yaml");if(!I8(Q))return $;try{let J=C8(Q,"utf8");return w8($,J)}catch{return $}}function w8(Z,$){let Q={ruleset_version:Z.ruleset_version,safety:{...Z.safety},range:{...Z.range}},J=null;for(let X of $.split(/\r?\n/)){let K=X.replace(/#.*$/,"").trim();if(!K||K.startsWith("-"))continue;let W=/^ruleset_version\s*:\s*(.+)$/.exec(K);if(W){Q.ruleset_version=W[1].trim().replace(/^["']|["']$/g,""),J=null;continue}if(/^safety\s*:\s*$/.test(K)){J="safety";continue}if(/^range\s*:\s*$/.test(K)){J="range";continue}let G=/^([A-Za-z0-9_]+)\s*:\s*\{(.+)\}\s*$/.exec(K);if(G&&J){let z=G[1],B=y8(G[2]);if(J==="safety"){let V=B.min;if(typeof V==="number")Q.safety[z]={min:V}}else{let{min:V,max:U,severity:H}=B;if(typeof V==="number"&&typeof U==="number")Q.range[z]={min:V,max:U,severity:H==="CRITICAL"?"CRITICAL":"HIGH"}}}}return Q}function y8(Z){let $={};for(let Q of Z.split(",")){let J=/^\s*([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$/.exec(Q);if(!J)continue;let X=J[1],Y=J[2].replace(/^["']|["']$/g,""),K=Number(Y);$[X]=Number.isFinite(K)&&Y!==""?K:Y}return $}function k8(Z){let $=new Map;for(let Q of Z)$.set(Q.name.toUpperCase(),Q.value);return $}function b(Z){return{check:Z.check,severity:Z.severity,value:Z.value,threshold_used:Z.threshold_used,ruleset_version:Z.ruleset_version,verdict:Z.verdict,tier:"system",source:Z.source,detail:Z.detail}}function XZ(Z,$={}){let Q=$.configDir?S8($.configDir):A$(),J=$.ruleset_version??Q.ruleset_version,X=k8(Z),Y=H$($.configDir);return[...v8(X,Q,J),...h8(X,J),...f8(X,Q,J,Y),...g8(Z,null)]}function v8(Z,$,Q){let J=[],X={kind:"rule-of-thumb"},Y=Z.get("ARMING_CHECK");if(Y===void 0)J.push(u("param.safety.arming",Q,"ARMING_CHECK absent"));else{let B=Y!==0;J.push(b({check:"param.safety.arming",severity:B?"OK":"CRITICAL",value:Y,threshold_used:$.safety.ARMING_CHECK?.min??1,verdict:B?"PASS":"FAIL",ruleset_version:Q,detail:B?"arming checks enabled":"ARMING_CHECK=0 \u2014 ALL pre-arm safety checks are disabled",source:X}))}let K=Z.get("BATT_LOW_VOLT"),W=Z.get("BATT_CRT_VOLT");if(K===void 0&&W===void 0)J.push(u("param.safety.battery_failsafe",Q,"BATT_LOW_VOLT/BATT_CRT_VOLT absent"));else{let B=(K??0)===0&&(W??0)===0;J.push(b({check:"param.safety.battery_failsafe",severity:B?"CRITICAL":"OK",value:B?0:W??K??0,threshold_used:0,verdict:B?"FAIL":"PASS",ruleset_version:Q,detail:B?"battery failsafe not configured (BATT_LOW_VOLT and BATT_CRT_VOLT both 0)":"battery failsafe configured",source:X}))}let G=Z.get("FS_THR_ENABLE");if(G===void 0)J.push(u("param.safety.radio_failsafe",Q,"FS_THR_ENABLE absent"));else{let B=G!==0;J.push(b({check:"param.safety.radio_failsafe",severity:B?"OK":"HIGH",value:G,threshold_used:$.safety.FS_THR_ENABLE?.min??1,verdict:B?"PASS":"FAIL",ruleset_version:Q,detail:B?"radio failsafe enabled":"FS_THR_ENABLE=0 \u2014 radio/throttle failsafe is OFF",source:X}))}let z=Z.get("FENCE_ENABLE");if(z===void 0)J.push(u("param.safety.fence",Q,"FENCE_ENABLE absent"));else{let B=z!==0;J.push(b({check:"param.safety.fence",severity:B?"OK":"HIGH",value:z,threshold_used:$.safety.FENCE_ENABLE?.min??1,verdict:B?"PASS":"FAIL",ruleset_version:Q,detail:B?"geofence enabled":"FENCE_ENABLE=0 \u2014 geofence disabled (advisory)",source:X}))}return J}function h8(Z,$){let Q=[],J={kind:"rule-of-thumb"},X=Z.get("BATT_CRT_VOLT"),Y=b8(Z);if(X===void 0||X===0||Y===null||Y<=0)Q.push(u("param.consistency.battery_cells",$,"BATT_CRT_VOLT and a cell-count source required"));else{let z=X/Y,B=z>=3&&z<=3.7;Q.push(b({check:"param.consistency.battery_cells",severity:B?"OK":"HIGH",value:CZ(z,3),threshold_used:3,verdict:B?"PASS":"FAIL",ruleset_version:$,detail:B?`per-cell critical voltage ${CZ(z,2)}V across ${Y}S looks sane`:`per-cell critical voltage ${CZ(z,2)}V (${Y}S) outside [3.0,3.7] \u2014 cell count vs BATT_CRT_VOLT inconsistent`,source:J}))}let K=Z.get("FRAME_CLASS"),W=Z.get("FRAME_TYPE");if(K===void 0&&W===void 0)Q.push(u("param.consistency.frame",$,"FRAME_CLASS/FRAME_TYPE absent"));else{let z=(K??0)!==0&&(W??0)!==0;Q.push(b({check:"param.consistency.frame",severity:z?"OK":"HIGH",value:K??null,threshold_used:1,verdict:z?"PASS":"FAIL",ruleset_version:$,detail:z?`frame configured (CLASS=${K}, TYPE=${W})`:`frame not fully configured (CLASS=${K??"?"}, TYPE=${W??"?"}) \u2014 must be non-zero for a copter`,source:J}))}let G=Z.get("INS_HNTCH_ENABLE");if(G===void 0)Q.push(u("param.consistency.notch_filter",$,"INS_HNTCH_ENABLE absent"));else Q.push(b({check:"param.consistency.notch_filter",severity:"OK",value:G,threshold_used:null,verdict:"PASS",ruleset_version:$,detail:G===0?"harmonic notch filter disabled (INS_HNTCH_ENABLE=0) \u2014 advisory; tune if vibration is high":"harmonic notch filter enabled",source:J}));return Q}function b8(Z){for(let Q of["BATT_CELLS","BATT_N_CELLS","BATT_CELL_COUNT"]){let J=Z.get(Q);if(J!==void 0&&J>0)return Math.round(J)}let $=Z.get("BATT_ARM_VOLT");if($!==void 0&&$>0)return Math.round($/3.6);return null}function f8(Z,$,Q,J){let X=[];for(let[Y,K]of Object.entries($.range)){let W=Z.get(Y.toUpperCase()),G=`param.range.${Y}`;if(W===void 0){X.push(u(G,Q,`${Y} absent`));continue}let z=L$(Y,J),B=z!==void 0&&(typeof z.min==="number"||typeof z.max==="number"),V=B&&typeof z.min==="number"?Math.max(K.min,z.min):K.min,U=B&&typeof z.max==="number"?Math.min(K.max,z.max):K.max,H=B&&typeof z.min==="number"?z.min:K.min,F=B&&typeof z.max==="number"?z.max:K.max,T=B?{kind:"datasheet",ref:"apm.pdef.xml"}:{kind:"tuned"},L=W>=V&&W<=U;X.push(b({check:G,severity:L?"OK":K.severity,value:W,threshold_used:L?F:W<V?H:F,verdict:L?"PASS":"FAIL",ruleset_version:Q,detail:L?`${Y}=${W} within [${H},${F}]${B?" (apm.pdef.xml)":""}`:`${Y}=${W} outside [${H},${F}]${B?" (apm.pdef.xml)":""}`,source:T}))}return X}function g8(Z,$){return[]}function u(Z,$,Q){return b({check:Z,severity:"OK",value:null,threshold_used:null,verdict:"SKIP",ruleset_version:$,detail:Q})}function CZ(Z,$){let Q=10**$;return Math.round(Z*Q)/Q}function q$(Z){if(Z.length===0)return"Parameter audit: no findings.";let $=(G)=>{if(G.verdict==="SKIP")return"\u2013";if(G.verdict==="PASS")return"\u2713";return G.severity==="CRITICAL"?"\u2717 CRIT":"\u2717 HIGH"},Q=Z.filter((G)=>G.verdict==="FAIL"),J=Z.filter((G)=>G.verdict==="PASS"),X=Z.filter((G)=>G.verdict==="SKIP"),Y=Q.filter((G)=>G.severity==="CRITICAL").length,K=[];K.push(`Parameter audit \u2014 ${Z.length} checks: ${J.length} pass, ${Q.length} fail (${Y} critical), ${X.length} skipped`);let W=[...Z].sort((G,z)=>O$(G)-O$(z));for(let G of W){let z=G.detail?` \u2014 ${G.detail}`:"";K.push(` ${$(G).padEnd(7)} ${G.check}${z}`)}return K.join(`
|
|
9
|
+
`)}function O$(Z){if(Z.verdict==="FAIL")return Z.severity==="CRITICAL"?0:1;if(Z.verdict==="PASS")return 2;return 3}import{existsSync as u8,readFileSync as x8}from"fs";import{join as m8}from"path";function d8(){return{ruleset_version:"v1",vibration:{field:"VibeX|VibeY|VibeZ",max_ms2:30,source:{kind:"rule-of-thumb"}},clipping:{field:"Clip",max:0},battery:{err_subsys:6,err_ecode:1,min_cell_v:3.3,max_sag_v:2,max_current_a:200,max_res_ohm:0.03,res_min_current_a:5,source:{kind:"rule-of-thumb"}},attitude:{fields:"DesRoll-Roll,DesPitch-Pitch",max_deg:5,window_samples:10},altitude:{field:"DAlt-Alt",max_m:2},hover:{field:"ThH",band:[0.25,0.75]},ekf:{fields:"IVN,IVE,IVD",max_ratio:0.5,max_variance:1},mag:{field:"Health",min:1,late_align_s:30,max_offset:600},gps:{field:"NSats",min:6,hdop_max:2,hacc_max_m:5},power:{min_vcc_v:4.5,source:{kind:"rule-of-thumb"}},rate:{max_err_dps:50,source:{kind:"rule-of-thumb"}},esc:{max_temp_c:90,source:{kind:"rule-of-thumb"}},imu:{max_err:0,accel_consistency_max:0.7,gyro_consistency_max:0.3,source:{kind:"rule-of-thumb"}},airspeed:{stall_min_ms:8,source:{kind:"rule-of-thumb"}}}}function E$(Z){let $=d8(),Q=m8(Z,".hydron","uav-thresholds.yaml");if(!u8(Q))return $;try{let J=p8(x8(Q,"utf8"));return T$($,J)}catch{return $}}function T$(Z,$){let Q={...Z};for(let[J,X]of Object.entries($)){let Y=Q[J];if(X&&typeof X==="object"&&!Array.isArray(X)&&Y&&typeof Y==="object"&&!Array.isArray(Y))Q[J]=T$(Y,X);else Q[J]=X}return Q}function F$(Z){let $=!1,Q=!1;for(let J=0;J<Z.length;J++){let X=Z[J];if(X==="'"&&!Q)$=!$;else if(X==='"'&&!$)Q=!Q;else if(X==="#"&&!$&&!Q)return Z.slice(0,J)}return Z}function N$(Z,$){let Q=[],J=0,X=!1,Y=!1,K=0;for(let W=0;W<Z.length;W++){let G=Z[W];if(G==="'"&&!Y)X=!X;else if(G==='"'&&!X)Y=!Y;else if(!X&&!Y){if(G==="["||G==="{")J++;else if(G==="]"||G==="}")J--;else if(G===$&&J===0)Q.push(Z.slice(K,W)),K=W+1}}return Q.push(Z.slice(K)),Q}function YZ(Z){let $=Z.trim();if($==="")return"";if(/^-?\d+(\.\d+)?$/.test($))return Number($);if($==="true")return!0;if($==="false")return!1;if($.startsWith("[")&&$.endsWith("]")){let Q=$.slice(1,-1).trim();return Q?N$(Q,",").map(YZ):[]}if($.startsWith("{")&&$.endsWith("}"))return _$($);return $.replace(/^["']|["']$/g,"")}function _$(Z){let $=Z.trim().replace(/^\{/,"").replace(/\}$/,""),Q={};for(let J of N$($,",")){let X=J.indexOf(":");if(X<0)continue;let Y=J.slice(0,X).trim().replace(/^["']|["']$/g,"");if(Y)Q[Y]=YZ(J.slice(X+1))}return Q}function p8(Z){let $={},Q=Z.split(/\r?\n/),J=0;while(J<Q.length){let X=F$(Q[J]);if(J++,!X.trim()||/^\s/.test(X))continue;let Y=/^([A-Za-z0-9_]+):\s*(.*)$/.exec(X);if(!Y)continue;let K=Y[1],W=Y[2].trim();if(W.startsWith("{"))$[K]=_$(W);else if(W!=="")$[K]=YZ(W);else{let G={};while(J<Q.length){let z=F$(Q[J]);if(!z.trim()){J++;continue}if(!/^\s/.test(z))break;J++;let B=/^\s+([A-Za-z0-9_]+):\s*(.*)$/.exec(z);if(B&&B[2].trim()!=="")G[B[1]]=YZ(B[2])}$[K]=G}}return $}function I(Z,$){let Q=$.toLowerCase();return Z.columns.findIndex((J)=>J.toLowerCase()===Q)}function h(Z){if(typeof Z==="number")return Number.isFinite(Z)?Z:null;if(typeof Z==="bigint")return Number(Z);if(typeof Z==="string"){let $=Number(Z);return Number.isFinite($)?$:null}return null}function l8(Z){if(typeof Z==="bigint")return Z;if(typeof Z==="number"&&Number.isFinite(Z))return BigInt(Math.trunc(Z));if(typeof Z==="string"&&/^-?\d+$/.test(Z.trim()))try{return BigInt(Z.trim())}catch{return null}return null}function d(Z){if(Z===null||Z===void 0)return null;if(typeof Z==="string")return Z;return String(Z)}function KZ(Z){return`"${Z.replace(/"/g,'""')}"`}function yZ(Z,$){try{return!!Z.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get($)}catch{return!1}}function kZ(Z,$){try{let Q=Z.query(`PRAGMA table_info(${KZ($)})`).all();return new Set(Q.map((J)=>J.name))}catch{return new Set}}function WZ(Z,$){try{let Q=Z.query($).get();if(!Q)return null;let J=Object.values(Q)[0];if(J===null||J===void 0)return null;let X=typeof J==="bigint"?Number(J):Number(J);return Number.isFinite(X)?X:null}catch{return null}}function n8(Z){for(let $ of["ATT","GPS","VIBE","CTUN","IMU"])if(yZ(Z,$)&&kZ(Z,$).has("TimeUS")){let Q=WZ(Z,`SELECT COUNT(*) AS c FROM ${KZ($)}`);if(Q===null||Q<=0)continue;let J=WZ(Z,`SELECT (MAX("TimeUS") - MIN("TimeUS")) AS d FROM ${KZ($)}`);if(J!==null)return J/1e6}return null}function c8(Z){if(!yZ(Z,"VIBE"))return null;let $=kZ(Z,"VIBE"),Q=["VibeX","VibeY","VibeZ"].filter((X)=>$.has(X));if(Q.length===0)return null;let J=Q.map((X)=>`SELECT MAX(${KZ(X)}) AS m FROM "VIBE"`).join(" UNION ALL ");return WZ(Z,`SELECT MAX(m) AS m FROM (${J})`)}function i8(Z){if(!yZ(Z,"CTUN")||!kZ(Z,"CTUN").has("ThH"))return null;return WZ(Z,'SELECT AVG("ThH") AS m FROM "CTUN"')}function MZ(Z,$){let Q=Z.tables.PARM;if(!Q)return null;let J=I(Q,"Name"),X=I(Q,"Value");if(J<0||X<0)return null;let Y=$.toLowerCase();for(let K of Q.rows)if(d(K[J])?.toLowerCase()===Y)return h(K[X]);return null}var o8={0:"Undefined",1:"Quad",2:"Hexa",3:"Octa",4:"OctaQuad",5:"Y6",6:"Heli",7:"Tri",8:"SingleCopter",9:"CoaxCopter",10:"BiCopter",11:"Heli_Dual",12:"DodecaHexa",13:"HeliQuad",14:"Deca",15:"Scripting"};function wZ(Z,$){let Q=Z.tables.MSG;if(!Q)return null;let J=I(Q,"Message")>=0?I(Q,"Message"):I(Q,"Msg");if(J<0)return null;for(let X of Q.rows){let Y=d(X[J]);if(Y&&$.test(Y))return Y}return null}var r8=/^([A-Za-z][A-Za-z0-9_+\-]*)\s+([0-9A-Fa-f]{8})\s+([0-9A-Fa-f]{8})\s+([0-9A-Fa-f]{8})\b/;function a8(Z){let $=Z.tables.MSG;if(!$)return null;let Q=I($,"Message")>=0?I($,"Message"):I($,"Msg");if(Q<0)return null;for(let J of $.rows){let X=d(J[Q]),Y=X&&r8.exec(X.trim());if(Y){let[,K,W,G,z]=Y;return{id:`${W}${G}${z}`.toUpperCase(),name:`${K}-${W.toUpperCase()}`}}}return null}function s8(Z){let $=a8(Z),Q=MZ(Z,"SYSID_THISMAV"),J,X;if($)J=$.id,X=$.name;else if(Q!==null)J=`sysid-${Q}`,X=wZ(Z,/ArduCopter|ArduPlane|ArduPilot|board/i)??J;else{let W=wZ(Z,/ArduCopter|ArduPlane|ArduPilot|ChibiOS|board/i);J=W??"unknown",X=W??J}let Y,K=MZ(Z,"Q_ENABLE");if(K!==null&&K>=1)Y="QuadPlane";else{let W=MZ(Z,"FRAME_CLASS");if(W!==null)Y=o8[W]??`class_${W}`}return{kind:"airframe",external_id:J,attrs:{name:X,frame:Y}}}function t8(Z){let $=Z.tables.GPS;if(!$)return null;let Q=I($,"GWk"),J=I($,"GMS");if(Q<0||J<0)return null;let X=315964800000,Y=18000,K=null;for(let G of $.rows){let z=h(G[Q]),B=h(G[J]);if(z===null||B===null||z<=0)continue;let V=X+z*7*24*3600*1000+B-Y;if(K===null||V<K)K=V}if(K===null)return null;let W=new Date(K);return Number.isNaN(W.getTime())?null:W.toISOString()}function e8(Z,$,Q){let J=wZ(Z,/ArduCopter|ArduPlane|ArduPilot/);return{kind:"analysis",source_hash:Q,logged_at:t8(Z),attrs:{firmware:J??null,duration_s:n8($),pilot_id:null,rc_id:null}}}function Z6(Z){let $=Z.tables.PARM;if(!$)return[];let Q=I($,"Name"),J=I($,"Value");if(Q<0||J<0)return[];let X=I($,"Default"),Y=[];for(let K of $.rows){let W=d(K[Q]);if(!W)continue;let G=h(K[J]);if(G===null)continue;let z={name:W,value:G};if(X>=0){let B=h(K[X]);z.default=B}Y.push(z)}return Y}function SZ(Z,$){let Q=I(Z,"TimeUS");if(Q<0)return 0n;return l8($[Q])??0n}function $6(Z){let $=[],Q=Z.tables.ERR;if(Q){let Y=I(Q,"Subsys"),K=I(Q,"ECode");for(let W of Q.rows)$.push({at:SZ(Q,W),type:"ERR",payload:{Subsys:Y>=0?h(W[Y]):null,ECode:K>=0?h(W[K]):null}})}let J=Z.tables.MODE;if(J){let Y=I(J,"TimeUS");for(let K of J.rows){let W={};J.columns.forEach((G,z)=>{if(z===Y)return;W[G]=h(K[z])??d(K[z])}),$.push({at:SZ(J,K),type:"MODE",payload:W})}}let X=Z.tables.MSG;if(X){let Y=I(X,"Message")>=0?I(X,"Message"):I(X,"Msg");for(let K of X.rows)$.push({at:SZ(X,K),type:"MSG",payload:{text:Y>=0?d(K[Y]):null}})}return $}function J6(Z,$){let Q=[],J=c8($);if(J!==null)Q.push({name:"vibe_peak",value:J,unit:"m/s/s"});let X=Z.tables.ERR;if(X){let K=I(X,"Subsys"),W=I(X,"ECode");if(K>=0&&W>=0){let G=0;for(let z of X.rows)if(h(z[K])===6&&h(z[W])===1)G++;Q.push({name:"failsafe_count",value:G})}}let Y=i8($);if(Y!==null)Q.push({name:"hover_thr",value:Y});return Q}function D$(Z,$,Q){return{entity:s8(Z),episode:e8(Z,$,Q.sourceHash),params:Z6(Z),events:$6(Z),measurements:J6(Z,$)}}import{mkdir as Q6,readFile as vZ,writeFile as GZ,readdir as j$,stat as X6}from"fs/promises";import{dirname as P$,join as p}from"path";function I$(Z,$,Q){return p(Z,".hydron","observations",$,zZ(Q))}function zZ(Z){return Z.replace(/[^A-Za-z0-9._-]/g,"_")||"unknown"}function hZ(Z,$){return typeof $==="bigint"?$.toString():$}function C$(Z){if(Array.isArray(Z.events))Z.events=Z.events.map(($)=>{let Q=$.at,J;if(typeof Q==="bigint")J=Q;else if(typeof Q==="string"&&/^-?\d+$/.test(Q))J=BigInt(Q);else if(typeof Q==="number")J=BigInt(Math.trunc(Q));else J=0n;return{...$,at:J}});return Z}var R$={OK:0,HIGH:1,CRITICAL:2};function Y6(Z){let $=Z.findings??[],Q=$.filter((K)=>K.verdict==="FAIL").length,J=$.filter((K)=>K.severity==="CRITICAL"&&K.verdict==="FAIL").length,X="OK";for(let K of $)if(R$[K.severity]>R$[X])X=K.severity;let Y=(K)=>Z.measurements?.find((W)=>W.name===K)?.value??null;return{source_hash:Z.episode.source_hash,logged_at:Z.episode.logged_at,ruleset_version:Z.ruleset_version,anomalies:Q,critical:J,vibe_peak:Y("vibe_peak"),failsafe_count:Y("failsafe_count"),worst_severity:X,...Z.tags&&Z.tags.length>0?{tags:Z.tags}:{}}}async function K6(Z){let $;try{$=await vZ(Z,"utf8")}catch{return[]}let Q=[];for(let J of $.split(`
|
|
10
|
+
`)){let X=J.trim();if(!X)continue;try{Q.push(JSON.parse(X))}catch{}}return Q}async function bZ(Z,$){let Q=p(Z,"index.jsonl"),J=await K6(Q),X=Y6($),Y=J.filter((K)=>K.source_hash!==X.source_hash);Y.push(X),await GZ(Q,Y.map((K)=>JSON.stringify(K)).join(`
|
|
11
11
|
`)+`
|
|
12
|
-
`,"utf8")}async function
|
|
13
|
-
## Finding-severity trend (only checks that differ)`),Z.finding_trend.length===0)K.push("_no system check changed severity across these flights_");else{K.push(X,Y);for(let
|
|
14
|
-
## Key-metric trend`),Z.metric_trend.length===0)K.push("_no measurements recorded_");else{K.push(X,Y);for(let
|
|
15
|
-
## Parameter drift (only params that changed) \u2014 ${Z.param_drift.length} changed`),Z.param_drift.length>0){K.push(X,Y);for(let
|
|
12
|
+
`,"utf8")}async function M$(Z,$){let Q=I$($.projectDir,$.workspace,Z.entity.external_id);await Q6(Q,{recursive:!0});let J=p(Q,`${zZ(Z.episode.source_hash)}.json`);return await GZ(J,JSON.stringify(Z,hZ,2),"utf8"),await bZ(Q,Z),J}async function S$(Z){let $=I$(Z.projectDir,Z.workspace,Z.sysid),Q;try{Q=await j$($)}catch{return null}let J=`${zZ(Z.excludeSourceHash)}.json`,X=Q.filter((K)=>K.endsWith(".json")&&K!==J),Y=null;for(let K of X){let W=p($,K),G;try{let U=await vZ(W,"utf8");G=C$(JSON.parse(U))}catch{continue}let z=0;try{z=(await X6(W)).mtimeMs}catch{}let B=G.episode?.logged_at?Date.parse(G.episode.logged_at):NaN,V=Number.isNaN(B)?-1/0:B;if(Y===null||V>Y.loggedAt||V===Y.loggedAt&&z>Y.mtime)Y={doc:G,loggedAt:V,mtime:z}}return Y?.doc??null}async function r(Z,$,Q){let J=p(Z,".hydron","observations",$),X=`${zZ(Q)}.json`,Y;try{Y=await j$(J)}catch{return null}for(let K of Y){let W=p(J,K,X),G;try{let z=await vZ(W,"utf8");G=C$(JSON.parse(z))}catch{continue}return{path:W,doc:G}}return null}async function w$(Z){return(await r(Z.projectDir,Z.workspace,Z.flight_id))?.doc??null}async function y$(Z){let $=await r(Z.projectDir,Z.workspace,Z.flight_id);if(!$)throw Error(`flight not found: ${Z.flight_id} (workspace ${Z.workspace})`);let Q=[...new Set([...$.doc.tags??[],...Z.tags])];return $.doc.tags=Q,await GZ($.path,JSON.stringify($.doc,hZ,2),"utf8"),await bZ(P$($.path),$.doc),Q}async function k$(Z){let $=await r(Z.projectDir,Z.workspace,Z.flight_id);if(!$)throw Error(`flight not found: ${Z.flight_id} (workspace ${Z.workspace})`);return $.doc.findings??[]}async function v$(Z){let $=await r(Z.projectDir,Z.workspace,Z.flight_id);if(!$)return[];return($.doc.findings??[]).filter((Q)=>Q.origin==="agent")}async function h$(Z){let $=await r(Z.projectDir,Z.workspace,Z.flight_id);if(!$)throw Error(`flight not found: ${Z.flight_id} (workspace ${Z.workspace})`);let Q=$.doc.findings??[],J={...Z.finding,origin:Z.finding.origin??"agent",ruleset_version:Z.finding.ruleset_version||$.doc.ruleset_version};if(J.id&&Q.some((X)=>X.id===J.id))return{added:!1,id:J.id,total:Q.length,reason:"id already present"};if(Z.supersedesId){let X=Q.findIndex((Y)=>Y.id===Z.supersedesId);if(X>=0)Q.splice(X,1)}return Q.push(J),$.doc.findings=Q,await GZ($.path,JSON.stringify($.doc,hZ,2),"utf8"),await bZ(P$($.path),$.doc),{added:!0,id:J.id??"",total:Q.length}}function W6(Z,$){let Q=new Map;for(let X of Z)Q.set(X.name,X.value);let J=[];for(let X of $){let Y=Q.get(X.name);if(Y===void 0)continue;if(Y!==X.value)J.push({name:X.name,old:Y,new:X.value})}return J}function G6(Z,$){return W6(Z.params??[],$.params??[])}function z6(Z,$){let Q=new Map;for(let X of Z.findings??[])if(X.tier==="system")Q.set(X.check,X);let J=[];for(let X of $.findings??[]){if(X.tier!=="system")continue;let Y=Q.get(X.check);if(!Y)continue;if(Y.severity!==X.severity)J.push({check:X.check,old:Y.severity,new:X.severity})}return J}function b$(Z,$){return{param_changes:G6(Z,$),finding_changes:z6(Z,$)}}function f$(Z){let $=Z.map((V)=>V.episode?.source_hash??"?"),Q=Z.map((V)=>{let U=new Map;for(let H of V.params??[])U.set(H.name,H.value);return U}),J=[],X=new Set;for(let V of Q)for(let U of V.keys())X.add(U);for(let V of X){let U=Q.map((T)=>T.has(V)?T.get(V):null),H=U.filter((T)=>T!==null),F=new Set(H).size>1||U.some((T)=>T===null);if(H.length>0&&F)J.push({name:V,values:U})}J.sort((V,U)=>V.name.localeCompare(U.name));let Y=Z.map((V)=>{let U=new Map;for(let H of V.findings??[])if(H.tier==="system")U.set(H.check,{severity:H.severity,verdict:H.verdict});return U}),K=[],W=new Set;for(let V of Y)for(let U of V.keys())W.add(U);for(let V of W){let U=Y.map((F)=>F.get(V)?.severity??"OK"),H=Y.map((F)=>F.get(V)?.verdict??"SKIP");if(new Set(U).size>1)K.push({check:V,severities:U,verdicts:H})}K.sort((V,U)=>V.check.localeCompare(U.check));let G=Z.map((V)=>{let U=new Map;for(let H of V.measurements??[])U.set(H.name,H.value);return U}),z=[],B=new Set;for(let V of G)for(let U of V.keys())B.add(U);for(let V of B)z.push({name:V,values:G.map((U)=>U.has(V)?U.get(V):null)});return z.sort((V,U)=>V.name.localeCompare(U.name)),{flights:$,param_drift:J,finding_trend:K,metric_trend:z}}var g$=[163,149];async function H6(Z){let $=await u$(Z,"r");try{let Q=Buffer.alloc(2);return await $.read(Q,0,2,0),Q[0]===g$[0]&&Q[1]===g$[1]}finally{await $.close()}}async function L6(Z){let $=await u$(Z,"r");try{let Q=B6("sha256"),J=Buffer.alloc(1048576),X=0;for(;;){let{bytesRead:Y}=await $.read(J,0,J.length,X);if(Y===0)break;Q.update(J.subarray(0,Y)),X+=Y}return Q.digest("hex").slice(0,16)}finally{await $.close()}}function O6(Z){let $=Z.filter((X)=>X.verdict==="FAIL").length,Q=Z.filter((X)=>X.severity==="CRITICAL"&&X.verdict==="FAIL").length,J=Z.filter((X)=>X.verdict==="PASS").length;return{total:Z.length,passed:J,anomalies:$,critical:Q}}async function x$(Z,$){if(await V6(Z),!await H6(Z))throw Error(`Not an ArduPilot DataFlash .bin (missing 0xA3 0x95 header): ${Z}`);let Q=await L6(Z),J=E$($.configDir??$.projectDir),{log:X,db:Y,cleanup:K}=await o(Z,Q,{configDir:$.configDir??$.projectDir});try{let W=D$(X,Y,{sourceHash:Q});W.episode.attrs.source_path=U6(Z);let G=K$(X);W.episode.attrs.vehicle_type=G;let z=B$(Y,J,{vehicleType:G});z=[...z,...XZ(W.params,{configDir:$.configDir??$.projectDir,ruleset_version:J.ruleset_version})];let B=await v$({projectDir:$.projectDir,workspace:$.workspace,flight_id:Q});z=[...z,...B];let V={schema_version:1,entity:W.entity,episode:W.episode,params:W.params,findings:z,events:W.events,measurements:W.measurements,ruleset_version:J.ruleset_version},U=await S$({projectDir:$.projectDir,workspace:$.workspace,sysid:W.entity.external_id,excludeSourceHash:Q}),H=await M$(V,{projectDir:$.projectDir,workspace:$.workspace}),F={flight_id:Q,vehicle_id:W.entity.external_id,metadata:{airframe:W.entity.attrs.name??W.entity.external_id,firmware:W.episode.attrs.firmware??null,duration_s:W.episode.attrs.duration_s??null},partial_decode:X.partial,decoded_until:X.decodedUntil,checks:z,summary:O6(z),ruleset_version:J.ruleset_version};if(U){let{param_changes:T,finding_changes:L}=b$(U,V);F.comparison={prior_flight_id:U.episode.source_hash,param_changes:T,finding_changes:L,narration:{claims:[]}}}return{report:F,doc:V,episodePath:H}}finally{K()}}var A6=new URL(import.meta.url).pathname.endsWith(".js"),q6=A6?new URL("./worker/worker-entry.js",import.meta.url).href:new URL("./worker-entry.ts",import.meta.url).href;function m$(Z){return new Promise(($,Q)=>{let J;try{J=new Worker(q6,{type:"module"})}catch(Y){Q(Y);return}let X=(Y)=>{try{J.terminate()}catch{}Y()};J.onmessage=(Y)=>{let{ok:K,result:W,error:G}=Y.data;X(()=>K?$(W):Q(Error(G??"worker failed")))},J.onerror=(Y)=>X(()=>Q(Error(Y.message??"worker error"))),J.postMessage(Z)})}async function d$(Z,$){try{return await m$({op:"analyze",binPath:Z,opts:$})}catch{return x$(Z,$)}}async function p$(Z){try{return await m$({op:"query",opts:Z})}catch{return X$(Z)}}async function n$(Z){let $=[],Q=[];for(let X of Z.flight_ids){let Y=await w$({projectDir:Z.projectDir,workspace:Z.workspace,flight_id:X});if(Y)$.push(Y);else Q.push(X)}if($.length<2)throw Error(`need \u22652 analyzed flights to compare (found ${$.length}). Run uav_analyze on each first.`+(Q.length?` Not in store: ${Q.join(", ")}.`:""));let J=f$($);return{multi:J,markdown:F6(J,$,Q),missing:Q}}var l$=(Z)=>Z===null?"\u2014":Number.isInteger(Z)?String(Z):Z.toFixed(3);function F6(Z,$,Q){let J=$.map((W,G)=>{return`${W.entity?.attrs?.name??W.entity?.external_id??`flight${G+1}`} (${(W.episode?.source_hash??"?").slice(0,8)})`}),X=`| metric | ${J.join(" | ")} |`,Y=`| --- | ${J.map(()=>"---").join(" | ")} |`,K=[`# Flight comparison (${$.length} flights)`];if(Q.length)K.push(`> not found (skipped): ${Q.join(", ")}`);if(K.push(`
|
|
13
|
+
## Finding-severity trend (only checks that differ)`),Z.finding_trend.length===0)K.push("_no system check changed severity across these flights_");else{K.push(X,Y);for(let W of Z.finding_trend)K.push(`| ${W.check} | ${W.severities.join(" | ")} |`)}if(K.push(`
|
|
14
|
+
## Key-metric trend`),Z.metric_trend.length===0)K.push("_no measurements recorded_");else{K.push(X,Y);for(let W of Z.metric_trend)K.push(`| ${W.name} | ${W.values.map(l$).join(" | ")} |`)}if(K.push(`
|
|
15
|
+
## Parameter drift (only params that changed) \u2014 ${Z.param_drift.length} changed`),Z.param_drift.length>0){K.push(X,Y);for(let W of Z.param_drift.slice(0,50))K.push(`| ${W.name} | ${W.values.map(l$).join(" | ")} |`);if(Z.param_drift.length>50)K.push(`
|
|
16
16
|
\u2026 ${Z.param_drift.length-50} more changed params`)}return K.join(`
|
|
17
|
-
`)}function
|
|
18
|
-
`),
|
|
17
|
+
`)}function E6(Z,$){let Q=Z.length;if(Q<=1)return;for(let J=1,X=0;J<Q;J++){let Y=Q>>1;for(;X&Y;Y>>=1)X^=Y;if(X^=Y,J<X)[Z[J],Z[X]]=[Z[X],Z[J]],[$[J],$[X]]=[$[X],$[J]]}for(let J=2;J<=Q;J<<=1){let X=-2*Math.PI/J,Y=Math.cos(X),K=Math.sin(X);for(let W=0;W<Q;W+=J){let G=1,z=0;for(let B=0;B<J>>1;B++){let V=Z[W+B],U=$[W+B],H=Z[W+B+(J>>1)],F=$[W+B+(J>>1)],T=H*G-F*z,L=H*z+F*G;Z[W+B]=V+T,$[W+B]=U+L,Z[W+B+(J>>1)]=V-T,$[W+B+(J>>1)]=U-L;let A=G*Y-z*K;z=G*K+z*Y,G=A}}}}function T6(Z){let $=1;while($<Z)$<<=1;return $}function N6(Z,$){let Q=Z.length;if(Q<4||!($>0))return{freqs:[],mags:[]};let J=Z.reduce((B,V)=>B+V,0)/Q,X=T6(Q),Y=Array(X).fill(0),K=Array(X).fill(0);for(let B=0;B<Q;B++){let V=0.5*(1-Math.cos(2*Math.PI*B/(Q-1)));Y[B]=(Z[B]-J)*V}E6(Y,K);let W=X>>1,G=Array(W),z=Array(W);for(let B=0;B<W;B++)G[B]=B*$/X,z[B]=Math.hypot(Y[B],K[B]);return{freqs:G,mags:z}}function _6(Z,$){return Z.columns.indexOf($)}function n(Z,$){let Q=_6(Z,$);if(Q<0)return[];let J=[];for(let X of Z.rows){let Y=X[Q],K=typeof Y==="bigint"?Number(Y):typeof Y==="number"?Y:Number(Y);if(Number.isFinite(K))J.push(K)}return J}function c$(Z){if(Z.length===0)return 0;let $=Z.reduce((J,X)=>J+X,0)/Z.length,Q=0;for(let J of Z){let X=J-$;Q+=X*X}return Q}function i$(Z,$){if($){let X=n(Z,$).filter((Y)=>Y>0);if(X.length>0){let Y=X.reduce((K,W)=>K+W,0)/X.length;if(Y>0)return{hz:Y,src:`${$} column (${Y.toFixed(0)} Hz)`}}}let Q=n(Z,"TimeUS");if(Q.length>=2){let J=[];for(let X=1;X<Q.length;X++){let Y=Q[X]-Q[X-1];if(Y>0)J.push(Y)}if(J.length>0){J.sort((Y,K)=>Y-K);let X=J[J.length>>1];if(X>0){let Y=1e6/X;return{hz:Y,src:`TimeUS deltas (median dt ${X.toFixed(0)} \xB5s \u2192 ${Y.toFixed(0)} Hz)`}}}}return{hz:0,src:"no sample rate available"}}function D6(Z){let $=Z.tables.IMU;if($&&$.rows.length>=4){let J=[{col:"GyrX",label:"GyrX"},{col:"GyrY",label:"GyrY"},{col:"GyrZ",label:"GyrZ"}],X=null;for(let Y of J){let K=n($,Y.col);if(K.length<4)continue;let W=c$(K);if(!X||W>X.energy)X={samples:K,axis:Y.label,energy:W}}if(X&&X.energy>0){let{hz:Y,src:K}=i$($,"GHz");if(Y>0)return{samples:X.samples,sampleRateHz:Y,axis:X.axis,rateSource:K}}}let Q=Z.tables.VIBE;if(Q&&Q.rows.length>=4){let J=[{col:"VibeX",label:"VibeX"},{col:"VibeY",label:"VibeY"},{col:"VibeZ",label:"VibeZ"}],X=null;for(let Y of J){let K=n(Q,Y.col);if(K.length<4)continue;let W=c$(K);if(!X||W>X.energy)X={samples:K,axis:Y.label,energy:W}}if(X&&X.energy>0){let{hz:Y,src:K}=i$(Q);if(Y>0)return{samples:X.samples,sampleRateHz:Y,axis:X.axis,rateSource:K}}}return null}function R6(Z,$,Q,J){let[X,Y]=J,K=[];for(let W=1;W<$.length-1;W++){let G=Z[W];if(G<X||G>Y)continue;if($[W]>=$[W-1]&&$[W]>=$[W+1])K.push({freq_hz:G,magnitude:$[W],axis:Q})}if(K.length===0){let W=-1;for(let G=0;G<$.length;G++){let z=Z[G];if(z<X||z>Y)continue;if(W<0||$[G]>$[W])W=G}if(W>=0)K.push({freq_hz:Z[W],magnitude:$[W],axis:Q})}return K.sort((W,G)=>G.magnitude-W.magnitude),K}function l(Z,$,Q){return Math.max($,Math.min(Q,Z))}var BZ={dominant_peak_hz:null,peaks:[],params:[],rationale:"insufficient gyro/vibe data"};function j6(Z){let $=Z.tables.ESC;if($&&n($,"RPM").some((Q)=>Q>0))return{mode:3,label:"ESC telemetry"};if(Z.tables.RPM)return{mode:2,label:"RPM sensor"};return{mode:1,label:"throttle-based"}}function o$(Z,$){try{let Q=$?.band_hz??[20,200],J=D6(Z);if(!J)return BZ;let{freqs:X,mags:Y}=N6(J.samples,J.sampleRateHz);if(X.length===0)return BZ;let K=R6(X,Y,J.axis,Q);if(K.length===0)return BZ;let W=K[0].freq_hz,G=K.slice(0,3),z={kind:"rule-of-thumb",ref:"ArduPilot harmonic notch tuning"},B=j6(Z),V=1,U;if(B.mode===1){let E=Z.tables.CTUN,N=E?n(E,"ThH").filter((_)=>_>=0&&_<=1):[];if(N.length>0)V=N.reduce((_,R)=>_+R,0)/N.length,U=`throttle-based (MODE 1): INS_HNTCH_REF = avg(CTUN.ThH) = ${V.toFixed(3)} (hover throttle)`;else U="throttle-based (MODE 1) but no CTUN.ThH \u2014 INS_HNTCH_REF defaulted to 1.0 (set it to your hover throttle)"}else U=`${B.label} (MODE ${B.mode}): the notch tracks measured RPM directly \u2014 INS_HNTCH_REF = 1.0 and INS_HNTCH_FREQ is the low-RPM floor. Preferred over throttle mode (holds the notch on the peak across the throttle range)`;let H=Math.round(W),F=Math.round(W/2),T=[{name:"INS_HNTCH_ENABLE",value:1,range:[0,1],source:z},{name:"INS_HNTCH_MODE",value:B.mode,range:[0,5],source:z},{name:"INS_HNTCH_FREQ",value:l(H,10,495),range:[10,495],source:z},{name:"INS_HNTCH_BW",value:l(F,5,250),range:[5,250],source:z},{name:"INS_HNTCH_REF",value:l(V,0,1),range:[0,1],source:z},{name:"INS_HNTCH_HMNCS",value:3,range:[0,255],source:z}],L="",A=G[1];if(A&&Math.abs(A.freq_hz-W)>15&&A.magnitude>=0.5*K[0].magnitude){let E=Math.round(A.freq_hz),N=Math.round(A.freq_hz/2);T.push({name:"INS_HNTC2_ENABLE",value:1,range:[0,1],source:z},{name:"INS_HNTC2_MODE",value:B.mode,range:[0,5],source:z},{name:"INS_HNTC2_FREQ",value:l(E,10,495),range:[10,495],source:z},{name:"INS_HNTC2_BW",value:l(N,5,250),range:[5,250],source:z},{name:"INS_HNTC2_REF",value:l(V,0,1),range:[0,1],source:z},{name:"INS_HNTC2_HMNCS",value:3,range:[0,255],source:z}),L=` A distinct second peak at ${A.freq_hz.toFixed(1)} Hz warrants the SECOND notch (INS_HNTC2_*) centred there.`}let O=`Dominant vibration peak at ${W.toFixed(1)} Hz on ${J.axis} (within the ${Q[0]}\u2013${Q[1]} Hz search band), found by FFT over ${J.samples.length} samples at a sample rate from ${J.rateSource}. Recommend a harmonic notch centred at ${H} Hz with bandwidth ${F} Hz `+"(\u2248 half the centre, a standard starting width) and the 1st+2nd harmonics "+`(HMNCS=3), tracked ${U}.${L} Review before applying; these are starting values to refine against a post-change FFT.`;return{dominant_peak_hz:W,peaks:G,params:T,rationale:O}}catch{return BZ}}import{mkdir as P6,writeFile as I6}from"fs/promises";import{join as C6}from"path";function M6(Z){if(Z===null||Z===void 0)return"";if(typeof Z==="bigint")return Z.toString();let $=String(Z);return/[",\n]/.test($)?`"${$.replace(/"/g,'""')}"`:$}async function fZ(Z,$){await P6($,{recursive:!0});let Q=[];for(let[J,X]of Object.entries(Z.tables)){if(!X.rows.length)continue;let Y=X.columns.join(","),K=X.rows.map((G)=>G.map(M6).join(",")).join(`
|
|
18
|
+
`),W=C6($,`${J}.csv`);await I6(W,`${Y}
|
|
19
19
|
${K}
|
|
20
|
-
`),Q.push(
|
|
21
|
-
`),Y=
|
|
20
|
+
`),Q.push(W)}return Q}function gZ(Z,$){let Q=$.toLowerCase();return Z.findIndex((J)=>J.toLowerCase()===Q)}function f(Z){if(typeof Z==="number")return Number.isFinite(Z)?Z:null;if(typeof Z==="bigint")return Number(Z);if(typeof Z==="string"){let $=Number(Z);return Number.isFinite($)?$:null}return null}function a(Z,$){let Q,J;if($.includes("."))[Q,J]=$.split(".",2);else J=$,Q=Object.keys(Z.tables).find((K)=>gZ(Z.tables[K].columns,J)>=0);if(!Q)return null;let X=Z.tables[Q];if(!X)return null;let Y=gZ(X.columns,J);if(Y<0)return null;return{table:X,tableName:Q,ci:Y,ti:gZ(X.columns,"TimeUS")}}function s$(Z,$){if(Z.length<=$)return Z;let Q=Z.length/$,J=[];for(let X=0;X<$;X++){let Y=Math.floor(X*Q),K=Math.min(Z.length,Math.floor((X+1)*Q)),W=Z[Y];for(let G=Y;G<K;G++)if(Math.abs(Z[G][1])>Math.abs(W[1]))W=Z[G];J.push(W)}return J}function VZ(Z,$,Q=1200){let J=a(Z,$);if(!J)return null;let X=[],Y=null;for(let K of J.table.rows){let W=f(K[J.ci]);if(W===null)continue;let G=J.ti>=0?f(K[J.ti]):X.length,z=G===null?X.length:G;if(Y===null)Y=z;X.push([J.ti>=0?(z-Y)/1e6:z,W])}if(!X.length)return null;return{name:$,points:s$(X,Q)}}function UZ(Z,$,Q){let J=a(Z,$);if(!J||J.ti<0)return VZ(Z,$)?S6(VZ(Z,$),Q):null;let X=new Map,Y=null;for(let K of J.table.rows){let W=f(K[J.ci]),G=f(K[J.ti]);if(W===null||G===null)continue;if(Y===null)Y=G;let z=Math.floor((G-Y)/1e6/Q),B=X.get(z)??{sum:0,n:0};B.sum+=W,B.n++,X.set(z,B)}if(!X.size)return null;return new Map([...X].map(([K,W])=>[K,W.sum/W.n]))}function S6(Z,$){let Q=new Map;for(let[J,X]of Z.points){let Y=Math.floor(J/$),K=Q.get(Y)??{sum:0,n:0};K.sum+=X,K.n++,Q.set(Y,K)}return new Map([...Q].map(([J,X])=>[J,X.sum/X.n]))}var w6=/^\s*(.+?)\s*([-+*/])\s*(.+?)\s*$/;function uZ(Z,$,Q){return $==="+"?Z+Q:$==="-"?Z-Q:$==="*"?Z*Q:Q===0?NaN:Z/Q}function y6(Z,$,Q=1200){let J=w6.exec($);if(!J)return VZ(Z,$.trim(),Q);let[,X,Y,K]=J,W=Number(K),G=a(Z,X);if(!G)return null;if(Number.isFinite(W)&&!K.includes(".")){let H=VZ(Z,X,Q);if(!H)return null;return{name:$.trim(),points:H.points.map(([F,T])=>[F,uZ(T,Y,W)])}}let z=a(Z,K);if(!z)return null;if(G.tableName===z.tableName&&G.ti>=0){let H=[],F=null;for(let T of G.table.rows){let L=f(T[G.ci]),A=f(T[z.ci]),O=f(T[G.ti]);if(L===null||A===null||O===null)continue;if(F===null)F=O;H.push([(O-F)/1e6,uZ(L,Y,A)])}return H.length?{name:$.trim(),points:s$(H,Q)}:null}let B=UZ(Z,X,0.5),V=UZ(Z,K,0.5);if(!B||!V)return null;let U=[];for(let[H,F]of B){let T=V.get(H);if(T!==void 0)U.push([H*0.5,uZ(F,Y,T)])}return U.length?{name:$.trim(),points:U.sort((H,F)=>H[0]-F[0])}:null}function k6(Z,$,Q,J=0.5){let X=UZ(Z,$,J),Y=UZ(Z,Q,J);if(!X||!Y)return null;let K=[];for(let[W,G]of X){let z=Y.get(W);if(z!==void 0)K.push([G,z])}if(!K.length)return null;return{name:`${Q} vs ${$}`,points:K.sort((W,G)=>W[0]-G[0]),kind:"scatter"}}function v6(Z,$){let Q=Z.length;for(let J=1,X=0;J<Q;J++){let Y=Q>>1;for(;X&Y;Y>>=1)X^=Y;if(X^=Y,J<X)[Z[J],Z[X]]=[Z[X],Z[J]],[$[J],$[X]]=[$[X],$[J]]}for(let J=2;J<=Q;J<<=1){let X=-2*Math.PI/J,Y=Math.cos(X),K=Math.sin(X);for(let W=0;W<Q;W+=J){let G=1,z=0;for(let B=0;B<J/2;B++){let V=Z[W+B],U=$[W+B],H=Z[W+B+J/2]*G-$[W+B+J/2]*z,F=Z[W+B+J/2]*z+$[W+B+J/2]*G;Z[W+B]=V+H,$[W+B]=U+F,Z[W+B+J/2]=V-H,$[W+B+J/2]=U-F;let T=G*Y-z*K;z=G*K+z*Y,G=T}}}}function h6(Z,$,Q={}){let J=a(Z,$);if(!J||J.ti<0)return null;let X=[],Y=[],K=-1/0;for(let L of J.table.rows){let A=f(L[J.ci]),O=f(L[J.ti]);if(A===null||O===null||O<=K)continue;X.push(A),Y.push(O),K=O}if(X.length<16)return null;let W=[];for(let L=1;L<Y.length;L++)W.push(Y[L]-Y[L-1]);W.sort((L,A)=>L-A);let G=W[W.length>>1]/1e6;if(!(G>0))return null;let z=1/G,B=X.reduce((L,A)=>L+A,0)/X.length,V=1;while(V<X.length)V<<=1;let U=Array(V).fill(0),H=Array(V).fill(0);for(let L=0;L<X.length;L++)U[L]=X[L]-B;v6(U,H);let F=Q.fMax??Math.min(z/2,250),T=[];for(let L=1;L<V/2;L++){let A=L*z/V;if(A>F)break;T.push([A,Math.hypot(U[L],H[L])/X.length])}return T.length?{name:`FFT ${$}`,points:T}:null}var r$=["#2563eb","#dc2626","#16a34a","#d97706","#9333ea","#0891b2"];function a$(Z,$={}){let Q=$.width??900,J=$.height??380,X={l:60,r:150,t:34,b:40},Y=Q-X.l-X.r,K=J-X.t-X.b,W=(E)=>E.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),G=Z.flatMap((E)=>E.points);if(!G.length)return`<svg xmlns="http://www.w3.org/2000/svg" width="${Q}" height="${J}"><text x="20" y="30">no data</text></svg>`;let z=G.map((E)=>E[0]),B=G.map((E)=>E[1]),V=Math.min(...z),U=Math.max(...z),H=Math.min(...B),F=Math.max(...B);if(U===V)U=V+1;if(F===H)F=H+1,H-=1;let T=(F-H)*0.06;H-=T,F+=T;let L=(E)=>X.l+(E-V)/(U-V)*Y,A=(E)=>X.t+(1-(E-H)/(F-H))*K,O=[];if(O.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${Q}" height="${J}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="12">`),O.push(`<rect width="${Q}" height="${J}" fill="#ffffff"/>`),$.title)O.push(`<text x="${X.l}" y="20" font-weight="600">${W($.title)}</text>`);O.push(`<rect x="${X.l}" y="${X.t}" width="${Y}" height="${K}" fill="none" stroke="#e5e7eb"/>`);for(let E=0;E<=5;E++){let N=H+E/5*(F-H),_=A(N);O.push(`<line x1="${X.l}" y1="${_}" x2="${X.l+Y}" y2="${_}" stroke="#f1f5f9"/>`),O.push(`<text x="${X.l-6}" y="${_+4}" text-anchor="end" fill="#64748b">${N.toFixed(2)}</text>`)}for(let E of[0,0.5,1]){let N=V+E*(U-V);O.push(`<text x="${L(N)}" y="${X.t+K+22}" text-anchor="middle" fill="#64748b">${N.toFixed(U-V<10?2:0)}</text>`)}return O.push(`<text x="${X.l+Y/2}" y="${J-6}" text-anchor="middle" fill="#475569">${W($.xLabel??"s")}</text>`),Z.forEach((E,N)=>{let _=r$[N%r$.length];if(E.kind==="scatter")for(let R of E.points)O.push(`<circle cx="${L(R[0]).toFixed(1)}" cy="${A(R[1]).toFixed(1)}" r="1.8" fill="${_}" fill-opacity="0.6"/>`);else{let R=E.points.map((w,M)=>`${M===0?"M":"L"}${L(w[0]).toFixed(1)},${A(w[1]).toFixed(1)}`).join(" ");O.push(`<path d="${R}" fill="none" stroke="${_}" stroke-width="1.2"/>`)}O.push(`<rect x="${X.l+Y+14}" y="${X.t+4+N*18}" width="10" height="10" fill="${_}"/>`),O.push(`<text x="${X.l+Y+28}" y="${X.t+13+N*18}" fill="#334155">${W(E.name)}</text>`)}),O.push("</svg>"),O.join("")}function t$(Z,$){let Q=$.mode??"timeseries",J=[],X=[],Y="s";if(Q==="xy"){if(!$.x||!$.y)return{svg:a$([]),plotted:[],missing:["x and y required for xy mode"],xLabel:""};let W=k6(Z,$.x,$.y);if(W)J.push(W);else X.push(`${$.y} vs ${$.x}`);Y=$.x}else if(Q==="fft"){for(let W of $.signals??[]){let G=h6(Z,W);if(G)J.push(G);else X.push(W)}Y="Hz"}else{for(let W of $.signals??[]){let G=y6(Z,W,$.points??1200);if(G&&G.points.length)J.push(G);else X.push(W)}Y="s"}let K=$.title??(J.map((W)=>W.name).join(", ")||Q);return{svg:a$(J,{title:K,xLabel:Y}),plotted:J.map((W)=>W.name),missing:X,xLabel:Y}}var b6=[{match:/\bVIBE\b|\bVibe[XYZ]\b/i,note:"VIBE = whole-airframe vibration at the IMU (per IMU instance) \u2014 NOT per-motor. To blame a motor, correlate the FFT peak Hz with that motor's RPM."},{match:/CTUN\.ThH\b|\bThH\b/i,note:"CTUN.ThH = slow hover-throttle estimate (~constant), NOT instantaneous throttle \u2014 use RCOU (motor PWM) or CTUN.ThO."},{match:/\bRCOU\b|RCOU\.C\d/i,note:"RCOU.Cn = per-motor/servo PWM output (\xB5s)."},{match:/XKF3|NKF3|\bI[VP][NEDT]\b/i,note:"XKF3/NKF3 innovations: velocity (IVN/IVE/IVD) reflect vibration stress; position (IPN/IPE/IPD) reflect GPS."}];function e$(Z){let $=Z.join(" "),Q=[];for(let{match:J,note:X}of b6)if(J.test($)&&!Q.includes(X))Q.push(X);return Q}import{readdir as f6,writeFile as g6}from"fs/promises";import{join as u6}from"path";async function Z0(Z,$,Q={}){let J=(await f6(Z).catch(()=>[])).filter((H)=>H.endsWith(".csv")).map((H)=>H.slice(0,-4)).sort(),X=["import os, sys, json","import pandas as pd, numpy as np",`CSV_DIR = ${JSON.stringify(Z)}`,"_cache = {}","def load(name):"," if name not in _cache:"," _cache[name] = pd.read_csv(os.path.join(CSV_DIR, name + '.csv'))"," return _cache[name]",`# Message tables available via load('NAME'): ${J.join(", ")}`,"# TimeUS is microseconds since boot; seconds = (TimeUS - TimeUS.iloc[0]) / 1e6",""].join(`
|
|
21
|
+
`),Y=u6(Z,"_uav_snippet.py");await g6(Y,X+`
|
|
22
22
|
`+$+`
|
|
23
|
-
`);let K=await x(),
|
|
23
|
+
`);let K=await x(),W=Bun.spawn([...K,Y],{stdout:"pipe",stderr:"pipe"}),G=setTimeout(()=>W.kill(9),Q.timeoutMs??30000),[z,B,V]=await Promise.all([new Response(W.stdout).text(),new Response(W.stderr).text(),W.exited]);return clearTimeout(G),{stdout:(z+(B.trim()?`
|
|
24
24
|
--- stderr ---
|
|
25
|
-
${B.trim()}`:"")).trim().slice(0,12000),ok:V===0,tables:J}}import{existsSync as
|
|
25
|
+
${B.trim()}`:"")).trim().slice(0,12000),ok:V===0,tables:J}}import{existsSync as Y0}from"fs";async function c(Z,$){let Q=ZZ(Z,e($?.configDir));if(Q){let{out:X,cleanup:Y}=await $Z(Z,Q);try{return await c(X)}finally{await Y()}}let J=Z.toLowerCase();if(J.endsWith(".bin"))return i(Z);if(J.endsWith(".ulog"))return t(Z);if(J.endsWith(".tlog")||J.endsWith(".log"))return m(Z);return i(Z).catch(()=>m(Z))}function l6(Z){let $=[];if(Z.partial_decode)$.push(`\u26A0\uFE0F PARTIAL DECODE \u2014 log truncated at TimeUS=${Z.decoded_until}. Post-cut checks were SKIPPED.`);$.push(`Flight ${Z.flight_id} \xB7 ${Z.metadata.airframe}`+(Z.metadata.firmware?` \xB7 ${Z.metadata.firmware}`:"")+(Z.metadata.duration_s?` \xB7 ${Math.round(Z.metadata.duration_s/60)}min`:"")),$.push(`Checks: ${Z.summary.total} run \xB7 ${Z.summary.passed} clean \xB7 ${Z.summary.anomalies} anomalies (${Z.summary.critical} critical) \xB7 ruleset ${Z.ruleset_version}`);let Q=Z.checks.filter((X)=>X.verdict==="FAIL").sort((X,Y)=>$0(Y.severity)-$0(X.severity));for(let X of Q){let Y=X.source?` [${X.source.kind}${X.source.ref?`: ${X.source.ref}`:""}]`:"";$.push(` ${X.severity.padEnd(8)} ${X.check}: value=${X.value??"\u2014"} vs threshold=${X.threshold_used??"\u2014"}${Y}`)}let J=Z.checks.filter((X)=>X.verdict==="SKIP").map((X)=>X.check);if(J.length)$.push(` (skipped \u2014 data absent: ${J.join(", ")})`);if(Z.comparison){$.push(`
|
|
26
26
|
vs prior flight ${Z.comparison.prior_flight_id}:`);for(let X of Z.comparison.param_changes)$.push(` param ${X.name}: ${X.old} \u2192 ${X.new}`);for(let X of Z.comparison.finding_changes)$.push(` finding ${X.check}: ${X.old} \u2192 ${X.new}`);if(!Z.comparison.param_changes.length&&!Z.comparison.finding_changes.length)$.push(" (no changes)")}else $.push(`
|
|
27
27
|
(first flight for this airframe \u2014 no prior to compare)`);return $.join(`
|
|
28
|
-
`)}var
|
|
29
|
-
`)}async function
|
|
30
|
-
`)}var
|
|
31
|
-
`),
|
|
28
|
+
`)}var $0=(Z)=>Z==="CRITICAL"?2:Z==="HIGH"?1:0;function n6(Z){if(!Z.length)return"(no findings stored for this flight yet)";let $=Z.filter((Y)=>Y.origin!=="agent"),Q=Z.filter((Y)=>Y.origin==="agent"),J=(Y)=>` ${Y.origin==="agent"?`[agent ${Y.id}]`:"[deterministic]"} ${Y.severity}/${Y.verdict} ${Y.check}${Y.detail?` \u2014 ${Y.detail}`:""}`,X=[`${Z.length} finding(s) stored:`];return X.push(`Deterministic (${$.length}):`,...$.map(J)),X.push(`Agent-authored (${Q.length}):`,...Q.length?Q.map(J):[" (none)"]),X.push("","Before adding, judge whether your finding is ALREADY represented above (same root cause, even if phrased differently). Add only if genuinely new; pass supersedes_id to replace a stale one."),X.join(`
|
|
29
|
+
`)}async function c6(Z,$,Q){let J=g(Z,".hydron","observations",$),X=await Q0(J).catch(()=>{throw Error(`No observation store at ${J}. Run uav_analyze first.`)});for(let Y of X){let K=g(J,Y,`${Q}.json`);try{let W=JSON.parse(await X0(K,"utf8"));if(Array.isArray(W?.params))return W.params}catch(W){if(W.code==="ENOENT")continue;throw W}}throw Error(`flight_id ${Q} not found in the observation store. Run uav_analyze first.`)}async function HZ(Z,$,Q){let J=g(Z,".hydron","observations",$),X=await Q0(J).catch(()=>{throw Error(`No observation store at ${J}. Run uav_analyze first.`)});for(let Y of X)try{let W=JSON.parse(await X0(g(J,Y,`${Q}.json`),"utf8"))?.episode?.attrs?.source_path;if(typeof W==="string"&&W)return W}catch(K){if(K.code==="ENOENT")continue;throw K}throw Error(`flight_id ${Q} not found (or no source_path). Run uav_analyze first.`)}function i6(Z){if(Z.dominant_peak_hz==null)return`No notch recommendation: ${Z.rationale}`;let $=[`Dominant vibration peak: ${Z.dominant_peak_hz.toFixed(1)} Hz`,Z.rationale,"","Recommended harmonic-notch params:"];for(let Q of Z.params){let J=Q.range?` (range ${Q.range[0]}\u2013${Q.range[1]})`:"",X=Q.source?` [${Q.source.kind}${Q.source.ref?`: ${Q.source.ref}`:""}]`:"";$.push(` ${Q.name} = ${Q.value}${J}${X}`)}return $.push("","Review before applying. Push via the gated MAVLink param link, then tag the retest."),$.join(`
|
|
30
|
+
`)}var o6=["# UAV domain pack active","","This project uses the Hydron UAV pack for ArduPilot/PX4 flight-log intelligence. Handle UAV","work (flight logs, parameters, tuning, telemetry) in whatever mode you are already in \u2014 do","NOT switch to a dedicated agent. The `uav_*` tools are available now: `uav_analyze` (decode +","run the system checks on a .bin/.log/.ulog/.tlog), `uav_query` (read-only SQL over a decoded","flight), `uav_python` (pandas/numpy over its tables \u2014 the tool for any non-trivial","computation), `uav_plot` (charts), `uav_param_audit`, `uav_tune`, `uav_tag`, `uav_finding`,","`uav_export`.","","For the domain rules \u2014 grounding discipline (the guardrails judge, you narrate; cite every","bound's source; never invent a threshold), signal semantics, per-check interpretation, the","tuning playbook, and when to fan work out \u2014 **load the `uav` skill**. Load it as soon as you","start UAV work, not only when narrating results.","","Specialist loops (spawn only for a genuine multi-step drill-down): `uav-investigate`","(open-ended 'why' on one flight), `uav-compare` (cross-flight regression), `uav-verify`","(adversarially pressure-test a proposed anomaly)."].join(`
|
|
31
|
+
`),J0=d6(p6(import.meta.url)),r6=[g(J0,"..","bridge","uav_mavlink_bridge.py"),g(J0,"..","..","bridge","uav_mavlink_bridge.py")];function a6(){return r6.find((Z)=>Y0(Z))}async function s6(Z){try{return await Bun.spawn([...Z,"-c","import mcp, pymavlink"],{stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited===0}catch{return!1}}var t6=async(Z)=>{let $=Z.directory,Q=Z.project?.id!=null?String(Z.project.id):"local";return{config:async(J)=>{let X=a6();if(!X)return;let Y=await EZ();if(!Y)return;J.mcp={...J.mcp,mavlink:{type:"local",command:[...Y,X,"--no-read-only","--allow-param-write"],environment:{MAVLINK_DIALECT:"ardupilotmega"},enabled:await s6(Y),timeout:1e4}}},"experimental.chat.system.transform":async(J,X)=>{X.system.push(o6)},tool:{uav_analyze:D({description:"Analyze one ArduPilot/PX4 flight log (.bin): decode, run the UAV system checks, persist the observation locally, and (if a prior flight for the same airframe exists) compute the param/finding diff. Returns a ranked anomaly report. Verdicts come from deterministic guardrails, not the model.",args:{path:D.schema.string().describe("Absolute path to the .bin DataFlash log")},async execute(J){let{report:X}=await d$(J.path,{projectDir:$,workspace:Q});return l6(X)}}),uav_query:D({description:"Ask an ad-hoc SQL question over a single decoded flight's message tables (one table per message type, e.g. VIBE, ATT, BAT, ERR, CTUN, RCOU). Read-only: SELECT/WITH only, single statement, capped at 10k rows. Use the table/column names from the ArduPilot log. Findings produced this way are exploratory (tier:user), not part of the regression record.",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze"),query:D.schema.string().describe("read-only SQL (SELECT/WITH) over the flight's message tables")},async execute(J){try{let X=await p$({projectDir:$,workspace:Q,flight_id:J.flight_id,query:J.query});return Y$(X)}catch(X){return`uav_query error: ${X instanceof Error?X.message:String(X)}`}}}),uav_compare:D({description:"Compare 2+ ALREADY-ANALYZED flights at once: finding-severity trend (only checks that "+"differ), key-metric trend (vibe_peak, hover_thr, \u2026), and parameter drift across the runs. "+"Analyze each flight first with uav_analyze. Memory-light \u2014 compares the stored per-flight "+"summaries, NOT the raw logs, so four 1 GB flights compare cheaply. Returns a markdown report.",args:{flight_ids:D.schema.array(D.schema.string()).describe("2+ flight_ids from prior uav_analyze runs, in the order to display")},async execute(J){try{let{markdown:X}=await n$({projectDir:$,workspace:Q,flight_ids:J.flight_ids});return X}catch(X){return`uav_compare error: ${X instanceof Error?X.message:String(X)}`}}}),uav_param_audit:D({description:"Audit a flight's ArduPilot parameters for safety-critical settings (arming checks, battery/radio failsafes), out-of-range tuning values, and consistency (cell-count vs failsafe voltages, frame/notch). Deterministic; bounds come from param-rules.yaml + built-in defaults. (uav_analyze already includes this; use this for a standalone param re-audit of a stored flight.)",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze")},async execute(J){try{let X=await c6($,Q,J.flight_id);return q$(XZ(X,{configDir:$}))}catch(X){return`uav_param_audit error: ${X instanceof Error?X.message:String(X)}`}}}),uav_tune:D({description:"Compute an FFT over a flight's gyro/vibration data, find the dominant vibration peak, and recommend ArduPilot harmonic-notch params (INS_HNTCH_ENABLE/FREQ/BW/REF/HMNCS) with their verified ranges. "+"Recommendation only \u2014 review before applying; pushing params is a separate gated step.",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze")},async execute(J){try{let X=await c(await HZ($,Q,J.flight_id),{configDir:$});return i6(o$(X))}catch(X){return`uav_tune error: ${X instanceof Error?X.message:String(X)}`}}}),uav_tag:D({description:"Tag a stored flight with test-profile / campaign labels (e.g. 'notch-v2', 'windy', 'post-prop-balance') for the param-history / run-tracking record. Returns the flight's full tag set.",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze"),tags:D.schema.array(D.schema.string()).describe("one or more tags to add")},async execute(J){try{let X=await y$({projectDir:$,workspace:Q,flight_id:J.flight_id,tags:J.tags});return`tagged ${J.flight_id}: [${X.join(", ")}]`}catch(X){return`uav_tag error: ${X instanceof Error?X.message:String(X)}`}}}),uav_finding:D({description:`Store or list a flight's findings. Findings from uav_analyze are deterministic; THIS tool records the conclusions YOU reach during an investigation so they attach to the flight.
|
|
32
32
|
`+`\u2022 op:"list" \u2014 show every stored finding (deterministic + agent). ALWAYS call this first.
|
|
33
|
-
`+'\u2022 op:"add" \u2014 append a NEW agent finding. Only add after listing and judging it is not already '+"represented (same root cause, even if worded differently). To replace one you now consider stale, "+"pass supersedes_id. A finding must cite the ABSOLUTE bound it breached (in detail) \u2014 not 'N\xD7 the "+"other instances'. The store refuses an exact-id rewrite but does NOT dedup semantically; that is your call.",args:{op:
|
|
33
|
+
`+'\u2022 op:"add" \u2014 append a NEW agent finding. Only add after listing and judging it is not already '+"represented (same root cause, even if worded differently). To replace one you now consider stale, "+"pass supersedes_id. A finding must cite the ABSOLUTE bound it breached (in detail) \u2014 not 'N\xD7 the "+"other instances'. The store refuses an exact-id rewrite but does NOT dedup semantically; that is your call.",args:{op:D.schema.enum(["list","add"]).describe("list existing findings, or add a new one"),flight_id:D.schema.string().describe("flight_id from a prior uav_analyze"),title:D.schema.string().optional().describe("add: short label, e.g. 'IMU2 accel-Y divergence'"),severity:D.schema.enum(["OK","HIGH","CRITICAL"]).optional().describe("add: magnitude (default HIGH)"),detail:D.schema.string().optional().describe("add: the grounded evidence + the ABSOLUTE bound it breached and that bound's source"),value:D.schema.number().optional().describe("add: measured metric, if any"),threshold_used:D.schema.number().optional().describe("add: the absolute bound it was judged against"),supersedes_id:D.schema.string().optional().describe("add: id of an existing finding this replaces")},async execute(J){try{if(J.op==="list")return n6(await k$({projectDir:$,workspace:Q,flight_id:J.flight_id}));if(!J.title)return"uav_finding error: title is required to add a finding";let X=J.severity??"HIGH",Y={check:J.title,severity:X,verdict:X==="OK"?"PASS":"FAIL",tier:"user",origin:"agent",value:J.value??null,threshold_used:J.threshold_used??null,ruleset_version:"",detail:J.detail,id:`a-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,created_at:new Date().toISOString()},K=await h$({projectDir:$,workspace:Q,flight_id:J.flight_id,finding:Y,supersedesId:J.supersedes_id});return K.added?`stored agent finding ${K.id} on ${J.flight_id} (${K.total} findings total)`+(J.supersedes_id?` \u2014 superseded ${J.supersedes_id}`:""):`not stored (${K.reason}); ${K.total} findings already on ${J.flight_id}`}catch(X){return`uav_finding error: ${X instanceof Error?X.message:String(X)}`}}}),uav_python:D({description:"Run a Python snippet (pandas + numpy) over a flight's decoded data \u2014 THE tool for any non-trivial "+"analysis: binning, correlation, scoring/ranking windows, regression, FFT, custom stats. The message "+"tables are available as `load('VIBE')`, `load('ATT')`, `load('BAT')`, \u2026 (each a DataFrame; TimeUS is \xB5s "+"since boot). `print()` your results. Use this INSTEAD of multiple uav_query calls or doing arithmetic "+"in your head \u2014 the numbers must come from code you actually run.",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze"),code:D.schema.string().describe("Python; use load('TABLE') for DataFrames, print() results")},async execute(J){try{let X=g($,".hydron","pycache",J.flight_id);if(!Y0(X)){let G=await c(await HZ($,Q,J.flight_id),{configDir:$});await fZ(G,X)}let{stdout:Y,ok:K,tables:W}=await Z0(X,J.code);return K?Y||"(snippet ran, no output \u2014 did you print()?)":`python exited non-zero:
|
|
34
34
|
${Y}
|
|
35
35
|
|
|
36
|
-
(tables available: ${
|
|
36
|
+
(tables available: ${W.join(", ")})`}catch(X){return`uav_python error: ${X instanceof Error?X.message:String(X)}`}}}),uav_export:D({description:"Export a decoded flight to one CSV per message type (PlotJuggler-importable) into a directory, so the lab's existing PlotJuggler validation flow isn't disrupted. Returns the output directory + file count.",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze"),out_dir:D.schema.string().describe("absolute output directory for the CSVs")},async execute(J){try{let X=await c(await HZ($,Q,J.flight_id),{configDir:$});return`exported ${(await fZ(X,J.out_dir)).length} CSV file(s) to ${J.out_dir} (PlotJuggler-importable)`}catch(X){return`uav_export error: ${X instanceof Error?X.message:String(X)}`}}}),uav_plot:D({description:"Chart flight signals as an SVG that renders in the editor. Use for ANY plot/chart/graph/visualise "+`request \u2014 never hand-write SQL via uav_query for visualisation. Three modes:
|
|
37
37
|
`+'\u2022 timeseries (default): signals vs time. Supports DERIVED expressions, e.g. "ATT.DesRoll - ATT.Roll" '+`(tracking error), "CTUN.ThH * 100". Multiple signals overlay.
|
|
38
38
|
`+"\u2022 xy: one signal vs another (time-aligned) \u2014 correlations, e.g. x=BAT.Curr y=BAT.Volt (sag/IR), "+`x=RCOU.C1 y=VIBE.VibeZ (vibe vs throttle \u2014 motor output, NOT CTUN.ThH which is the constant hover estimate).
|
|
39
39
|
`+`\u2022 fft: magnitude spectrum (Hz) of a signal \u2014 vibration peaks, e.g. signals=["IMU.GyrX"] or ["VIBE.VibeZ"].
|
|
40
|
-
`+"Signals/exprs use TABLE.COLUMN (VIBE.VibeZ, ATT.DesRoll, BAT.Volt, RCOU.C1, XKF3.IVN, \u2026).",args:{flight_id:
|
|
41
|
-
_(no data for: ${
|
|
40
|
+
`+"Signals/exprs use TABLE.COLUMN (VIBE.VibeZ, ATT.DesRoll, BAT.Volt, RCOU.C1, XKF3.IVN, \u2026).",args:{flight_id:D.schema.string().describe("flight_id from a prior uav_analyze"),mode:D.schema.enum(["timeseries","xy","fft"]).optional().describe("default timeseries"),signals:D.schema.array(D.schema.string()).optional().describe('timeseries/fft: signals or derived exprs, e.g. ["ATT.DesRoll - ATT.Roll"] or ["IMU.GyrX"]'),x:D.schema.string().optional().describe("xy mode: x-axis signal, e.g. BAT.Curr"),y:D.schema.string().optional().describe("xy mode: y-axis signal, e.g. BAT.Volt"),title:D.schema.string().optional().describe("optional chart title")},async execute(J){try{let X=await c(await HZ($,Q,J.flight_id),{configDir:$}),{svg:Y,plotted:K,missing:W}=t$(X,{mode:J.mode,signals:J.signals,x:J.x,y:J.y,title:J.title});if(!K.length)return`uav_plot: nothing plottable (${W.join(", ")||"no signals given"}) for flight ${J.flight_id}`;let G=g($,".hydron","plots");await m6(G,{recursive:!0});let z=(J.mode??"ts")+"_"+K.join("_").replace(/[^A-Za-z0-9._-]/g,"-").slice(0,50),B=g(G,`${J.flight_id}__${z}.svg`);await x6(B,Y);let V=`data:image/svg+xml;base64,${Buffer.from(Y).toString("base64")}`,U=W.length?`
|
|
41
|
+
_(no data for: ${W.join(", ")})_`:"",H=[...J.signals??[],J.x,J.y].filter((L)=>!!L),F=e$(H.length?H:K),T=F.length?`
|
|
42
42
|
|
|
43
|
-
`+
|
|
43
|
+
`+F.map((L)=>`> \u2139\uFE0F ${L}`).join(`
|
|
44
44
|
`):"";return`
|
|
45
45
|
|
|
46
|
-
Chart saved: ${B} (\u2318\u21E7V to preview)${U}${T}`}catch(X){return`uav_plot error: ${X instanceof Error?X.message:String(X)}`}}})}}},
|
|
46
|
+
Chart saved: ${B} (\u2318\u21E7V to preview)${U}${T}`}catch(X){return`uav_plot error: ${X instanceof Error?X.message:String(X)}`}}})}}},oJ=t6;export{oJ as default};
|
package/skills/uav/SKILL.md
CHANGED
|
@@ -46,7 +46,10 @@ never to author a verdict from raw numbers.
|
|
|
46
46
|
genuinely new (`supersedes_id` to replace a stale one). Only ABSOLUTE-bound findings the
|
|
47
47
|
deterministic checks did not already cover — never a relative-only one.
|
|
48
48
|
- **Bench / live vehicle** → `serial_*` (watch/drive the FC console) and the `mavlink` MCP
|
|
49
|
-
(read params/telemetry;
|
|
49
|
+
(read params/telemetry; `param_set`/`param_set_batch`) when a link is configured. ⚠ param
|
|
50
|
+
writes are **enabled by default and go to whatever is on the connect target — possibly a real
|
|
51
|
+
airframe**. Always state the exact param + old→new value and get explicit confirmation before
|
|
52
|
+
writing; never batch-write a tuning set without the user agreeing to it first.
|
|
50
53
|
|
|
51
54
|
## Load the reference that matches the task
|
|
52
55
|
|
|
@@ -65,8 +65,10 @@ These params are **starting values**, not a verified tune. They must be reviewed
|
|
|
65
65
|
human (or the gating subagent) before they touch a vehicle. Critically, this module
|
|
66
66
|
only *recommends* — it has no path to a flight controller.
|
|
67
67
|
|
|
68
|
-
**Pushing params is a separate,
|
|
68
|
+
**Pushing params is a separate, explicit action.** `/uav-tune` (or the `uav_tune` tool) only
|
|
69
69
|
recommends — confirm the recommendation against the airframe and check the ranges, then
|
|
70
|
-
write parameters over the
|
|
70
|
+
write parameters over the MAVLink param link (`param_set` / `param_set_batch`, available by
|
|
71
|
+
default). ⚠ that link writes to whatever is on the connect target, which may be a **live
|
|
72
|
+
vehicle**: state each param and its old→new value and get explicit confirmation first. After applying,
|
|
71
73
|
re-fly and re-run `recommendNotch` on the new
|
|
72
74
|
log to confirm the peak dropped — the same FFT→notch loop, closed automatically.
|