Files
bfebbx/docs/superpowers/specs/2026-07-22-bfebbx-design.md
Kyle Isom be169d1c2a Add bfebbx design doc
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>
2026-07-22 01:41:10 -07:00

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

  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 <output>/<craft_name>_<YYYYMMDD>_<HHMMSS>.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-namebetaflight.
  • buildFilename(name, time_t)<name>_<YYYYMMDD>_<HHMMSS>.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.