Document the firmware version and all settings of a drone (Betaflight / INAV, optionally KISS Ultra later) and evaluate it against configurable rules.
When a flight controller is plugged in over USB the tool:
- Identifies it over MSP (binary): FC variant, firmware version, build info (incl. short git hash) and the 96-bit MCU unique id used as the flight-controller serial.
- Drops into the CLI and captures the full configuration
(
version,dump all,status), checking each response for completeness, then exits cleanly so the FC reboots.dump allis used because it lists every setting with its absolute value — rule evaluation never has to guess a firmware default. (diff allis optional; add it tocli_commandsif you also want the portable human-readable backup stored.) Both CLI variants are handled automatically: the classic#text prompt (Betaflight ≤ 4.5.x, INAV) and the framed MSP-CLI used by Betaflight 4.5.4+ / 2025.x, which the#prompt no longer reaches. - Normalises the data — in particular the VTX power configuration (armed / disarmed power and the radio switches that select it).
- Verifies the firmware hash against a local allowlist and/or the official firmware GitHub repository.
- Evaluates rules written in CEL and shows a green/red verdict with the reasons.
- Logs everything into its own folder, by default
logs/<timestamp>_<pilot_name>_<craft_name>/.
The capture never waits on operator input and logs are written once and never
modified or moved — they contain only the real data read from the flight
controller. The pilot and craft names come from the FC (pilot_name /
craft_name), which are the single source of truth and also name the folder.
Manual entry is off by default. Enable allow_manual_pilot to let the
operator set a fallback pilot name that is used only for the folder label when
the FC reports none — it never alters the captured data files. The folder naming
is configurable via folder_template.
Beyond live capture, the web UI has a logs page (/logs) to browse past
captures and view any capture's configuration in the real Betaflight
Configurator — served by the read-only bf-configd backend (preferred and
default) with SITL as a fallback. See
Logs page & "view in Configurator".
- Python 3.10+
- A flight controller exposed as a USB serial (CDC/VCP) port. On Windows the
STM32 VCP driver ships with the OS; on Linux the user must be in the
dialoutgroup to access/dev/ttyACM*. - Close Betaflight / INAV Configurator before running — it holds the serial port open.
- WSL is needed only on Windows for the "View in Configurator" feature (below). On Linux the backend binaries (bf-configd / SITL) run natively (no WSL). Capturing and rule-checking work fully without any of this, and the button is hidden when no backend environment is available.
From the repository root, run the guided installer for your OS — it checks
Python, creates the .venv, installs drone-check, and asks whether you also
want the "View in Configurator" feature. If you opt in it downloads the pre-built
binary bundles (bf-configd — the read-only default — and SITL) from the project's
binaries GitHub release, so no toolchain is needed. (The download needs no
authentication; or pass a local bundle with --bfcd-bundle / --sitl-bundle.)
# Windows (binaries run under WSL)
powershell -ExecutionPolicy Bypass -File scripts\install.ps1# Linux / macOS (binaries run natively on Linux; not available on macOS)
bash scripts/install.shUnattended variants (same idea on both):
powershell -ExecutionPolicy Bypass -File scripts\install.ps1 -NoSitl
powershell -ExecutionPolicy Bypass -File scripts\install.ps1 -Sitl -BfcdBundle C:\path\bfcd-bundle.tar.gzbash scripts/install.sh --no-sitl
bash scripts/install.sh --sitl --bfcd-bundle /path/bfcd-bundle.tar.gzStart it afterwards with drone-check serve (or .\.venv\Scripts\drone-check.exe serve / ./.venv/bin/drone-check serve) and open http://127.0.0.1:8000.
# Windows (PowerShell)
python -m venv .venv
.\.venv\Scripts\activate
pip install -e ".[dev]"# Linux / macOS
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"This installs the drone-check command. (Equivalently you can always run
python -m drone_check <command>.)
For the "View in Configurator" feature you additionally need WSL with a distro (one-time, Administrator PowerShell, then reboot):
wsl --install -d UbuntuThen put the backend binaries in place. bf-configd is the preferred (default) backend, so install it first — either a pre-built bundle (no toolchain needed):
drone-check bfcd install C:\path\to\bfcd-bundle.tar.gz
drone-check bfcd list # confirmSITL is the fallback backend; install it too if you want it available
(drone-check sitl install C:\path\to\sitl-bundle.tar.gz). Either can also be
built from source inside WSL (see docs/CONFIGURATOR.md for
SITL and docs/bfcd/ for bf-configd). If WSL is absent the feature is
simply hidden — nothing else is affected.
# Local web UI + USB hot-plug watcher
drone-check serve # http://127.0.0.1:8000
# Try the UI without hardware (then click "Run demo")
drone-check serve --demo
# List serial ports (find your flight controller's COMx)
drone-check ports
# Bring-up: identify + raw CLI dump, no rules (great for first contact)
drone-check probe COM5 --debug --raw
# Capture a single drone on a known port and print the report
drone-check inspect COM5
# Run the whole pipeline against built-in sample drones
drone-check demo| Command | What it does |
|---|---|
serve [--host H] [--port P] [--demo] |
Start the local web UI and the USB hot-plug watcher. --demo skips the watcher and shows a "Run demo" button instead (the button only appears in --demo mode). Stop with Ctrl+C / Enter / the "Server beenden" button — see Stopping the server. |
ports |
List available serial ports with VID:PID — find your FC's COMx. |
probe <port> [--raw] |
First-contact bring-up: MSP identify + raw CLI dump, no rule evaluation. |
inspect <port> |
Full capture + hash check + rule evaluation; prints the report. Exit code 0 = PASS, 2 = FAIL. |
demo |
Run the pipeline against the built-in sample drones (no hardware). |
Serial flags for probe / inspect: --baud N, --connect-delay S,
--debug (tee raw traffic to ./debug/<port>-<time>.log). Global: --config DIR.
The everyday workflow is drone-check serve: open the page, plug a drone in —
it is read automatically and a green/red verdict appears; the pilot and craft
names come from the drone. Unplug and repeat.
For the first run against real hardware, follow HARDWARE_TEST.md.
serve runs until you stop it. Any of these does a clean shutdown (it ends
an active viewer session — bf-configd or SITL — and stops the WSL distro it
started):
- Ctrl+C in the terminal — on Windows a native console handler triggers the
graceful shutdown directly (works in
cmd.exeand PowerShell).⚠️ Windows PowerShell 5.1 can still swallow Ctrl+C before it reaches the program; if it appears to do nothing there, use one of the next two instead. - Press Enter in the
servewindow — reliable in every terminal. - The "Server beenden" button in the web UI header.
The web UI has a second page at /logs (link in the header) that lists every
capture in the log directory, newest first — searchable (pilot, craft, UID,
firmware version, folder) and filterable by verdict and firmware variant. Each
row can open the capture folder and view the configuration in the real
Betaflight Configurator, so an inspector sees exactly what the drone owner would
see, with all firmware-version-specific GUI behaviour handled by the real
Configurator rather than re-implemented by us.
Two backends can serve that view. bf-configd is the preferred — and default —
backend; SITL is a fallback for the rare cases bf-configd cannot serve a
capture. The /logs page shows a single View in Configurator button per
capture; which backend it uses is a config choice — viewer_backend: bfcd
(default) or sitl in settings.yaml.
bf-configd serves a capture's dump all to the Configurator over MSP using a
backend built from official Betaflight source with a firmware-enforced
read-only guard — every MSP write is refused, so an inspector can view
everything but nothing can be changed or persisted. It is lighter than SITL (no
flight loop) and is built per version, covering every Betaflight version
drone-check ships a backend for — 4.4.0, 4.5.0–4.5.4 and
2025.12.1–2025.12.4 (Linux/WSL). Each binary is built from its own release
tag, so it serves that version's dump faithfully across the 4.5.4 framed-CLI
boundary. Prefer it for every inspection; reach for SITL only if bf-configd has
trouble with a particular capture.
Build the backends once (inside WSL/Linux); the static binaries can then be bundled and handed to other machines (which need only WSL, no toolchain):
# build every shipped version into the cache (or pass a subset of tags)
bash scripts/build_bfcd.sh 4.4.0 4.5.0 4.5.1 4.5.2 4.5.3 4.5.4 2025.12.1 2025.12.2 2025.12.3 2025.12.4
drone-check bfcd package C:\share\bfcd-bundle.tar.gz # bundle the cache
drone-check bfcd install C:\share\bfcd-bundle.tar.gz # on the target machine
# then on /logs, click "View in Configurator" on a capture -> ws://127.0.0.1:6762
# or from the CLI, no UI:
.\.venv\Scripts\python.exe -m drone_check bfcd plan <dump.txt> # selection only
.\.venv\Scripts\python.exe -m drone_check bfcd serve <dump.txt> # run + serveSee bf-configd/README.md and docs/bfcd/.
SITL runs the full Betaflight firmware as a Software-In-The-Loop host. It is
the fallback backend: heavier than bf-configd (it boots the whole FC) and not
read-only, kept for the cases bf-configd cannot serve a capture. To use it, set
viewer_backend: sitl in settings.yaml. One-time setup (build the SITL binaries
for the versions you inspect, inside WSL):
sudo apt-get install -y build-essential ruby git
bash scripts/build_sitl.sh 4.4.0 4.5.4 2025.12.2Both old semver tags (e.g. 4.4.0) and the newer date-based tags (e.g.
2025.12.2) work — the build script adapts to the different source-tree layout
each firmware generation uses.
The binaries are statically linked, so you only build once and can hand the cache to other machines (which then need only WSL, no toolchain):
drone-check sitl package C:\share\sitl-bundle.tar.gz # bundle the cache
drone-check sitl install C:\share\sitl-bundle.tar.gz # on the target machineThen on /logs (with viewer_backend: sitl), click View in Configurator and
connect the Betaflight web Configurator (manual connection) to
ws://127.0.0.1:6761.
See docs/CONFIGURATOR.md for the full SITL guide: setup, how it works, the VTX config patch, caching, configuration and troubleshooting.
Every capture is written once into its own immutable folder (default template
{timestamp}_{pilot_name}_{craft_name}):
logs/<timestamp>_<pilot_name>_<craft_name>/
snapshot.json normalised firmware + VTX + names + all settings
evaluation.json rule results + overall verdict
report.txt human-readable summary
raw/<command>.txt raw output of each captured CLI command
A per-session application log (logs/session-<timestamp>.log) records USB/COM
port actions, warnings, errors and successful captures. The web UI shows the
same entries in a live "Session log" list (newest first, length configurable via
log_list_length). The header always reflects the current state — after an
unstable connection it returns to "Ready" once the drone is removed.
All config lives in config/:
| File | Purpose |
|---|---|
settings.yaml |
log dir, folder template, manual-pilot toggle, baud rate, CLI commands, hash-check toggles, capture-retry cap, SITL/Configurator options |
rules.yaml |
CEL rules; a drone passes only if every critical rule passes |
firmware_allowlist.yaml |
approved git hashes per variant + version |
drone.pilot_name, drone.craft_name (read from the FC)
drone.firmware.{variant,version,target,git_hash,...}
drone.vtx.{power_armed_max_mw,power_disarmed_mw,low_power_disarm,
switches[].{aux_channel,power_index,reachable_mw[]}}
drone.settings["<cli_setting_name>"]
checks.firmware_hash_approved
Example (both armed and disarmed VTX power must stay at/below 25 mW):
- id: vtx-power-armed-max
severity: critical
expr: 'drone.vtx.power_armed_max_mw <= 25'Betaflight puts VTX power on a switch through vtx control lines (not
adjrange), mapping an AUX channel + PWM range to a power index; the
index→mW mapping comes from vtxtable powervalues. vtx_low_power_disarm
forces the lowest power while disarmed.
INAV has no vtx control lines — the Programming Framework drives power via
logic conditions with operation 25 ("Set VTx Power Level"). The commanded
value may be a constant (operand type 0), an RC channel (type 1, i.e. a
pilot switch/pot) or another logic condition (type 4, traced back to its RC
channel). INAV operation-25 values are 0-based, one below the vtx_power
setting.
Because the live switch position is unknown on the bench, the reported armed power is the maximum any switch position can select (a dynamically driven level is treated as reaching the whole table) — no position may exceed the limit.
vtxtable powervalues are the numbers sent to the VTX; their unit depends on the
protocol, while vtxtable powerlabels are free-form OSD strings that can be set
to anything. A cheater can label a 400 mW level "25" to read 25 mW on the OSD
while transmitting 400 mW. drone-check decodes the real power from the value
and flags any level whose label understates it (osd_power_mismatch).
The encoding is determined by the VTX device type (from MSP MSP_VTX_CONFIG:
SmartAudio / Tramp / RTC6705 / MSP) plus the value pattern:
| Protocol | powervalues |
Verifiable? |
|---|---|---|
| IRC Tramp | milliwatts (25 100 200 400 600) | yes |
| SmartAudio 2.1 | dBm (14 20 26 36 → 25/100/400/4000 mW) | yes |
| SmartAudio V1/V2 | opaque power indices (0 1 2 3) | no |
| RTC6705 | milliwatts | yes |
The exact VTX type comes from MSP; it is not in the text dump. The SmartAudio
sub-version is not exposed by the FC at all, so it is inferred from the value
encoding. For index-based tables (SmartAudio V1/V2) the real mW lives only in
the device and the manipulable label, so it cannot be verified from the FC —
drone-check marks such captures power_verifiable: false and fails the
vtx-power-verifiable rule rather than trusting the label.
config/firmware_allowlist.yaml is generated from the official release tags:
python scripts/update_allowlist.py # refresh from GitHub
python scripts/update_allowlist.py --min-btfl 4.4.0 --min-inav 7.0.0Each entry is a release tag's full commit SHA; the firmware reports an
abbreviation of it, which drone-check matches by prefix. Set GITHUB_TOKEN
to raise the API rate limit. Manual entries (e.g. approved custom builds) can be
added by hand and survive as long as you don't regenerate.
pytestThe parser, VTX normalisation, capture assembly and rule engine are covered by tests using built-in sample drones, so the full pipeline is verifiable without hardware.