# cu-flight-controller
A bare-metal quadcopter flight controller implemented end-to-end using Copper components.
## Overview
This example demonstrates a complete flight controller running on the MicoAir H743 board (STM32H743), plus a separate host Copper runtime intended for onboard compute. It showcases Copper's ability to run deterministic, real-time control loops on embedded hardware with zero dynamic allocation during runtime.
The deployment has one multi-Copper root, `flight_controller.ron`, with independent MCU and
compute subsystems. Cargo features select coherent graph fragments inside each subsystem: firmware
and BevyMon use the manual/preview fragments, while `sim` and `end2end` select the closed-loop
autonomy fragments and typed Zenoh interconnects.
The simulator runs these same graphs in `sim_mode`; it replaces external hardware and transport
at the graph boundaries without changing the application topology.
The real camera is enabled by the `end2end` feature and uses `cu_zed::Zed`. The simulator starts both subsystem runtimes in one Bevy process, preempts the hardware ZED source, and transfers the normal typed bridge messages through a simulator-owned link. On a deployed system the compute-side Copper Zenoh bridge listens on `udp/0.0.0.0:7447` and uses bincode for the two fixed-size messages. Binding a no-std UDP network resource and Zenoh backend on the H743 is intentionally deferred; the firmware graph remains buildable without pretending that transport exists.
## Hardware
- **MCU**: STM32H743VIT @ 400MHz
- **IMU**: BMI088 (accelerometer + gyroscope)
- **RC Input**: CRSF protocol (ExpressLRS compatible)
- **ESC Output**: BDShot (bidirectional DShot with telemetry)
- **VTX/OSD**: MSP DisplayPort protocol
- **Storage**: MicroSD card for logging
- **Battery**: ADC voltage monitoring
See [doc/PINOUT.md](doc/PINOUT.md) for the complete pinout reference.
## Architecture
Manual and AUTO commands join at one MCU-side mode supervisor before the existing cascaded controller:
```
GNSS + AHRS + heading -> navigation -> round-trip mission -> MCU context bridge
|
v
ZED -> ViTFly (compute)
|
v
RC Input -> RC Mapper -------------------------------> mode supervisor -> attitude -> rate -> mixers
\-> AUTO mission/controller <- MCU command bridge ---/
```
### Task Graph
| Task | Description |
|------|-------------|
| `bmi088` | BMI088 IMU driver (accelerometer + gyroscope) |
| `imu_cal` | Gyroscope bias calibration on arm |
| `ahrs` | Attitude and Heading Reference System |
| `mapper` | RC channel mapping and arm/mode logic |
| `navigation` | Latches GNSS, AHRS, and true heading into a freshness-checked navigation state |
| `auto_mission` | Latches home and a point 500 m ahead / 8 m up, then alternates forever |
| `autonomy_context` | Publishes the fixed-size ViTFly context at approximately 30 Hz |
| `auto_controller` | Tracks fresh ViTFly world-frame `[north, west, up]` commands toward the current waypoint |
| `mode_supervisor` | Selects manual or AUTO controls before the cascaded flight controller |
| `attitude` | Outer loop PID (angle to rate setpoint) |
| `rate` | Inner loop PID (rate to motor commands) |
| `mixer0-3` | QuadX motor mixing |
| `battery_adc` | Battery voltage monitoring |
| `vtx_osd` | MSP DisplayPort OSD rendering |
| `led_blink` | Status LED heartbeat |
### Flight Modes
- **Angle**: Self-leveling mode with attitude hold
- **Acro**: Rate mode for aerobatic flight
- **Position Hold**: (placeholder for GPS integration)
- **Auto**: SC middle position. Each entry latches the current position/heading, creates a target
500 m ahead at current MSL altitude + 8 m, and alternates between that point and home forever.
SC high remains reserved for forced recovery.
### Features
- **Airmode**: Maintains control authority at zero throttle for aerobatic maneuvers
- **Expo curves**: Configurable stick expo for smoother control
- **Gyro calibration**: Automatic bias calibration on arm
- **Zero-copy logging**: Binary logs to SD card via unified logger
## Building
### Task graph
```bash
just dag
```
This renders the complete simulated deployment using the `default` mission. Both selectors remain
overridable, for example with `just dag mission=flow` or `just dag features=end2end`.
### Distributed subsystem compile check
```bash
just subsystems-check
```
This checks the real ZED/ViTFly/Zenoh compute graph and the current buildable STM32 firmware graph.
The autonomy MCU graph becomes the firmware entrypoint once its no-std UDP resource
and bridge backend are bound.
To compile or run the real ZED-enabled compute graph:
```bash
just end2end-check
just end2end
```
### Firmware (for flashing to hardware)
```bash
# Build and flash with text logging (debug)
just fw
# Build and flash release (optimized, no text logs)
just fwr
# Build and flash debug profile
just fwd
```
### Log Reader (host tool)
```bash
# Extract CopperLists from log file
just logreader log=logs/embedded.copper
# Check log file integrity
just fsck log=logs/embedded.copper
# Extract text logs (requires log index)
just textlogs log=logs/embedded.copper
```
### Replay Debug Server
To replay a recorded simulation log through the remote debug API:
```bash
cd examples/cu_flight_controller
just resim-mcu-debug
```
To inspect the compute-side log in Time Traveler—including the depth raster
that was actually passed to ViTFly—run:
```bash
# Generate a flight_compute_sim.copper log, then exit the simulator.
just sim
# Serve that compute-side log to Time Traveler.
just resim-compute-debug
```
Then attach Time Traveler to
`copper/examples/cu_flight_controller/compute/debug/v1`.
The replay target uses the standardized replay contract:
- `--debug-base` selects the remote debug namespace
- `--log-base` selects the recorded Copper log to inspect
- `--replay-log-base` selects the per-session replay log template
If you are building another replay target, follow the same pattern with
`cu29::replay::{ReplayCli, ReplayDefaults, per_session_replay_log_base, serve_remote_debug}`.
### Python GNSS Extraction
You can use the PyO3 bindings to iterate CopperLists directly from Python and
extract GNSS fields without going through JSON.
This is an offline log-analysis workflow, not a runtime Python task. Python only
touches data that Copper has already recorded, so this does not affect the
realtime behavior of the flight controller itself.
```bash
# Build the Python extension module
just py-build
# Print GNSS latitude/longitude from the flight-controller sim log
just py-gnss log=logs/flight_controller_sim.copper
```
The script is at `python/print_gnss_from_log.py` and can also be run directly:
```bash
python3 python/print_gnss_from_log.py logs/flight_controller_sim.copper
```
Implementation notes:
- `src/python_module.rs` exposes an app-specific `#[pymodule]`
- it uses `gen_cumsgs!("mcu_config.ron")` so the CopperList type matches this app
- the Python script imports that module and iterates typed CopperLists plus runtime lifecycle records
This pattern is the recommended Python story in Copper: post-process logs in Python
after the run, keep Python off the control path during the run.
### RC Tester (simulation)
```bash
# Test RC input via joystick
just rc
```
### Simulator (Bevy + Copper)
```bash
# Run the normal full-window simulator with MCU + compute Copper runtimes
just
# Run the split BevyMon simulator
just bevy
# Run the split BevyMon simulator in the browser
just web
# Build a deployable browser bundle into dist/flight-controller with hashed asset filenames
just web-dist
```
The split BevyMon path reuses the same `cu_bevymon::spawn_split_layout(...)` shell as
`cu_rp_balancebot` and `cu_bevymon_demo`, but the left panel still runs the real flight-controller
sim world, OSD, and help overlays.
The simulated compute subsystem publishes ZED2i-compatible 320×240 stereo RGBA images at a 100°
vertical field of view, matching the 4:3 projection used to train and evaluate ViTFly. It also
publishes depth and confidence maps, calibration and rig transforms, plus IMU, magnetometer,
barometer, and frame metadata. Bevy performs the camera rendering and GPU depth readback; the
simulator injects those buffers into the preempted `cu_zed::Zed` source. The compute graph runs
ViTFly inference on the CPU by default and draws its predicted velocity over the top-right depth
inset. Use `just sim-cuda` to run the same graph with ViTFly inference on NVIDIA CUDA.
The simulator has mutually exclusive `forest-world` and `urban-world` features. `forest-world` is
the default for `cargo run` and `just sim`; use `just sim-urban` for the original city scene. The
equivalent Cargo commands are:
```bash
cargo run
cargo run --no-default-features --features sim,urban-world
```
The forest is a deterministic 635 m hilly world built from the CC0 Kenney Nature Kit. Its source
GLBs, license, editable Blender scene, final GLB, and spawn-point preview live under `assets/`.
Regenerate the scene with:
```bash
just forest-assets
```
Both world GLBs are resolved through the same Copper CDN asset path as the quadcopter and lighting
assets. Atmospheric fog is enabled only for the forest observer camera, so the simulated ZED depth
camera continues to measure scene geometry without a cosmetic fog layer. Each world feature carries
its own scale, bounds, spawn position, and spawn yaw.
CPU runs use `RAYON_NUM_THREADS=8` from the checked-in `.cargo/config.toml`. Making the pool size
explicit prevents Candle from repeatedly probing Linux CPU topology while retaining parallelism for
Bevy and Avian. The setting is scoped to this example, and an existing shell value takes precedence.
### RC Input In Simulation
The simulator reads RC input from a host joystick device (`evdev`). By default it only auto-connects to
radio-style joystick profiles, to avoid false positives from keyboards/mice/gamepads that also expose joystick interfaces.
Environment variables:
```bash
# Prefer a specific device name substring (case-insensitive)
CU_SIM_JOYSTICK="radiomaster" just sim
# Allow generic/non-radio joystick devices as RC input
CU_SIM_ALLOW_GENERIC_JOYSTICK=1 just sim
```
Notes:
- If no compatible RC joystick is found, the sim falls back to keyboard controls.
- When connected, the bottom-right help panel shows the selected device name and technical axis bindings (`ABS_X`, `ABS_RY`, etc.).
- USB and Bluetooth radios both work if they appear as a joystick device on the host.
- `R` resets the rigid body, controller/estimator state, AUTO mission, camera cache, and ViTFly
recurrent state. With a joystick, move ARM low and AUTO off after resetting before the interlock
permits rearming.
### Compatible TX/RX Notes
- **Simulation path**: no receiver is used directly; input is read from the host joystick interface.
- **Auto-detected TX joystick profiles**:
- ExpressLRS-style joystick names (`expresslrs`, `elrs`, `radiomaster`)
- OpenTX / EdgeTX USB joystick names (`opentx`, `edgetx`)
- **ExpressLRS BLE mapping**: CH1-CH8 are joystick axes and CH9-CH16 are buttons. The sim uses
CH5 / SA (`ABS_Z`) for arm, CH6 / SB (`ABS_RZ`) for Acro/Angle/Position Hold, and
CH7 / SC (`ABS_THROTTLE`) middle position for Auto; the high position is reserved for forced
recovery mode.
- **Keyboard mapping**: `1`/`2`/`3` select Acro/Angle/Position Hold and `4` toggles Auto.
- **Firmware path (real hardware)**: RC input is CRSF on UART, so use a CRSF-compatible RX link
(for example ExpressLRS/Crossfire-class receivers and compatible transmitters).
## Configuration
`mcu_config.ron` and `compute_config.ron` contain each subsystem's shared graph exactly once. Their
feature-gated manual/preview and autonomy fragments add only the nodes and connections that differ.
The 500 m / 8 m waypoint geometry and infinite repetition are fixed demo constants, not RON
parameters. Key configurable parameters:
### Rate Controller
```ron
config: {
"kp": 0.04,
"ki": 0.0,
"kd": 0.0005,
"airmode": true,
"airmode_start_throttle_percent": 20.0,
}
```
### Attitude Controller
```ron
config: {
"angle_limit_deg": 60.0,
"acro_rate_dps": 180.0,
"acro_expo": 0.3,
"kp": 1.0,
}
```
### RC Mapper
```ron
config: {
"arm_channel": 4,
"arm_min": 1700,
"arm_max": 1811,
"mode_channel": 5,
}
```
## Motor Layout
QuadX configuration (props out):
```
Front
3 1
\ /
X
/ \
2 0
Rear
```
| Motor | Position | Rotation |
|-------|----------|----------|
| 0 | Rear Right | CCW |
| 1 | Front Right | CW |
| 2 | Rear Left | CW |
| 3 | Front Left | CCW |
## Development
### Attaching to Running Target
```bash
just attach
```
### Viewing Logs
The firmware logs to the SD card in Copper's binary format. Use the log reader tools to extract and analyze:
```bash
# Extract structured data
just logreader
# Extract text logs (when compiled with textlogs feature)
just textlogs
```
## Dependencies
Key Copper components used:
- `cu29` - Core runtime
- `cu-ahrs` - Attitude estimation
- `cu-bdshot` - Bidirectional DShot ESC protocol
- `cu-crsf` - CRSF RC protocol (ExpressLRS)
- `cu-msp-bridge` - MSP protocol for VTX/OSD
- `cu-pid` - PID controller
- `cu-micoairh743` - MicoAir H743 HAL bundle
- `cu-logmon` - Log monitoring