Betaflight Extract Blackbox: cross-platform C++ CLI that detects the FC serial port, reads craft_name, reboots into MSC mode, and copies btfl_all.bbl to <craft_name>_<date>_<time>.bbl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.5 KiB
bfebbx — Betaflight Extract Blackbox — Design
Date: 2026-07-22 Status: Approved for planning
Overview
bfebbx is a cross-platform (macOS + Linux) C++ command-line tool that extracts
the blackbox log from a Betaflight flight controller connected over USB. In a
single command it detects the FC's serial port, reads the craft name, reboots the
FC into USB mass-storage mode, waits for the storage volume to mount, copies
btfl_all.bbl to a timestamped, craft-named file, and ejects the volume.
Target filename: <craft_name>_<YYYYMMDD>_<HHMMSS>.bbl.
Goals
- One command performs the full mount-and-copy chain.
- Minimize subprocess use: everything is native syscalls/frameworks except a single Linux eject fallback.
- No third-party libraries; system frameworks only.
- Work on both macOS and Linux behind a thin platform seam.
Non-Goals
- Erasing the FC dataflash after download.
- Reading/using the FC real-time clock.
- Downloading anything other than
btfl_all.bbl. - A GUI or any interactive configuration beyond a possible port prompt (not in this version — ambiguous ports error out).
CLI
bfebbx [--port PORT] [--output DIR] [--craft-name NAME] [--timeout SEC] [-v]
| Flag | Default | Meaning |
|---|---|---|
--port PORT |
auto-detect | Serial device to use. Overrides auto-detection. |
--output DIR |
current working directory | Directory the .bbl is written to. |
--craft-name NAME |
read from FC | Override the craft name used in the filename. Also the fallback if the FC's craft_name is unset. |
--timeout SEC |
30 | Max seconds to wait for the mass-storage volume to appear. |
-v |
off | Verbose logging of each step. |
Runtime Flow
- Find port. Scan
/dev/tty.usbmodem*(macOS) //dev/ttyACM*(Linux), ideally filtered by Betaflight's USB VID/PID. Exactly one candidate → use it. Zero or multiple → error listing candidates and suggest--port.--portbypasses the scan. - Read craft_name. Open serial at 115200 8N1. Enter Betaflight CLI (send
#, wait for the CLI banner/prompt). Sendget craft_name, parse thecraft_name = Xline. This happens before msc because msc reboots the FC. If unset/empty and no--craft-name, fall back tobetaflight. - Snapshot mounts. Record the current set of mounted volumes (native; see below) so the new volume can be identified by difference.
- Enter MSC. Send
msc. The FC reboots and re-enumerates as a USB mass-storage device; the serial port disappears. - Wait for volume. Poll (up to
--timeout) for a new mounted volume (present in the after-set, absent from the before-set) that actually containsbtfl_all.bbl. macOS auto-mounts viadiskarbitrationd; Linux relies on the desktop automounter. Identifying by "new volume containing the file" avoids guessing the FAT volume label. - Copy + rename.
std::filesystem::copy_filethebtfl_all.bblto<output>/<craft_name>_<YYYYMMDD>_<HHMMSS>.bbl. Timestamp is local wall-clock at extraction time. Refuse to overwrite an existing destination file. - Eject the volume on success. On failure (copy error, file missing), leave it mounted and report that, so the user can inspect.
Minimizing Subprocesses
| Operation | macOS | Linux |
|---|---|---|
| Serial I/O | termios (native) | termios (native) |
| Mount enumeration | getmntinfo(3) → struct statfs[] |
read /proc/self/mountinfo |
| Mounting new volume | auto (diskarbitrationd) |
auto (desktop automounter) |
| File copy | std::filesystem::copy_file |
std::filesystem::copy_file |
| Eject / unmount | DiskArbitration.framework (DADiskUnmount) |
umount(2); on EPERM fall back to shelling udisksctl unmount |
The only subprocess in the entire tool is the Linux eject fallback, and only
when the unprivileged umount(2) is denied.
Naming Rules
sanitizeCraftName: characters outside[A-Za-z0-9._-]are replaced with_.- Empty craft_name and no
--craft-name→betaflight. buildFilename(name, time_t)→<name>_<YYYYMMDD>_<HHMMSS>.bblusing local time.
Betaflight Serial Handshake
- Baud 115200, 8N1 (nominal over USB VCP, set regardless).
- Enter CLI: send
#; the FC replies with a CLI banner and a#prompt. - Read craft name: send
get craft_name\r\n; parse the response line matching^craft_name\s*=\s*(.*)$. - Enter MSC: send
msc\r\n; the FC prints a restart notice and reboots into mass-storage mode.
Technical bets to verify against real firmware during implementation: the
exact CLI-entry handshake and that msc is the reboot-to-mass-storage command.
Both are standard Betaflight, but firmware/board variation exists — hence the
generous timeout and the "volume actually contains the file" check rather than
trusting labels.
Code Structure
bfebbx/
├── CMakeLists.txt
├── README.md
├── src/
│ ├── main.cpp # arg parsing, orchestration, exit codes
│ ├── serial.{h,cpp} # termios open/config, timed read, readUntil/readLine
│ ├── betaflight.{h,cpp} # enterCli / getCraftName / enterMsc + pure parse fn
│ ├── mounts.{h,cpp} # MountEntry, listMounts() [getmntinfo | mountinfo], diff (pure)
│ ├── eject.{h,cpp} # macOS DiskArbitration; Linux umount(2) + udisksctl fallback
│ └── naming.{h,cpp} # sanitizeCraftName(), buildFilename() — pure
└── tests/
├── test_naming.cpp
├── test_betaflight_parse.cpp
└── test_mounts.cpp
Design intent: hardware-touching code (serial, eject, live mount listing) is isolated behind thin functions; the logic — craft_name parsing, filename building, mount before/after diffing — is pure and independently testable.
- Build: C++17, CMake.
-Wall -Wextra. macOS linksDiskArbitrationandCoreFoundationframeworks; no third-party libraries on either platform. - Platform seam:
#ifdef-guarded implementations insidemounts.cppandeject.cpp;serial.cppis shared POSIX termios.
Testing
Matching the "no frameworks, stdlib only" philosophy, adapted to C++: small
assert-based test executables registered with add_test and run via ctest.
| Test | Covers |
|---|---|
test_naming |
sanitizeCraftName edge cases; buildFilename format |
test_betaflight_parse |
craft_name extraction from sample CLI output (set, empty, whitespace, surrounding noise) |
test_mounts |
diffMounts(before, after) returns the correct new entries |
Serial handshake and eject require hardware and are verified by manual integration testing against a real FC, documented in the README.
Error Handling & Exit Codes
0 on success; distinct non-zero codes with a clear stderr message per class:
| Class | Behavior |
|---|---|
| No candidate port | Error, suggest --port. |
| Multiple candidate ports | Error listing them, suggest --port. |
| Serial open / CLI handshake failure | Error; suggest checking the connection/firmware. |
| craft_name unreadable | Warn, fall back to --craft-name or betaflight, continue. |
| Volume never appears within timeout | Error; suggest increasing --timeout. |
btfl_all.bbl not found on volume |
Error; note what the volume contained. Leave mounted. |
| Copy failure | Error. Leave volume mounted for inspection. |
| Destination file exists | Error; refuse to overwrite. |
Future Work (out of scope)
- Optional dataflash erase after successful download.
- Interactive port selection when multiple candidates are found.
- Downloading individual per-flight logs in addition to
btfl_all.bbl.