# Course Spec: Adaptive Quadrocopter, from Sim to Printed Drone

Oct 1, 2026 · @Oskar Freye · Revised Oct 24, 2026

## Summary

Nine build modules take a builder from a blank repo to a quadrocopter they designed, printed and built, running a model that adapts to physics it was never trained on. Each module ships code or hardware and ends with a measurable gate. All software is open source or free to use, so the hardware kit is the only purchase.

This is Volume 2 of the sim2real course. Volume 1, the Adaptive Robot Arm ([spec](https://uni.draht.dev/downloads/sim2real-arm-course-spec.md)), keeps the same nine modules on a desk arm and comes first. This volume builds on it without requiring it: a learner who built the arm turns its torque stand into the thrust stand, and the arm can dock the drone (Volume 1 hand-off).

The course answers one question: what must a network learn so a hidden vector captures a drone's real dynamics? The answer is to predict the next state from state and action. Module 4 tests it by training all four candidate objectives side by side.

Part 1 (software and ML, Modules 1 to 5) builds the simulator, the classical baselines and the learned adaptation. Part 2 (hardware, Modules 6 to 9) designs the frame in CAD, prints and builds it, measures it, and flies the trained model on it. The printed drone is the unseen physics: its real mass, inertia and motors will differ from both sim and CAD.

## Outcomes and non-goals

A learner who finishes owns four things:

- A quadrotor simulator they wrote and can explain line by line.
- One benchmark that scores PID, EKF-adaptive and learned adaptation on the same perturbation set, in sim and on hardware, with RAPTOR as the outside reference.
- A quadrocopter they designed in CAD, printed and built, flying their trained model with added payload.
- A public repo and episode series documenting every gate, including the ones that failed.

Non-goals:

- Outdoor and GPS-based flight. All flying happens indoors or in a netted area.
- Vision, SLAM, racing and aggressive acrobatics.
- Teaching drone regulation. Learners check their own obligations before flying (Module 7).
- Research-scale training. Everything runs on one laptop plus, at most, one GPU.

All gate thresholds below are starting values. Recalibrate them after Module 1 once the sim is validated.

## Prerequisites and stack

Learners need solid Python, matrix basics and derivative intuition. No control theory, RL, CAD or soldering background is assumed. Part 2 needs a 3D printer (or a print service), a soldering iron, drone liability insurance, and the hardware listed under Hardware and budget.

Every tool in the course is open source or free to use. Learners pay for the hardware kit and nothing else: no software licenses, subscriptions or cloud compute.

| Layer | Choice | Why |
| --- | --- | --- |
| Simulator (M1–M5) | Own JAX sim, pure `step(state, action, params)` | Every term is understood; `vmap` gives thousands of parallel envs for M5 |
| Reference sim | gym-pybullet-drones | Independent Crazyflie model to check M1 against; it shares the thrust model, so agreement does not validate aerodynamics |
| Training | JAX (Flax, Optax) on a laptop CPU | One stack from sim to policy, no GPU required |
| RL at scale (optional) | Isaac Lab on an RTX-class GPU, run once by Oskar on a rented card | Speed comparison published as episode 7; learners never need paid compute |
| Editor | VS Code (free) or any editor | Hosts the CAD viewer and the course tooling |
| CAD (M6) | build123d (Python code-CAD) | Parametric, diffable in git, editable by coding agents; the single source of truth for the frame |
| CAD viewer (M6) | OCP CAD Viewer, as VS Code extension or standalone | Live 3D view of the build123d model while you code |
| Hand-made parts (M6–M7) | FreeCAD or Dune 3D, imported as STEP | GUI modeling for one-off parts such as a motor mount or the payload latch |
| Wiring plan (M6–M7) | WireViz | Connectors and wires as YAML in git, rendered to a diagram and wire list; the trainer checks it before anything is soldered |
| Slicer (M7) | OrcaSlicer or PrusaSlicer | Prints the parts and reports their masses for the CAD-to-sim step |
| Printing (M7) | FDM in PETG or carbon-fiber nylon | PLA cracks on impact and softens near warm motors |
| Flight controller (M7) | Crazyflie Bolt 1.1 | Crazyflie firmware and IMU on a brushless build; custom controllers run onboard as out-of-tree apps |
| Radio link | Crazyradio 2.0 with cflib (Python) | Off-board control, logging and parameters |
| Positioning (M7–M9) | Lighthouse deck plus two Lighthouse V2 base stations | Absolute indoor pose for control and for ground truth |
| Test rig (M7–M8) | Printed thrust stand with load cell, HX711 and a USB microcontroller board | Measures the thrust map, and with a lever-arm fixture the torque constant; the board streams HX711 readings to the laptop |
| Outside baseline (M9) | RAPTOR foundation policy | Open-source adaptive policy that already flies Crazyflie-firmware drones |
| Flight data (sim and hardware) | Rerun | Sim and Lighthouse flights on one 3D timeline, in a desktop or browser viewer |
| Renders (episodes) | Blender | Turntables and exploded views from build123d's STL export |
| Web models (episodes) | Yet Another CAD Viewer | Interactive 3D frame embedded in the course pages |

The controller interface is fixed from Module 2 on: collective thrust plus body rates. The flight controller's own rate loop stays in charge, so the trained model can fly off-board over the radio first and onboard the Bolt in Module 9.

## Hardware and budget

The drone flies on a Crazyflie Bolt 1.1 with Lighthouse positioning, for about 1,160 to 1,200 € in parts. Learners buy this kit once. A learner who built Volume 1 already owns the load cell with its HX711 and a Pico 2 to read them, and can skip those two lines. An optional optical tachometer lets Module 8 measure motor speed; bidirectional DShot telemetry from the Bluejay ESC or an accelerometer method can substitute, and otherwise those values are declared unresolved. Part 1 needs only a laptop, so the purchase can wait until Module 6. The Bolt runs the Crazyflie 2.1 firmware and IMU on a brushless build ([Bitcraze](https://www.bitcraze.io/products/crazyflie-bolt-1-1/)), so sim, reference model and real firmware line up, and custom controllers run onboard as out-of-tree apps ([guide](https://www.bitcraze.io/2023/02/adding-an-estimator-or-controller/)). In a 5×5 m space Bitcraze measured better than 1 mm relative precision and better than 10 cm absolute accuracy, with precision varying by a few centimeters at the corners ([Bitcraze docs](https://www.bitcraze.io/documentation/system/positioning/ligthouse-positioning-system/)). Plan the flight volume before buying base stations. The crossing-beam Lighthouse position serves as benchmark ground truth.

<!-- BEGIN parts table: generated from src/data/sim2real-parts.ts by "bun run sim2real:parts"; edit the data, not the table -->

| Part | Qty | Source | Price incl. 19% VAT |
| --- | --- | --- | --- |
| Crazyflie Bolt 1.1 | 1 | [exp-tech.de](https://exp-tech.de/products/crazyflie-bolt-11), 185 € net | 220 € |
| Lighthouse positioning deck | 1 | [exp-tech.de](https://exp-tech.de/collections/bitcraze), 86 € net | 102 € |
| Lighthouse V2 base station (SteamVR Base Station 2.0) | 2 | [HTC Vive shop](https://myshop.vive.com/vive_de/1920589.html), 239 € each | 478 € |
| Crazyradio 2.0 | 1 | [exp-tech.de](https://exp-tech.de/collections/bitcraze), 39 € net | 46 € |
| 1404 motor, about 4500 KV for 3S | 4 | [fpv24.com](https://www.fpv24.com/de/flyfish/flyfishrc-flash-1404-4500kv-fpv-motor-silber-lila), 15.90–21.90 € each | 64–88 € |
| 4in1 ESC, 20 A class or higher, with Bluejay firmware | 1 | [Amazon.de](https://www.amazon.de/dp/B0HKS4L3WW), EM40A Bluejay, 5.2 g | 67 € |
| LiPo 3S 450 mAh, XT30 | 3 | [fpv24.com](https://www.fpv24.com/de/gaoneng/gaoneng-gnb-lipo-akku-450mah-3s-111v-80c-xt30-long-type), 11.90–12.90 € each | 36–39 € |
| 3-inch props | 2 sets | Any FPV shop | approx. 10 € |
| LiPo charger and fire-safe bag | 1 | Any FPV shop | approx. 60 € |
| 1 kg load cell with HX711 | 1 | [Digi-Key](https://www.digikey.at/en/products/detail/dfrobot/SEN0160/6588549), 14.41 € net | 17 € |
| USB microcontroller board to read the HX711, such as a Raspberry Pi Pico | 1 | Any electronics shop | approx. 6 € |
| PETG or carbon-fiber nylon filament | 1 kg | Any filament shop | approx. 25–40 € |
| Vane anemometer, for the Module 9 fan condition | 1 | Any electronics shop | approx. 25 € |
| Optical tachometer (optional) | 1 | Any electronics shop | approx. 20 € |

<!-- END parts table -->

Shop prices as of October 2026, before shipping; "approx." marks typical prices that were not looked up. Positioning is the biggest cost at about 580 €. Bitcraze asks for at least two Lighthouse V2 base stations ([Bitcraze docs](https://www.bitcraze.io/documentation/system/positioning/ligthouse-positioning-system/)). Amazon.de listed the base station at about 575 € in October 2026 and Bitcraze resellers at 295 € net, so compare before buying; used base stations can cost much less. Power the ESC straight from the battery: the Bolt's own ESC connectors are limited to 8 A per motor. The Bolt still takes the battery through its own XT30, so it can measure voltage, and only the signal wires run from its ESC ports to the ESC ([Bitcraze datasheet](https://www.bitcraze.io/documentation/hardware/crazyflie_bolt_1_1/crazyflie_bolt_1_1-datasheet.pdf)).

The ESC runs Bluejay, or BLHeli_S on a board that Bluejay supports and the learner flashes. The Bolt's default protocol is OneShot125. Once a build switches to DShot, the Crazyflie firmware enables bidirectional DShot by default: it reads each motor's speed back from the ESC and checks those speeds before it arms, which needs Bluejay and the 300 kHz DShot rate ([firmware Kconfig](https://github.com/bitcraze/crazyflie-firmware/blob/master/src/modules/src/Kconfig)). A stock BLHeli_S ESC reports no speed, so the arming check fails. The same telemetry gives Module 8 per-motor rotor speed. Bitcraze suggests four separate ESCs on the arms, because a 4in1 under the Bolt takes deck space. The kit keeps the 4in1: four separate 20 A ESCs weigh about 16 g against 5 g, and the build carries only the Lighthouse deck. A 4in1 usually mounts on 20×20 mm holes and the Bolt on 30.5×30.5 mm M3, so the frame needs both patterns.

The drone stays under 250 g including test payloads, so the design target is 210 g all-up and the weighed build may reach at most 227 g, leaving room for the +10% payload. Four 1404 motors (about 10 g each, [example](https://www.fpv24.com/de/speedy-bee/speedybee-1404-v2-4600kv-4s-fpv-motor-schwarz)) and one 3S 450 mAh pack (about 40 g) already take about 80 g of that. Under 250 g and below 19 m/s, a privately built drone flies in EU subcategory A1 ([EASA](https://www.easa.europa.eu/en/node/116548)). Without a camera it also needs no operator registration, which applies above 250 g or with a sensor that can capture personal data ([AESA](https://www.seguridadaerea.gob.es/en/node/2412)). German law requires liability insurance for drones of every weight ([Allianz](https://www.allianz.de/recht-und-eigentum/privat-haftpflichtversicherung/drohnen-versichern/)).

## Conventions and interfaces

One file, `conventions.py`, fixes frames, units and rates for every part of the course. Sim, controllers, firmware app and log parsers all import it, and a round-trip test checks sim states against real firmware logs.

| Item | Convention |
| --- | --- |
| Frames | ENU world, FLU body and scalar-first quaternions; adapters convert the firmware's own field orders explicitly |
| Motors | M1 front-right, M2 back-right, M3 back-left, M4 front-left, as in the firmware's mixer; M2 and M4 spin clockwise |
| Units | SI inside all code: m, s, rad, N, kg |
| Action | Collective thrust as a fraction of maximum, plus body rates in rad/s; the stock controller turns the yaw rate into a held heading |
| Thrust map | Thrust fraction to motor command as a function of battery voltage; identified in Module 8, used in both directions |
| Rates | Physics at 1 kHz; controllers and policy at 100 Hz with zero-order hold |
| History | Last 50 policy steps (0.5 s) of state and action |
| Network budget | At most 10k float32 parameters, so the model fits onboard the Bolt |
| Observation model | Sim adds the noise and delay measured on the real onboard state estimate in Module 8 |
| Gyro sampling | The 1 kHz gyro is averaged over each 10 ms control step before it enters an observation; point samples alias frame vibration |

Frame and sign mix-ups are the most common sim-to-real bug. The round-trip test catches them on the desk instead of in the air, and the Module 1 attitude comparison catches them in simulation.

## Benchmark spec

One harness, `bench.py`, scores every controller the same way in sim and on hardware. The course ships it and learners never edit it.

| Item | Spec |
| --- | --- |
| Trajectories | Hover for 30 s at 1 m. Figure-eight x = 0.5·sin φ, y = 0.25·sin 2φ at 1 m height, where the phase φ ramps up from rest over 2 s and then advances at ω for a 6 s period (peak speed about 0.75 m/s); fast variant with a 4 s period |
| Sim perturbations | Mass +20% and −10%, one motor at 85%, drag ×2, lateral force of 10% of weight, a desk-fan jet that the figure-eight crosses twice per lap, an uncompensated thrust-map error that grows to 15% over a flight, sensor noise, 10 and 20 ms latency, mid-flight mass step of +20% |
| Hardware conditions | Nominal, added payload (+10% mass, total still under 250 g), desk fan at a marked spot, low battery: start at a declared voltage and land at the app's landing threshold |
| Runs | 20 seeds per sim condition, each with its own seeded start offset. 5 flights per hardware condition, trajectory and controller, controller order randomized within each repeat block |
| Ground truth | True state in sim; on hardware the Lighthouse crossing-beam position, scored at its own measurement time |
| Metrics | RMS and max position error, crash or abort count, recovery time after a step, measured to the last re-entry into the error band after which the error stays inside for at least 0.5 s, mean absolute body-rate command |
| Report | Mean ± std per controller, condition and trajectory, with commit, config and seed on every row |

Lighthouse is precise but only accurate to under 10 cm in absolute terms. Hardware results therefore compare controllers flown in the same volume, never against absolute centimeter thresholds.

## Course repo and gates

Learners fork a starter repo and build into it. The course owns the checks; the learner, or their coding agent, owns the code.

- **Provided:** `bench.py` with its trajectories, perturbations and metrics, `conventions.py`, one executable check per module in `gates/`, a `PROGRESS.md` template, and CI that runs every sim gate on push, sized to fit free CI tiers.
- **Built by the learner:** `sim/`, `controllers/`, `estimators/`, `learning/`, `cad/` and the onboard firmware app.
- **Maker/checker:** Gate code and thresholds live in `gates/` and are read-only for learners. Sim gates pass or fail in CI. Hardware gates run the same checks on uploaded flight logs, plus a video for trainer review.
- **PROGRESS.md:** One line per module with gate status, a link to the passing run, hours spent and notes. Oskar's own run publishes real hours as the course's effort estimate.
- **Reproducibility:** Every results row records commit hash, config hash and seed. Plots are generated from those files, never edited by hand.

## Part 1 · Software and ML

Modules 1 to 5 run entirely in simulation. They end with a trained adaptation model and one benchmark that scores every controller.

### Module 1 · Write your own quadrotor sim

The learner builds a 6-DoF quadrotor simulator from first principles, parameterized so every physical constant can later be randomized.

- **Build:** State of position, velocity, quaternion and body rates. Four motors with thrust k\_f·ω², torque k\_m·ω² plus rotor-acceleration reaction torque, and first-order motor lag. RK4 integration at 1 kHz. A firmware layer that ports the Crazyflie rate controller (500 Hz, degrees per second, PWM output) and mixer, so the sim's inner loop matches the real one; the Bolt ships with Crazyflie 2 gains, so re-port them after tuning the 3-inch build. A battery model whose terminal voltage drops with load current, V = OCV(SOC) − I·R, so thrust reserve shrinks when a recovery draws current.
- **Learn:** Newton-Euler equations, quaternion kinematics, the mixing matrix, and why explicit Euler integration drifts.
- **Deliverable:** `sim/` with a pure `step()` function and a `Params` dataclass holding mass, inertia, k\_f, k\_m, drag, motor lag and battery. Crazyflie values as defaults until Module 6 replaces them; take k\_m from the firmware's torque-to-thrust ratio, not the reference model's URDF, and set the reference model to the same k\_m for the comparison.
- **Gate:** Hover thrust equals m·g within 1%. Free fall matches the analytic solution. The course's open-loop excitation (single-motor, yaw, roll and pitch steps) tracks gym-pybullet-drones within 2 cm and 2° over 0.5 s, with motor lag and Bullet damping off in both (open loop is unstable, so keep the horizon short). None of these checks moves air through a rotor, so inflow and ground effect stay unvalidated until flight.

### Module 2 · Fly it classically, then break it

The learner flies the sim with textbook control, then reproduces the sim-to-real gap on purpose and explains each failure.

- **Build:** Cascaded controller: position PID to attitude to thrust plus body rates. An LQR around hover as the second baseline. The sim perturbations from the Benchmark spec, including the growing thrust-map error and 10 to 20 ms latency.
- **Learn:** PID tuning, linearization around hover, LQR, and why integral action fixes steady-state offsets but not transients.
- **Deliverable:** `controllers/` plus a first results table from `bench.py`, the course's harness.
- **Gate:** Nominal RMS error under 5 cm. At least one perturbation pushes maximum error past 30 cm or causes a crash or abort; a baseline that survives them all documents a separate stress sweep instead of being weakened. A written cause for each result.

`bench.py` is the spine of the course. Every later module reruns it unchanged.

### Module 3 · The analytical fix

The learner estimates the unknown physics online with classical tools. The result is the baseline every learned method must beat.

- **Build:** An EKF whose state is augmented with mass and a 3-D disturbance force. Feed the estimates back: thrust feedforward from estimated mass, cancellation of estimated disturbance. Stretch: L1 adaptive control on the same interface.
- **Learn:** Kalman filter derivation, observability, persistence of excitation (why mass and thrust constant can't be told apart in a perfect hover), disturbance observers.
- **Deliverable:** `estimators/ekf.py` plus a `bench.py` rerun.
- **Gate:** From a prior at least 10% off, the mass estimate stays within 5%, with 2σ within 5%, from 2 s into hover plus a small maneuver. On the +20% mass case, tracking error after those 2 s stays within 1.5× nominal or nominal plus 5 mm, whichever is larger, with no crash or abort.

### Module 4 · Answer B, built

The learner trains a history encoder whose latent captures the drone's real dynamics, then tests all four interview answers against each other.

- **Build:** Generate flights with randomized mass, motor constants, drag and wind. A history encoder (GRU or 1-D conv over the last 50 steps of state and action, per Conventions) outputs an 8- to 16-dim latent z. A forward model predicts the residual between the real state 10 policy steps (0.1 s) ahead and the nominal model's rollout over the same actions, given state, actions and z. Over a single 10 ms step the residual is mostly sensor noise.
- **Ablation:** Same encoder trained with the other three objectives: A reconstructs current sensors, C separates flights contrastively, D only minimizes latent size.
- **Learn:** Residual learning over a nominal model, what makes a latent identifiable, why translational motion reveals only the ratio k\_f/m, linear probing.
- **Deliverable:** Trained models plus a probing notebook that regresses true parameters from z.
- **Gate:** With objective B, a linear probe recovers the thrust-to-mass ratio k\_f/m with R² above 0.9 on held-out vehicles, scored per history window as the median over the same three or more training seeds for every objective. Mass and motor constant enter the acceleration only as that ratio. A, C and D are reported side by side, whatever they show.

Stretch: use the learned model inside a simple MPC and rerun `bench.py`.

### Module 5 · RMA: adaptation without knowing the physics

The learner trains a policy that adapts at runtime from its own recent history, using the two-phase RMA recipe.

- **Phase 1:** PPO in thousands of `vmap`-parallel envs with domain randomization, resampled within episodes as well as at reset. The reward pays a survival bonus, so ending an episode early never scores best. A privileged encoder maps the true parameters to z; the policy sees state and z and outputs thrust plus body rates.
- **Phase 2:** Freeze the policy. Train an adaptation module to regress z from the same 50-step history, on data the policy itself generates.
- **Learn:** PPO, choosing randomization ranges, teacher-student training, and why the target is z rather than raw parameters.
- **Deliverable:** Policy plus adaptation module, `bench.py` rerun against Modules 2 to 4.
- **Gate:** No crash or abort, and tracking error within a non-inferiority margin of the Module 3 EKF (1.10× plus 5 mm), in every held-out cell, and no higher in total across the adaptation conditions. Recovery within 1 s after a +20% mass step in mid-flight. Variant B and RMA reported side by side.

**Variant B** closes the course question. Freeze the Module 4 encoder, train the policy on its latent instead of RMA's, and compare both on `bench.py`. The frozen encoder saw narrower data than RMA's adaptation module, so treat Variant B as a transfer test. The reference design is RAPTOR, a recurrent policy with 2084 parameters that adapts in context ([paper](https://arxiv.org/abs/2509.11481)).

**Constraints:** at most 10k parameters, observation noise and delay, command jitter and dropped commands randomized, and training finishes in under 2 hours on a laptop CPU. If it doesn't, shrink the network or switch to an off-policy method before reaching for a GPU. Oskar runs Phase 1 once in Isaac Lab on a rented RTX-class GPU (roughly $5 to $15 of compute) and publishes the speed comparison as episode 7, so learners see the result without paying. Learners who already own an RTX-class NVIDIA GPU can repeat it.

## Part 2 · Hardware

Modules 6 to 9 build the drone the model has never seen. Module 6 can start in parallel once Module 1 is done, because its CAD output feeds the sim.

### Module 6 · Design the frame in CAD

The learner designs a parametric frame in code and turns the CAD model into the sim's physics parameters.

- **Build:** Pick components first (motors, props, ESCs, Bolt, battery, Lighthouse deck) and model each as a mass block. Keep each ESC under the Bolt's 8 A connector limit or power the ESCs directly. Record the ESC firmware (Bluejay or BLHeli_S), the ESC protocol and the motor's pole count, and give the frame both the ESC's and the Bolt's mounting patterns. Draw the wiring plan in WireViz: the battery split to ESC and Bolt, every connector and wire gauge, and which Bolt ESC port drives which motor, per Conventions. Then a parametric frame in build123d: arm length, motor mount pattern, prop size, electronics bay, a top mount with clear sight lines for the Lighthouse deck, prop guards. A learner who built Volume 1 adds the handle of its Module 6 dummy drone, so the arm can dock the drone (Volume 1 hand-off); the handle counts toward the 210 g.
- **Learn:** Code-CAD, design for FDM printing (layer orientation, walls, infill), weight budgets, planning a power path, and why frame stiffness matters to the rate loop.
- **Deliverable:** `cad/` with the parametric model, STL exports, the wiring plan `wiring.yml` with its rendered diagram, and `cad_to_params.py`, which builds mass and inertia from slicer-reported part masses and weighed components (solid CAD overstates printed mass because of infill) and writes them into the sim's `Params`.
- **Gate:** Estimated all-up weight at or under 210 g, so test payloads stay below 250 g. Thrust-to-weight of at least 2:1 with the +10% payload, from motor data scaled to a loaded low-battery voltage. The Module 2 controller hovers the CAD drone in sim. The trainer reviews the wiring plan before anything is soldered.

From here on, Module 5's randomization ranges are centered on the CAD parameters instead of Crazyflie defaults.

### Module 7 · Print, build, first flight

The learner prints the frame and a thrust stand, builds the drone and flies it on stock firmware.

- **Build:** Print frame and parts, then a thrust stand with a load cell and an HX711, read by a USB microcontroller board that streams to the laptop; a learner who built Volume 1 turns its torque stand into the thrust stand instead. Solder motors, ESCs, FC and power to the Module 6 wiring plan. Flash the Crazyflie firmware, configure it for the Bolt, and set 3S battery thresholds, because the Bolt's defaults are 2S values. If the build uses DShot, keep bidirectional DShot at 300 kHz with the Bluejay ESC, or turn it off for an ESC without telemetry, and set the motor's pole count, which the firmware fixes at compile time and defaults to 12. Mount and calibrate the Lighthouse base stations.
- **Safety:** Props off for all bench work. Charge LiPos in a fire-safe bag, never unattended; store them at storage voltage and discharge them before disposal. The app lands at a landing voltage before the firmware's critical shutdown cuts the motors. Test failsafe and kill switch before props go on. With the ESC on battery power, switching the Bolt off leaves the ESC live, so unplug the battery before handling the drone. First hover tethered or in a net. Fly indoors only, in a net. Keep the drone under 250 g with no camera, and confirm liability insurance before the first flight.
- **Learn:** Soldering, ESC protocols, firmware setup, failsafes, and how vibration reaches the gyro.
- **Deliverable:** A flying drone, a build log with photos, the wiring plan updated to the wiring as built, and a BOM with weighed (not datasheet) masses.
- **Gate:** A stable, airborne 60 s hover in stock stabilized mode. Clipping flags computed onboard at the 1 kHz sensor rate show no gyro or accelerometer clipping, and tilt stays under 5° RMS. Weighed mass is at most 227 g. Battery thresholds are set for 3S: critical at least 3.0 V per cell, below low, below the landing threshold, at most 3.8 V per cell.

### Module 8 · Measure the real drone

The learner measures how far the printed drone sits from CAD and sim, and checks that the trained model has seen physics like it.

- **Build:** Thrust-stand sweeps per motor, with the rotor at least two diameters from any surface, for the thrust map down to the Module 9 low-battery voltage. The stand's board and the Bolt log on separate clocks, so match them by laptop time and average each settled hold. A lever-arm fixture for the torque constant and, where available, motor speed and lag from bidirectional DShot (the `motor.m1_rpm` to `motor.m4_rpm` log variables), checked against a tachometer because a wrong pole count scales every reading. A bifilar pendulum for the inertia tensor. A scale for mass. Drag from steady-speed flights, battery internal resistance from a four-motor load step, rate-step responses of the real rate loop, and the noise of position, velocity and body rate with observation and command delay and dropped commands.
- **Learn:** System identification, curve fitting, and honest error bars.
- **Deliverable:** `params_real.yaml`, a CAD-versus-real mismatch table, and a `bench.py` sim rerun with the measured values.
- **Gate:** Every parameter measured with error bars, or declared unresolved with an assumed range from a named source. Each interval or assumed range lies inside the Module 5 randomization range; if one doesn't, widen that range and retrain before Module 9. The thrust map at the logged hover command matches weighed m·g within 3%. Mass and inertia are always measured.

### Module 9 · Fly the trained model

The learner runs the Module 5 model on the printed drone and benchmarks it against the classical controllers and RAPTOR.

- **Deploy:** Round 1: every controller runs on the laptop at 100 Hz and sends thrust plus body-rate setpoints over Crazyradio to one small onboard app, so latency is equal for all. Round 2: the learned model runs inside that app on the Bolt, exported from JAX to C. RAPTOR writes motor commands from its own onboard controller, so it flies as a separate onboard deployment and is compared with the onboard learned model.
- **Safety:** Thrust and rate outputs are clipped. A tilt limit and a Lighthouse geofence hand control back to the stock controller. A setpoint timeout lands the drone if the radio link drops. Same net, same kill switch.
- **Test protocol:** The hardware conditions from the Benchmark spec, flown by PID, EKF, the learned model and RAPTOR (if its Crazyflie build runs on the Bolt) in randomized order, five flights per condition, trajectory and controller. Stretch: a printed latch drops the payload mid-flight.
- **Deliverable:** Flight logs, the hardware `bench.py` table, and a side-by-side video of every controller under payload.
- **Gate:** The hardware table is complete for every controller and published, whatever it shows. Onboard outputs match JAX within 1e-4 on recorded inputs, and onboard RMS error stays within 10% of the off-board result beyond flight-to-flight noise. Inference finishes within 2 ms worst case on the Bolt. The learned model never crashes, and every abort has a written reason. A tuned classical controller may win on hardware, and that is a valid result.

## Volume 1 hand-off: the arm docks the drone

A learner who built Volume 1's arm keeps it on the desk as the drone's docking station. The drone lands on a pad within the arm's reach, and the arm lifts it by its handle into a cradle and back onto the pad for the next flight. Volume 1 fixes the interface ([arm spec](https://uni.draht.dev/downloads/sim2real-arm-course-spec.md), "Volume 2 hand-off"), and this volume designs against it:

- The frame carries the handle of Volume 1's Module 6 dummy, and the handle counts toward the 210 g design target. The dummy weighs 230 g, the 227 g weighed limit rounded up, so the arm docks the drone without test payloads.
- The pad and the cradle sit at fixed positions inside the arm's 0.30 m reach. The drone flies only inside the net, so the pad sits inside it too: the arm stands at the net's edge, or the net leaves a port for it.
- The arm moves only while the drone's latest report, at most 0.5 s old, says disarmed; a missing or older report counts as armed. The drone arms only while the arm's latest report, at most 0.5 s old, says it is in its rest pose. If the reports stop while the arm holds the drone, the arm stops and holds; unplug the drone's battery before reaching in, because switching the Bolt off leaves its ESC live.
- Volume 1's torque stand, with its load cell, HX711 and bench Pico 2, becomes the thrust stand of Module 7.

Without the arm there is no dock, and no gate depends on it.

## Reading spine

One or two primary sources per module, read while building, never before. Required reading is free: arXiv preprints, free online books, or the authors' own copy of a paywalled paper. Paid books are marked optional.

| Module | Reading | What it gives |
| --- | --- | --- |
| M1 | Mahony, Kumar, Corke, "Multirotor Aerial Vehicles" (IEEE RAM, 2012); Tedrake, *Underactuated Robotics* (MIT, free online) | Quadrotor equations and the math behind them |
| M2 | Mellinger and Kumar, "Minimum Snap Trajectory Generation and Control for Quadrotors" (ICRA 2011) | The standard cascaded controller |
| M3 | Labbe, *Kalman and Bayesian Filters in Python* (free online). Optional: Thrun, Burgard, Fox, *Probabilistic Robotics*; Hovakimyan and Cao, *L1 Adaptive Control Theory* | Kalman filtering; the adaptive baseline |
| M4 | Shi et al., "Neural Lander" (ICRA 2019); O'Connell et al., "Neural-Fly" (Science Robotics, 2022) | Learned residual dynamics on real drones |
| M5 | Kumar et al., "RMA: Rapid Motor Adaptation" (RSS 2021); Tobin et al., "Domain Randomization" (IROS 2017); Eschmann, Albani, Loianno, ["RAPTOR: A Foundation Policy for Quadrotor Control"](https://arxiv.org/abs/2509.11481) | The two-phase recipe; why randomization transfers; the current state of the art |
| M6–M7 | build123d documentation; [Crazyflie Bolt 1.1](https://www.bitcraze.io/products/crazyflie-bolt-1-1/) and its [datasheet](https://www.bitcraze.io/documentation/hardware/crazyflie_bolt_1_1/crazyflie_bolt_1_1-datasheet.pdf); Bitcraze, ["Adding an estimator or controller"](https://www.bitcraze.io/2023/02/adding-an-estimator-or-controller/) | Code-CAD; wiring the Bolt; onboard custom controllers |
| M8 | Tedrake, *Underactuated Robotics*, system identification chapter (free online). Optional: Ljung, *System Identification: Theory for the User* | Fitting models to measured data |
| M9 | Kaufmann et al., ["Swift"](https://doi.org/10.1038/s41586-023-06419-4) (Nature, 2023); Eschmann et al., "Learning to Fly in Seconds" (RA-L, 2024); [PX4 RAPTOR module docs](https://docs.px4.io/main/en/neural_networks/raptor) | Real-data residuals for sim-to-real; policies on real quadrotors; how RAPTOR is deployed and benchmarked |

## Episode map and access tiers

Seven free episodes carry the story: the hook, the first two builds, the Module 4 result, the laptop-versus-GPU comparison, the CAD design and the RAPTOR race. The builds behind them are customer-tier.

| # | Episode | Module | Tier |
| --- | --- | --- | --- |
| 0 | Can you solve this NVIDIA interview question? Answer B, then promise to prove it | Intro | Free |
| 1 | I wrote a drone simulator from scratch | M1 | Free |
| 2 | I broke my drone on purpose | M2 | Free |
| 3 | Teaching a drone to weigh itself | M3 | Customer |
| 4 | A, B, C or D? I trained all four | M4 | Free |
| 5 | Building the history encoder | M4 | Customer |
| 6 | RMA in my own JAX sim | M5 | Customer |
| 7 | My laptop vs a rented GPU: the same policy in JAX and Isaac Lab | M5 | Free |
| 8 | I designed a drone in Python | M6 | Free |
| 9 | Printing and building it | M7 | Customer |
| 10 | How wrong was my CAD? | M8 | Customer |
| 11 | The model meets the printed drone | M9 | Customer |
| 12 | Can my model beat RAPTOR? | M9 | Free |
| T | Gate rubrics and common failure modes per module | All | Trainer |

## Open questions

- [ ] Bolt maturity: Bitcraze sells the Bolt 1.1 as early access, with tested hardware and software still in progress ([Bitcraze store](https://store.bitcraze.io/products/crazyflie-bolt-1-1)). Confirm firmware support for the course's needs, including RAPTOR, before ordering learner kits.
- [ ] PX4 capstone: add a Module 10 that ports the controller to PX4 and races RAPTOR inside PX4's own module ([PX4 docs](https://docs.px4.io/main/en/neural_networks/raptor)), or keep it for a follow-up course?
- [ ] Dock layout with Volume 1's arm: does the arm stand at the net's edge, or does the net leave a port for it?
- [ ] Charging the drone in the arm's cradle.
