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>
95 lines
4.0 KiB
Markdown
95 lines
4.0 KiB
Markdown
# 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/
|