uv-managed Python app for ramping Crazyflie 2.1 / Bolt motors on a
thrust test stand over a Crazyradio.
- cf_link.py: thread-safe cflib wrapper — scan/connect, supervisor
arming (send_arming_request) with legacy forceArm fallback, motor
override via motorPowerSet params, battery + estimator-state logging,
firmware-console capture, and a log buffer for the UI console.
- app.py: DearPyGui UI with a per-frame ramp loop.
* Linked / individual motor control, sliders + numeric entry (percent)
* Max-throttle limit, ramp rate, EMERGENCY STOP (button + SPACE)
* ARM button: disabled when disconnected, green when ready, yellow
when armed
* Telemetry: estimator-state panel (pose/yaw/flow), throttle + battery
plots that start at zero and only scroll while armed (freeze on disarm)
* Live console panel (app + cflib + firmware messages)
Benign cflib warnings (stale log blocks, missing optional params) are
filtered; safety teardown disarms on disconnect/quit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.0 KiB
test-stand
A small GUI for ramping Crazyflie motors on a thrust/verification test
stand. Connect to a Crazyflie 2.1 (cf21) or Bolt (cfbl) over a Crazyradio,
arm the motor override, and ramp motors individually or all together with a
configurable rate and a hard max-throttle limit.
Built with cflib (the official Crazyflie Python library) for comms and DearPyGui (a Dear ImGui–based UI) for the front-end.
Why Python and not C++? There is no official, full-featured C++ Crazyflie API. Bitcraze's only official C++ component is
crazyflie-link-cpp(the radio link layer, which ships Python bindings). The complete param/logging API needed for motor control is Python-first viacflib; the C++ alternatives are third-party and lag firmware. DearPyGui keeps the imgui feel while letting the comms stay on the supported path.
How motor control works
The firmware exposes a motor-override param group used for exactly this kind of bench testing:
| Param | Meaning |
|---|---|
motorPowerSet.enable |
0 off · 1 per-motor (m1–m4) · 2 all follow m1 |
motorPowerSet.m1..m4 |
raw PWM 0–65535 |
system.forceArm = 1 |
keeps motors armed while stationary (recent fw) |
motor.batCompensation = 0 |
PWM maps directly to output (repeatable tests) |
"Linked" mode uses enable = 2; "Individual" mode uses enable = 1. Missing
params on older firmware are reported in the UI and otherwise skipped.
Setup
Managed with uv. Requires Python 3.10+ and a Crazyradio (PA) dongle.
# from the repo root — creates .venv and installs deps from pyproject.toml
uv sync
On Linux, allow non-root USB access to the radio (one-time) if you get permission errors — see Bitcraze's USB permissions guide.
No uv? Install with
pip install -r requirements.txtinto a virtualenv and runpython main.pyinstead.
Run
uv run test-stand # or: uv run main.py
- Scan → pick the URI (e.g.
radio://0/80/2M/E7E7E7E7E7) → Connect. - Set a sensible Max throttle limit and Ramp rate first.
- ARM.
- Move the Master slider (Linked) or per-motor M1–M4 sliders (Individual). Output ramps smoothly toward the slider target; live throttle and battery are plotted.
Safety
- Motors never move until you press ARM, and throttle starts at 0.
- The max-throttle limit clamps every target.
- EMERGENCY STOP (button or the SPACE key) cuts all motors instantly.
- Disconnecting, losing the link, or quitting the app disarms automatically.
- Always secure the drone/motors to the stand before arming.
Troubleshooting
Unable to find variable [system.forceArm]/[motor.batCompensation]— harmless. These legacy params don't exist on current firmware; the app arms through the supervisor (a platform arming request) instead and sets those params only if present. The motors are driven bymotorPowerSet.*, which is what matters. (The app no longer emits this line, but older builds did.)Error no LogEntry to handle id=N— a benign cflib warning: the firmware is streaming a stale log block left over from a previous run that didn't disconnect cleanly, or from another client (e.g. cfclient) connected to the same Crazyflie. It doesn't affect motor control. The app filters this line, but to clear it at the source: close any other client and power-cycle the Crazyflie.
Layout
main.py entry point
test_stand/cf_link.py cflib wrapper: connect, arm, motor PWM, telemetry
test_stand/app.py DearPyGui UI + per-frame ramp loop