Minimal terminal wrapper around Betaflight flight-controller config management. Similar spirit to the Betaflight Configurator browser app, but focused on the bare essentials:
- find serial ports / auto-detect the FC, including a named registry for a fleet of drones
- connect over USB serial (CLI text protocol)
- pull configs (
diff all/dump all) and save them underconfigs/— one FC or--allof them - push a saved config back to the drone, auto-backing up the live config first
- diff two local configs, or a board's saved backup history over time
- list / enable / disable feature flags
- read/write CLI variables, run raw commands, interactive shell
Use at your own risk. This tool writes directly to your flight controller's CLI. Provided as-is with no warranty — always take props off before connecting, verify a config before pushing it, and keep backups (see Safety notes).
cd betaflight_config
make install # creates .venv and installs bfctl (editable)
source .venv/bin/activateOr without Make:
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Useful Make targets: make help, make check (offline tests: scripts/check.py
pytest, no FC required),make doctor,make ports,make status PORT=STM32G473,make pull PORT=…,make clean.
Optional tab-completion (commands, flags, --port fragments):
pip install -e ".[completion]"
eval "$(register-python-argcomplete bfctl)" # add to ~/.bashrc or ~/.zshrc# List serial ports (* = FC, S = sticky default)
bfctl ports
# FC status / version (see "Multiple drones" below for how the target is picked)
bfctl status
bfctl status --port TUNERCF405 # skip prompt
# Pull config (recommended: diff of non-defaults across all profiles)
# Saves as configs/TUNERCF405.txt (board_name from the FC)
bfctl pull
bfctl pull --timestamp # configs/TUNERCF405-diff-<utc>.txt
bfctl pull --mode dump --name my-freestyle-5inch
bfctl pull --all # snapshot every connected FC in one go (always timestamped)
# List local backups
bfctl list
# Push a config back (props OFF; confirm prompts)
# Uses device tags in the file (usb_serial / board_name) to pick the right FC,
# shows a live change preview, backs up the live config, then applies.
# `save` makes the FC reboot itself.
bfctl push configs/TUNERCF405.txt
bfctl push TUNERCF405.txt --yes
bfctl push TUNERCF405.txt --force # override version/board hard fails
bfctl push TUNERCF405.txt --no-reboot # prefer save noreboot if firmware supports it
bfctl push TUNERCF405.txt --verify # after reboot, re-dump and compare
bfctl push TUNERCF405.txt --no-preview # skip pre-push change diff
bfctl push TUNERCF405.txt --no-backup # skip the automatic pre-push backup
# Standalone verify (optional --wait after a manual reboot)
bfctl verify TUNERCF405.txt
bfctl verify TUNERCF405.txt --wait --timeout 120
# Local diffing (no FC needed)
bfctl diff configs/TUNERCF405-diff-A.txt configs/TUNERCF405-diff-B.txt
bfctl history TUNERCF405 # chronological diff across TUNERCF405's timestamped backups
bfctl history TUNERCF405 --mode dump
# Features
bfctl features
bfctl feature enable LED_STRIP
bfctl feature disable SOFTSERIAL --save
# Variables
bfctl get p_roll
bfctl set p_roll=45
bfctl set gyro_lpf1_static_hz=0 --save
# Raw CLI / interactive
bfctl cmd version
bfctl cmd -- diff all
bfctl shellAuto-detects the first port whose product/manufacturer looks like Betaflight. Override with --port /dev/cu.usbmodem….
- Never push a dump/diff from a different firmware version — variable names and ranges change and can silently corrupt settings.
bfctl pushruns a metadata compatibility check (board name + firmware major/minor from dump headers vs the live FC) and blocks on hard mismatches unless you pass--force.- That check is not a full CLI schema validator: individual
setlines are only validated by the FC while applying. Unknown settings may error mid-push. - Prefer
diff allbackups over fulldump allfor day-to-day restore. bfctl pushbacks up the live config before applying, by default (a fulldump all, same filebfctl pull --mode dump --timestampwould produce). Pass--no-backupto skip.bfctl history <board>shows what changed across a board's saved backups over time.- Take props off before connecting or writing.
push --defaultsrunsdefaults nosavefirst (factory reset of config) — destructive.saveis what reboots the FC (Betaflight writes EEPROM then reboots). bfctl only sends that CLI command; a USB disconnect right after is expected and treated as success.push --verify/bfctl verify: wait for USB to return, pull a livedump all, and check that every setting from the local file is present with the same value. Reports missing/mismatched lines (failed applies). This is expected⊆live comparison, not a byte-identical file diff.
With more than one FC plugged in, bfctl needs to pick a target port. It resolves one in priority order, stopping at the first hit:
--port— explicit path (/dev/cu.usbmodem…), a registered device nickname (see below), a serial number, or a product/name fragment (e.g.TUNERCF405). Never prompts. If a fragment matches more than one connected FC, bfctl refuses to guess (fails loudly rather than picking one).- Device tags on the file (
pushonly) —bfctl pullstamps every saved config withusb_serial/usb_product/board_namein its header. Pushing that file back auto-targets the FC it was pulled from, even with other drones connected — you'll see aTarget board:line confirming the match before anything is sent.bfctl verifydoesn't read these tags; it falls through to sticky/single-FC like everything else. - Sticky default —
bfctl selectremembers a device (by serial number, falling back to product/board name) in.bfctl/state.json. Every later command uses it automatically as long as that FC is plugged in. - Single FC — if only one is connected, it's used with no extra flags needed.
- Interactive prompt — multiple FCs, no tag/sticky/
--portmatch: bfctl lists them and asks (non-interactive shells get a clear error instead of hanging).
bfctl select # interactive picker
bfctl select STM32G473 # by product / path / serial, no prompt
bfctl select --show # what's sticky, and is it currently plugged in
bfctl select --clear # go back to prompt-per-commandbfctl select only remembers one default at a time. For an actual fleet, register each drone under a nickname — --port <nickname> then resolves it regardless of which physical port it enumerates on:
bfctl devices add racer --port TUNERCF405 # registers whatever --port resolves to
bfctl devices add freestyle5 # or resolve interactively/via sticky, then register
bfctl devices list # nicknames + current plugged-in status
bfctl devices remove racer
bfctl status --port racer # works from any command that takes --port
bfctl push configs/racer.txt --port racerUses the same text CLI the Configurator “CLI” tab uses:
- Open serial @ 115200
- Send
#to enter CLI - Run commands (
diff all,feature,set …, …) - Optionally
save/exit noreboot
Firmware build options (compile-time USE_* / radio protocol flags from cloud build) are only partially visible over CLI. bfctl status prints them when the firmware exposes them; otherwise use Configurator’s firmware flasher / cloud build UI for full option sets. Runtime feature flags (feature GPS, etc.) are fully supported.
bfctl/ Python package
configs/ Local dumps saved by `bfctl pull`
tests/ pytest suite (offline, no FC required)
scripts/ check.py — pure-function smoke checks run by `make check`