DearPyGui app that visualizes the Multi-ranger ToF sensors and builds a top-down map of the drone's surroundings. Handheld and experimental hardened-scan capture, Flow-deck drift correction (ZUPT), props-off dry-run mode, and optional 3D point-cloud streaming to CloudView. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
212 lines
12 KiB
Markdown
212 lines
12 KiB
Markdown
# Crazyflie RangeView
|
||
|
||
A small [DearPyGui](https://github.com/hoffstadt/DearPyGui) tool (same GUI
|
||
binding as the test-stand and mission clients) that visualises the
|
||
**Multi-ranger** deck's five Time-of-Flight sensors on a Bitcraze **Crazyflie**
|
||
and builds a top-down **map of the drone's immediate surroundings**.
|
||
|
||
It has two independent parts:
|
||
|
||
1. **Mapping** - each ToF distance is projected through the drone's position
|
||
estimate (Flow v2 deck) into a world-frame point cloud. This needs only
|
||
pose + range streaming, so it works **whether the drone is flying or you are
|
||
holding it in your hand and walking it around**.
|
||
2. **Flight (automated scan)** - an optional capture profile that flies the
|
||
drone itself: arm, lift off, then **ascend slowly while rotating in place** so
|
||
the sensors sweep a full circle hands-free.
|
||
|
||
```
|
||
┌─────────────────────────────┬──────────────────────────────────────┐
|
||
│ Connection [radio...][Scan][⏻] │ Map (top-down) extent +/- 3m │
|
||
│ ─────────────────────────── │ ┌────────────────────────────────┐ │
|
||
│ Status: Connected - ready │ │ · ·· ··· │ │
|
||
│ Battery 3.9V Height 0.00m │ │ · · │ │
|
||
│ Multi-ranger attached │ │ · ╲front / · │ │
|
||
│ Flow v2 attached │ │ · left●--●--●right · │ │
|
||
│ Pos x+0.0 y+0.0 z+0.0 Yaw 0 │ │ · (drone) · │ │
|
||
│ ─────────────────────────── │ │ · · │ │
|
||
│ ToF (m) F:1.8 B:2.1 L:0.9 R... │ │ ··· ·· · │ │
|
||
│ Up:2.4 Down:0.03 │ └────────────────────────────────┘ │
|
||
│ ─────────────────────────── │ │
|
||
│ Mapping [✓ Record] │ │
|
||
│ [Reset origin] [Clear map] │ │
|
||
│ Points: 1843 │ │
|
||
│ ─────────────────────────── │ │
|
||
│ Flight (automated scan) │ │
|
||
│ Start 0.40m Top 1.50m │ │
|
||
│ Climb 0.15m/s Yaw 45°/s │ │
|
||
│ [ ARM -> SCAN -> LAND ] │ │
|
||
│ [Disarm] [EMERGENCY (SPACE)] │ │
|
||
│ ─────────────────────────── │ │
|
||
│ Console ... │ │
|
||
└─────────────────────────────┴──────────────────────────────────────┘
|
||
```
|
||
|
||
## Hardware
|
||
|
||
- Crazyflie (brushless or standard) with current firmware.
|
||
- **Flow v2 deck** - supplies the position/altitude estimate the map is plotted
|
||
against, and is required to arm.
|
||
- **Multi-ranger deck** - the five VL53L1x ToF sensors (front / back / left /
|
||
right / up). The Flow deck's down-facing z-ranger provides the floor distance.
|
||
|
||
## Install & run
|
||
|
||
```sh
|
||
# with uv
|
||
uv run main.py
|
||
|
||
# or a plain venv
|
||
python -m venv .venv && . .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
python -m rangeview # or: python main.py
|
||
|
||
# custom radio URI
|
||
CFLIB_URI=radio://0/80/2M/E7E7E7E7E7 python -m rangeview
|
||
```
|
||
|
||
## How the map is built
|
||
|
||
The Multi-ranger reports a distance per sensor in millimetres. Each horizontal
|
||
sensor points along a fixed body axis (front `+x`, back `−x`, left `+y`, right
|
||
`−y`). For every sample, RangeView rotates that body direction by the drone's
|
||
estimated yaw and offsets it from the estimated `(x, y)` position, giving the
|
||
world-frame coordinate of the obstacle the beam hit:
|
||
|
||
```
|
||
yaw = radians(stabilizer.yaw)
|
||
wx = cos(yaw)·bx − sin(yaw)·by # body dir -> world dir
|
||
wy = sin(yaw)·bx + cos(yaw)·by
|
||
point = (stateEstimate.x + wx·d, stateEstimate.y + wy·d)
|
||
```
|
||
|
||
Points accumulate into the cloud while **Record** is on, but only once the drone
|
||
has actually moved (≥2 cm, ≥5 cm in height, or ≥2°), so a stationary drone never
|
||
dumps noise into the map. Readings at or beyond the sensor's ~4 m ceiling are
|
||
treated as "nothing there" and skipped, so empty directions don't pollute the
|
||
map. The world origin is wherever the estimator was last reset.
|
||
|
||
The in-app map is the 2D top-down **horizontal slice** (the four side sensors) —
|
||
a quick live diagnostic. The points are world-anchored: each is placed using the
|
||
drone's estimated pose *at the moment it was measured*, so the cloud stays fixed
|
||
in space while the drone marker moves through it (accuracy is bound by the
|
||
position estimate — drift smears it).
|
||
|
||
## 3D view (CloudView)
|
||
|
||
For a real 3D point cloud, RangeView streams the **full cloud** — horizontal
|
||
hits at the drone's height, plus ceiling/floor points from the up/down sensors —
|
||
to the standalone **[CloudView](../cloudview)** viewer over a socket. Tick
|
||
**Stream 3D to** and give the viewer's address (default
|
||
`tcp://127.0.0.1:9870`; `unix:///path` also works). Start the viewer with
|
||
`python -m cloudview` in the `cloudview/` project first, or any time after — the
|
||
publisher reconnects and replays the whole cloud, and the stream is best-effort
|
||
so it never blocks flight. The 2D map keeps working regardless; CloudView is a
|
||
generic viewer that other robots can feed too (see its README for the protocol).
|
||
|
||
## Drift correction
|
||
|
||
A Crazyflie with a Flow v2 deck **drifts even when it is sitting perfectly
|
||
still** - there's no fix in the firmware estimator for this. Two things cause it:
|
||
|
||
- The firmware Kalman filter integrates Flow-deck velocity continuously from the
|
||
moment it powers on. With nothing to correct it, a freshly-connected drone can
|
||
report a position **tens of metres** from where it actually is (on the bench
|
||
here it read +78.9 m, −33.7 m). RangeView therefore **resets the estimator
|
||
automatically on connect** so you always start from a clean `(0, 0)` origin -
|
||
and you can re-zero any time with **Reset origin**.
|
||
- Even after a reset, optical-flow noise keeps integrating into position and gyro
|
||
bias into yaw, so the estimate slowly wanders (~a few mm per second at rest).
|
||
|
||
To cancel the residual wander, RangeView applies a **zero-velocity update
|
||
(ZUPT)**, toggled by the **Drift correction (hold pose when still)** checkbox
|
||
(on by default). It detects "at rest" from the *raw* gyro rate and optical-flow
|
||
counts - signals that read ~0 when the drone is genuinely still, independent of
|
||
the drifting estimate - and while at rest it pins the corrected pose in place,
|
||
folding the wander into an offset instead of letting it move the map. When you
|
||
pick the drone up and move it, real motion passes straight through; when you set
|
||
it down or pause, the drift is re-absorbed. The **Motion** line shows the current
|
||
state and how much drift is being held off. Measured on the bench: over 15 s
|
||
stationary the raw estimate drifted ~0.4 cm while the corrected pose held at
|
||
0.0 cm.
|
||
|
||
### Workflow A - handheld mapping (no flight)
|
||
|
||
1. Connect.
|
||
2. Place/hold the drone where you want `(0, 0)` and press **Reset origin** (this
|
||
resets the Kalman estimator and clears the map).
|
||
3. Make sure **Record** is checked, then slowly carry the drone through the space
|
||
- keep it roughly level so the Flow deck tracks well. Walls fill in live.
|
||
|
||
> The Flow v2 estimate drifts over long, fast, or feature-poor moves (it needs
|
||
> texture under the down-facing camera and a return for the z-ranger). Move
|
||
> smoothly and reset the origin if the map starts to smear.
|
||
|
||
### Workflow B - automated scan (EXPERIMENTAL)
|
||
|
||
> **A continuously-rotating ascent on a Flow deck caused a flyaway** (the drone
|
||
> bounced off the walls, ceiling, and floor). A Flow deck cannot hold position
|
||
> while yaw-rotating: spinning the optical-flow sensor injects phantom motion,
|
||
> the firmware Kalman estimate diverges, and the position controller chases it
|
||
> into obstacles. The client-side **drift correction only fixes the displayed
|
||
> pose - the firmware flies on its own (diverging) estimate**, so it does *not*
|
||
> make flight safer.
|
||
>
|
||
> The scan was therefore redesigned to be as gentle as possible, but rotation on
|
||
> a Flow deck remains inherently unstable. **Bench-test with the props off,
|
||
> then tethered, before any free flight, and keep the area clear and SPACE
|
||
> (emergency stop) ready.**
|
||
|
||
The hardened profile never spins continuously:
|
||
|
||
1. Set **Start height**, **Top height**, **Climb rate**, and **Yaw rate** (the
|
||
rotation speed of each step, hard-capped at 30 °/s). Defaults are conservative.
|
||
2. Place the drone level on the floor with clear space all around.
|
||
3. Press **ARM**, then **SCAN (!)**. The drone:
|
||
- takes off with the firmware **high-level commander** (which holds position
|
||
autonomously, so a slow UI can't drop it) and **settles into a stable hover**
|
||
before doing anything else;
|
||
- rotates one full turn in discrete **~30° "stop-and-stare" steps**, pausing at
|
||
each bearing so the estimate re-converges and the sensors take a clean
|
||
static reading;
|
||
- climbs to the next ring (~0.3 m up) and repeats, up to the top height;
|
||
- lands automatically when done.
|
||
|
||
It **aborts to a controlled landing** if the estimate starts to diverge (any
|
||
sustained horizontal speed while it's supposed to be holding still), if a wall
|
||
comes within 25 cm, or if it loses the flying state, and **emergency-stops** on a
|
||
tumble/crash. The climb is also **capped below the ceiling** sensed by the
|
||
up-facing ToF. Press **LAND** any time to bring it down early; **SPACE** cuts the
|
||
motors instantly.
|
||
|
||
### Dry run (props off, no motors)
|
||
|
||
Tick **Dry run (props off ...)** before arming to validate the whole sequence on
|
||
the bench with **zero flight risk**. In dry run the controller walks the entire
|
||
scan state machine - takeoff, every stop-and-stare go-to bearing, ring climbs,
|
||
and landing - and logs each decision to the console (`[DRY RUN] ...`), but it
|
||
**never arms the motors and never sends a flight command**. Live sensor data
|
||
still drives the safety guards, so you can also confirm an abort fires (e.g.
|
||
hold a hand within 25 cm of a horizontal sensor and watch it abort). Run this
|
||
with the props off first; only after the sequence looks right should you remove
|
||
dry run and fly tethered.
|
||
|
||
## Safety
|
||
|
||
- The drone never spins up until you press **ARM**, and arming is refused
|
||
without a Flow v2 deck, on low battery, or if the supervisor reports the drone
|
||
tumbled/locked.
|
||
- **EMERGENCY STOP** (button or **SPACE**) cuts the motors instantly from any
|
||
state. A real emergency stop requires a reboot before flying again.
|
||
- All radio I/O runs on a dedicated worker thread that streams hover setpoints at
|
||
10 Hz, so a slow or frozen UI can't make the drone drift or fall. Closing the
|
||
window lands the drone (if airborne) and closes the link cleanly.
|
||
|
||
## Layout
|
||
|
||
- `rangeview/controller.py` - threaded Crazyflie link: connection, logging of
|
||
`range.*` + `stateEstimate.*` + supervisor status, the arm/scan/land flight
|
||
profile, and the world-frame map accumulation.
|
||
- `rangeview/app.py` - DearPyGui front-end: telemetry readouts, the mapping and
|
||
flight controls, the top-down map plot, and the firmware console.
|