Files
cf-test-stand/README.md
K. Isom c27d91c857 Crazyflie motor test stand GUI (cflib + DearPyGui)
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>
2026-06-23 15:12:31 -07:00

95 lines
4.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ImGuibased 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 (m1m4) · `2` all follow m1 |
| `motorPowerSet.m1..m4` | raw PWM `065535` |
| `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 **M1M4** 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/