# 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: `__.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 1. **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`. `--port` bypasses the scan. 2. **Read craft_name.** Open serial at 115200 8N1. Enter Betaflight CLI (send `#`, wait for the CLI banner/prompt). Send `get craft_name`, parse the `craft_name = X` line. This happens *before* msc because msc reboots the FC. If unset/empty and no `--craft-name`, fall back to `betaflight`. 3. **Snapshot mounts.** Record the current set of mounted volumes (native; see below) so the new volume can be identified by difference. 4. **Enter MSC.** Send `msc`. The FC reboots and re-enumerates as a USB mass-storage device; the serial port disappears. 5. **Wait for volume.** Poll (up to `--timeout`) for a *new* mounted volume (present in the after-set, absent from the before-set) that actually contains `btfl_all.bbl`. macOS auto-mounts via `diskarbitrationd`; Linux relies on the desktop automounter. Identifying by "new volume containing the file" avoids guessing the FAT volume label. 6. **Copy + rename.** `std::filesystem::copy_file` the `btfl_all.bbl` to `/__.bbl`. Timestamp is local wall-clock at extraction time. Refuse to overwrite an existing destination file. 7. **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)` → `__.bbl` using 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 links `DiskArbitration` and `CoreFoundation` frameworks; no third-party libraries on either platform. - **Platform seam:** `#ifdef`-guarded implementations inside `mounts.cpp` and `eject.cpp`; `serial.cpp` is 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`.