Two-step ARM -> LAUNCH -> LAND flight flow for a Crazyflie + Flow v2 deck: - controller.py: cflib link + flight logic on one worker thread with a command queue. Supervisor arming, Kalman estimator reset/convergence wait, high-level-commander takeoff/land, configurable hover height (0.2-2.0 m), emergency stop, graceful land-on-shutdown, thread-safe snapshot()/scan(). States: DISCONNECTED/CONNECTING/READY/ARMING/ARMED/ FLYING/LANDING/EMERGENCY/ERROR. - mission_client.py: DearPyGui front-end, manual render loop polling snapshot() each frame. Context button ARM(green)->LAUNCH(blue)->LAND (yellow), secondary Disarm, hover-height slider, console log, and an Estimator state panel (x/y/z, yaw, flow counts) for drift inspection. - uv-managed (pyproject.toml + uv.lock); run `uv run mission_client.py`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.3 KiB
Crazyflie Mission Client
A small DearPyGui cockpit for flying a Bitcraze Crazyflie with a Flow v2 deck (same GUI binding as the test-stand app). Arm it and the drone lifts off to 1 m above the ground and holds position; disarm to land gently; emergency-stop to cut the motors instantly.
┌──────────────────────────────────────────────┐
│ Connection │
│ [ radio://0/80/2M/E7E7E7E7E7 ▾ ] [Scan][Connect]│
│ ──────────────────────────────────────────────│
│ Status │
│ Hovering at 1.40 m ← colour-coded │
│ Battery: 3.92 V Height: 1.41 m │
│ Flow v2 deck: attached │
│ Supervisor: armed, flying │
│ ──────────────────────────────────────────────│
│ Estimator state │
│ Pos (m): x +0.02 y -0.01 z +1.00 │
│ Yaw: +1.4 deg │
│ Flow counts: dx +5 dy -3 │
│ ──────────────────────────────────────────────│
│ Flight │
│ Hover height [====●========] 1.40 m │
│ [ LAUNCH (to 1.40 m) ] ← ARM→LAUNCH→LAND │
│ [ Disarm ] ← only while ARMED │
│ [ EMERGENCY STOP (SPACE) ] │
│ ──────────────────────────────────────────────│
│ Console [Clear] │
│ 12:00:01 I controller: [READY] … │
│ 12:00:03 I controller: [ARMED] ready to LAUNCH │
│ 12:00:08 I controller: [FLYING] Hovering 1.40 m│
│ 12:00:09 I crazyflie.console: ...fw output... │
└──────────────────────────────────────────────┘
Flight is a two-step flow with one context button:
| State | Button | Colour | Action |
|---|---|---|---|
| Ready | ARM | green | reset estimator + arm; drone idles armed on the ground |
| Armed | LAUNCH (to N m) | blue | take off to the hover height and hold |
| Flying | LAND | yellow | gentle descent + disarm |
While ARMED on the ground a separate Disarm button backs out without launching (the firmware also auto-disarms after ~30 s if you never launch). The Hover height slider (0.2–2.0 m) can be adjusted right up until LAUNCH (it latches there) and locks once airborne. Below everything, a console log of roughly the same height shows mission events, cflib messages, and firmware console output (auto-scrolls while at the bottom; Clear empties it). Use Scan to discover URIs, or type/select one in the combo before connecting.
Hardware
- Crazyflie 2.x with up-to-date firmware (protocol v7+ recommended for the supervisor arming system; older firmware falls back to the thrust-unlock path).
- Flow v2 deck mounted underneath (provides optical-flow XY + laser Z hold).
- Crazyradio PA / 2.0 plugged into the host.
- Charged battery, propellers in good shape, and clear space above the drone.
Install
This project uses uv. From autover/:
uv sync
That creates .venv/ and installs cflib and dearpygui (a self-contained
wheel with its own GL/windowing backend — no system GUI packages needed).
On Linux you also need udev permissions for the Crazyradio (see the cflib USB docs).
requirements.txtis kept for non-uv (pip install -r requirements.txt) setups;pyproject.tomlis the source of truth for uv.
Run
uv run mission_client.py
# Use a non-default radio URI:
CFLIB_URI=radio://0/80/2M/E7E7E7E7E7 uv run mission_client.py
Flying
- Connect — opens the radio link, starts telemetry, and probes for the Flow deck. (Optionally Scan first and pick a URI from the combo.)
- ARM — runs pre-flight checks (Flow deck present, battery ≥ 3.10 V, not tumbled/locked), resets and waits for the Kalman estimator to converge, and arms the motors. The drone then idles armed on the ground (props at idle) in the ARMED state; the button turns blue and reads LAUNCH. (Back out any time with Disarm; the firmware also auto-disarms after ~30 s if idle.)
- Set the hover height with the slider (0.2–2.0 m, default 1 m) — adjustable until launch — then LAUNCH: takes off to the chosen height (climb timed for a gentle ~0.35 m/s) and hovers. The button turns yellow and reads LAND.
- LAND — descends gently, stops the commander, and disarms.
- EMERGENCY STOP (button or SPACE) — cuts the motors immediately. On protocol-v7 firmware this latches the supervisor, so you must reboot the drone before it will fly again.
Closing the window (or Ctrl-C) lands the drone first (if airborne) before disconnecting.
Drift inspection
The Estimator state panel streams the estimator pose and raw optical-flow counts
(stateEstimate.x/y + stateEstimate.z, stabilizer.yaw, motion.deltaX/deltaY)
so you can diagnose a small circular/looping drift ("toilet-bowl effect"):
- Yaw creeping steadily while hovering → heading-estimate drift; the position hold corrects in a rotated frame and orbits. Power-cycle for a clean gyro start.
- x/y tracing a circle rather than jittering around zero → same orbiting hold.
- Flow counts near zero or erratic while the drone is clearly moving → poor surface/lighting for the deck (try a textured, matte, well-lit floor at ~0.7–1 m).
Safety notes
- Place the drone on a flat, level surface before arming — the estimator and supervisor expect it level and stationary.
- The Flow deck needs a textured floor and reasonable light to hold position; over a blank/reflective surface it can drift.
- This client commands a stationary hover at the configured height (0.2–2.0 m) with no horizontal movement. Keep a hand near the emergency stop (spacebar) the first few flights.
Layout
| File | Responsibility |
|---|---|
controller.py |
All flight logic + cflib link on one worker thread. |
mission_client.py |
DearPyGui front-end; polls controller.snapshot() per frame. |
requirements.txt |
Python dependencies. |
controller.py has no UI dependencies, so the flight logic can be reviewed and
driven independently of the GUI. The UI reads state through the thread-safe
snapshot() each frame and issues commands (connect/arm/disarm/
emergency_stop) that the worker thread executes.