# 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 via `cflib`; 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. ```bash # 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][udev]. > No uv? Install with `pip install -r requirements.txt` into a virtualenv and > run `python main.py` instead. ## Run ```bash uv run test-stand # or: uv run main.py ``` 1. **Scan** → pick the URI (e.g. `radio://0/80/2M/E7E7E7E7E7`) → **Connect**. 2. Set a sensible **Max throttle limit** and **Ramp rate** first. 3. **ARM**. 4. 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 by `motorPowerSet.*`, 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 ``` [uv]: https://docs.astral.sh/uv/ [cflib]: https://github.com/bitcraze/crazyflie-lib-python [DearPyGui]: https://github.com/hoffstadt/DearPyGui [udev]: https://www.bitcraze.io/documentation/repository/crazyflie-lib-python/master/installation/usb_permissions/