Implement bfebbx: Betaflight blackbox extractor
Cross-platform (macOS + Linux) C++17 CLI that extracts a Betaflight FC's blackbox log in one command: auto-detect the serial port, read craft_name over the Betaflight CLI, send `msc` to reboot into USB mass storage, wait for the volume to mount, copy btfl_all.bbl to <craft_name>_<YYYYMMDD>_<HHMMSS>.bbl, and eject. Native throughout (termios serial, getmntinfo/proc-mounts mount detection, DiskArbitration/umount eject); the only subprocess is the Linux udisksctl eject fallback. Includes CMake/ctest build, unit tests for the pure logic (naming, craft_name parsing, mount diff, serial via pty, CLI parsing), README with a manual hardware integration procedure, and design/plan docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "betaflight.h"
|
||||
#include "cli.h"
|
||||
#include "eject.h"
|
||||
#include "mounts.h"
|
||||
#include "naming.h"
|
||||
#include "serial.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using namespace bfebbx;
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kUsage =
|
||||
"usage: bfebbx [--port PORT] [--output DIR] [--craft-name NAME] "
|
||||
"[--timeout SEC] [-v]\n";
|
||||
|
||||
// Exit codes (documented in README).
|
||||
enum Exit {
|
||||
OK = 0,
|
||||
NO_PORT = 2,
|
||||
AMBIGUOUS_PORT = 3,
|
||||
SERIAL_FAIL = 4,
|
||||
VOLUME_TIMEOUT = 5,
|
||||
FILE_MISSING = 6,
|
||||
COPY_FAIL = 7,
|
||||
DEST_EXISTS = 8,
|
||||
USAGE = 64,
|
||||
};
|
||||
|
||||
std::string resolvePort(const Options& opt) {
|
||||
if (!opt.port.empty()) return opt.port;
|
||||
auto ports = scanPorts("/dev", defaultPortPrefix());
|
||||
if (ports.empty()) {
|
||||
std::cerr << "error: no Betaflight serial port found (looked for /dev/"
|
||||
<< defaultPortPrefix() << "*). Use --port.\n";
|
||||
std::exit(NO_PORT);
|
||||
}
|
||||
if (ports.size() > 1) {
|
||||
std::cerr << "error: multiple candidate ports; pass --port:\n";
|
||||
for (const auto& p : ports) std::cerr << " " << p << "\n";
|
||||
std::exit(AMBIGUOUS_PORT);
|
||||
}
|
||||
return ports.front();
|
||||
}
|
||||
|
||||
// Poll for a newly-mounted volume that contains btfl_all.bbl.
|
||||
MountEntry waitForVolume(const std::vector<MountEntry>& before, int timeoutSec,
|
||||
bool verbose) {
|
||||
using clock = std::chrono::steady_clock;
|
||||
auto deadline = clock::now() + std::chrono::seconds(timeoutSec);
|
||||
while (clock::now() < deadline) {
|
||||
auto added = diffMounts(before, listMounts());
|
||||
for (const auto& m : added) {
|
||||
fs::path candidate = fs::path(m.mountpoint) / "btfl_all.bbl";
|
||||
std::error_code ec;
|
||||
if (fs::exists(candidate, ec)) {
|
||||
if (verbose) std::cerr << "found volume: " << m.mountpoint << "\n";
|
||||
return m;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
std::cerr << "error: mass-storage volume did not appear within " << timeoutSec
|
||||
<< "s (try --timeout).\n";
|
||||
std::exit(VOLUME_TIMEOUT);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Options opt;
|
||||
try {
|
||||
opt = parseArgs(argc, argv);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "error: " << e.what() << "\n" << kUsage;
|
||||
return USAGE;
|
||||
}
|
||||
if (opt.help) {
|
||||
std::cout << kUsage;
|
||||
return OK;
|
||||
}
|
||||
|
||||
const std::string port = resolvePort(opt);
|
||||
if (opt.verbose) std::cerr << "port: " << port << "\n";
|
||||
|
||||
// 1. Read craft_name over the CLI (before msc reboots the board).
|
||||
std::string craft = opt.craftName;
|
||||
try {
|
||||
Serial s = Serial::openPort(port, 115200);
|
||||
enterCli(s);
|
||||
auto name = getCraftName(s, std::chrono::milliseconds(2000));
|
||||
if (craft.empty() && name.has_value()) craft = name.value();
|
||||
|
||||
// 2. Snapshot mounts, then trigger mass-storage mode.
|
||||
auto before = listMounts();
|
||||
if (opt.verbose) std::cerr << "entering mass-storage mode...\n";
|
||||
enterMsc(s);
|
||||
// Ensure the msc reboot trigger is actually transmitted before we
|
||||
// release the fd, since a non-blocking fd offers no such guarantee.
|
||||
s.drain();
|
||||
// Serial port goes away as the FC reboots; drop it before polling.
|
||||
s = Serial::fromFd(-1);
|
||||
|
||||
// 3. Wait for the volume.
|
||||
MountEntry vol = waitForVolume(before, opt.timeoutSec, opt.verbose);
|
||||
|
||||
// 4. Build destination name and copy.
|
||||
std::string sanitized = sanitizeCraftName(craft);
|
||||
if (sanitized.empty()) {
|
||||
sanitized = "betaflight";
|
||||
if (craft.empty()) {
|
||||
std::cerr << "warning: could not read craft_name; using '" << sanitized
|
||||
<< "'\n";
|
||||
}
|
||||
}
|
||||
std::string fname = buildFilename(sanitized, std::time(nullptr));
|
||||
fs::path dest = fs::path(opt.output) / fname;
|
||||
|
||||
std::error_code ec;
|
||||
if (fs::exists(dest, ec)) {
|
||||
std::cerr << "error: destination already exists: " << dest << "\n";
|
||||
return DEST_EXISTS;
|
||||
}
|
||||
fs::path src = fs::path(vol.mountpoint) / "btfl_all.bbl";
|
||||
if (!fs::exists(src, ec)) {
|
||||
std::cerr << "error: btfl_all.bbl not found on " << vol.mountpoint
|
||||
<< " (leaving volume mounted).\n";
|
||||
return FILE_MISSING;
|
||||
}
|
||||
fs::copy_file(src, dest, ec);
|
||||
if (ec) {
|
||||
std::cerr << "error: copy failed: " << ec.message()
|
||||
<< " (leaving volume mounted).\n";
|
||||
return COPY_FAIL;
|
||||
}
|
||||
std::cout << "wrote " << dest << "\n";
|
||||
|
||||
// 5. Eject on success.
|
||||
try {
|
||||
ejectVolume(vol);
|
||||
if (opt.verbose) std::cerr << "ejected " << vol.mountpoint << "\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "warning: eject failed: " << e.what()
|
||||
<< " (file already saved; unmount manually).\n";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "error: " << e.what() << "\n";
|
||||
return SERIAL_FAIL;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
Reference in New Issue
Block a user