# Course Spec: Adaptive Robot Arm, from Sim to Printed Arm

Oct 21, 2026 · @Oskar Freye · Draft

## Summary

Nine build modules take a builder from a blank repo to a desk robot arm 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. Along the way the arm learns eight tasks, from waving hello to throwing a ball into a cup. All software is open source or free to use, so the hardware kit is the only purchase: about 505 € including the 3D printer.

This is Volume 1 of the sim2real course. Volume 2, the Adaptive Quadrocopter ([spec](https://uni.draht.dev/downloads/sim2real-course-spec.md)), keeps the same nine modules, and the arm built here becomes the drone's docking station.

The course answers one question: what must a network learn so a hidden vector captures a robot'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 arm in CAD, prints and builds it, measures it, and runs the trained model on it. The printed arm is the unseen physics: its link masses, friction and backlash will differ from both sim and CAD. The servos add a second unknown. Feetech publishes their register map but not their firmware, so the position loop inside each servo is a black box. Module 1 models it and Module 8 identifies it.

## Outcomes and non-goals

A learner who finishes owns four things:

- An arm simulator they wrote and can explain line by line, including a model of a servo whose firmware they cannot read.
- One benchmark that scores the stock servo loop, PID, gravity feedforward, EKF-adaptive and learned adaptation on the same perturbation set, in sim and on hardware.
- A robot arm they designed in CAD, printed and built, running their trained model with an unknown payload.
- A public repo and episode series documenting every gate and every task on the ladder, including the ones that failed.

Non-goals:

- Learned grasping, vision beyond AprilTags, and imitation learning from teleoperation. LeRobot already covers those for the SO-101.
- Contact-rich simulation. Grasps attach objects kinematically, and the pen's contact with the paper is not simulated.
- Industrial robot safety. The arm is desk-sized and weak, but it still pinches (Module 7).
- Research-scale training. Everything runs on one laptop.

All gate and task thresholds below are starting values. Oskar's own run recalibrates them twice: the sim thresholds after his Module 1, once the sim is validated, and the thresholds that depend on the servo loop (the sag, tracking and payload numbers in Modules 2 to 5) after his Module 8 has identified a real servo. Each recalibration ships as a new kit version with its reason.

## Task ladder

The modules add eight tasks to the arm's repertoire, roughly from easy to hard. Each task except the melody runs in sim first. Once the Module 7 arm exists it also runs on hardware, and Module 9 replays the whole ladder on the printed arm. Sim checks pass in CI; hardware checks need a video for trainer review. Each task reuses its module's deliverable, so the ladder adds props, not new subsystems.

| # | Task | Module | What the arm does | What it teaches | Check |
| --- | --- | --- | --- | --- | --- |
| 1 | Wave hello | M1, real in M7 | Open-loop sines on the elbow and wrist | Joint space, and the servo loop's lag: the wave shrinks and falls behind as the frequency rises | Sim and real joint traces overlaid in Module 8 |
| 2 | Draw | M2, real in M7 and M9 | The benchmark figure-eight, then the learner's name from an SVG path, with a pen in a spring-loaded holder | Inverse kinematics, trajectory timing, and why a payload makes a position servo sag | Scanned drawing, registered to the arm's frame by the printed marks on the drawing sheet and overlaid on the command; RMS deviation reported |
| 3 | Weigh it | M3, real in M9 | Lifts a cup of weights, moves it briefly and reports the mass | Kalman filtering and observability | Within 10 g in sim; real error against the scale reported |
| 4 | Which cup is heavier? | M4, real in M9 | Wiggles each of three identical-looking cups for 0.5 s, then lines them up by weight | What a learned latent captures, read out with a linear probe | Right order in 9 of 10 sim trials |
| 5 | Throw | M5, real in M9 | Flings a ping-pong ball from a printed spoon into a cup on the table, 40 cm from the base to start | Reward design and domain randomization, with the same trainer as the tracking policy | Hit rate of at least 60% in sim; real hit rate reported whatever it shows |
| 6 | Play a melody | M7, on the beat in M8 | Strikes an 8-note glockenspiel with a mallet | Command-to-motion delay, which grows with the size of the move: uncompensated, notes after long moves land late and the rhythm stumbles; with each strike sent early by the delay the servo model predicts for its move, the melody keeps time | Strike times from a microphone recording, compared with the score after removing one constant offset (the recording shares no clock with the commands); RMS error under 20 ms after compensation |
| 7 | Dock the drone | M6 in sim, real in M9 | Lifts a printed dummy of the Volume 2 drone (230 g) by its handle from a pad into a cradle | Torque budgets: peaks against the stall torque, holds against the rated torque | 10 of 10 cycles without a safety latch, with the shoulder's RMS current per cycle under its rated-torque current (about 0.9 A) |
| 8 | Ball in a cup (stretch) | M5, real in M9 | Swings a ball on a string up into a cup held by the gripper | The classic policy-search demo: string slack and catch timing | Success rate over 20 tries, reported whatever it shows |

The melody tests the real command path, so it has no sim version.

## Prerequisites and stack

Learners need solid Python, matrix basics and derivative intuition. No control theory, RL, CAD or electronics background is assumed. Part 2 needs a 3D printer with a bed of at least 180 × 180 mm (or a print service), a soldering iron for pin headers and heat-set inserts, a scale that reads to 0.1 g, and the hardware listed under Hardware and budget. The servos daisy-chain on their own cables, so soldering is limited to pin headers on the bench electronics.

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 | MuJoCo, loading an MJCF that the course generates from the same `Params` | Independent rigid-body solver to check M1 against; it shares the parameters, so agreement validates the dynamics code, not the servo model |
| Default arm (M1–M5) | The SO-101 model in MuJoCo Menagerie (Apache-2.0) | A documented arm on the same servo family; its link masses and joint terms stand in until Module 6 |
| Training | JAX (Flax, Optax) on a laptop CPU | One stack from sim to policy, no GPU required |
| 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 arm |
| CAD viewer (M6) | OCP CAD Viewer, as VS Code extension or standalone | Live 3D view of the build123d model while you code |
| Servo body (M6) | The STS3215 STEP file in TheRobotStudio's SO-ARM100 repo (Apache-2.0) | The real servo's shape for brackets and clearance checks |
| Hand-made parts (M6–M7) | FreeCAD or Dune 3D, imported as STEP | GUI modeling for one-off parts such as a tool handle |
| Wiring plan (M6–M7) | WireViz | The harness from the supply through the kill switch and adapter to the servo chain and the Pico 2 tap, as YAML in git; the trainer checks it before power-on |
| Slicer (M6–M7) | OrcaSlicer, PrusaSlicer or Bambu Studio | Prints the parts. Sliced one at a time with supports, brim and prime tower off, each part gets a filament-weight estimate for the CAD-to-sim step |
| Printing (M6–M7) | FDM in PETG | Servos run warm under load, and PLA softens around 60 °C |
| Servo bus (M7–M9) | Waveshare Bus Servo Adapter (A), driven from Python with LeRobot's motor bus code | USB to the laptop, or the adapter's UART header to the Pico 2, with a jumper to choose; Feetech's wiki lists every STS register address, and model defaults come from LeRobot or from reading the servo. LeRobot's `sync_read` reads one register per call, so the telemetry block read (Conventions) uses rustypot's `sync_read_raw_data` |
| Onboard computer (M9) | Raspberry Pi Pico 2 on the servo bus | Runs the exported model and the safety supervisor, as the Bolt does in Volume 2 |
| Ground truth (M7–M9) | USB webcam with AprilTags; scanned pen traces | Independent of the servo encoders the controllers use |
| Test rig (M6–M8) | Printed pendulum bench and a torque stand, both on the kit's sixth servo; the stand's load cell and HX711 are read by their own Pico 2 | Identifies the servo while the arm stays assembled; becomes the thrust stand in Volume 2 |
| Outside servo fit (M8) | Rhoban's BAM (Apache-2.0) | A second fit of the same bench data; its library already ships a model of the 7.4 V STS3215 |
| Run data (sim and hardware) | Rerun | Sim and hardware runs 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 arm embedded in the course pages |

The controller interface is fixed from Module 2 on: goal angles for the four arm joints plus a gripper goal, at 100 Hz. The servos' own position loops stay in charge, so the trained model can run off-board over USB first and onboard the Pico 2 in Module 9. On hardware every goal passes the safety supervisor (Conventions) before it reaches the bus.

## Hardware and budget

The arm runs on five Feetech STS3215 12 V serial bus servos: base rotation, shoulder, elbow, wrist pitch and gripper. A sixth drives the pendulum bench and the torque stand, so Module 8 can measure a servo while the calibrated arm stays assembled, and it is the spare. The kit costs about 505 € in parts, including a Bambu Lab A1 mini at its October 2026 price of 189 € (regular price 319 €). Learners who own a printer with a 180 mm bed pay about 315 €. Learners buy this kit once. Part 1 needs only a laptop, so the purchase can wait until Module 6.

The servo choice follows from the Module 6 torque gate. Take an example arm whose shoulder axis sits 10 cm above the table, so it needs 32 cm from shoulder to grip point to reach 30 cm out at table height. Stretched out horizontally, it carries the elbow servo 13 cm out, the wrist servo 26 cm out and the gripper servo 29 cm out (55 g each), three 30 g printed parts centered at 6.5, 19.5 and 29 cm, and the 230 g dummy drone at 32 cm. That takes about 12.8 kg·cm at the shoulder. Both servos are derated by the same factor of 11/12 for the voltage the daisy-chained cables lose at high current. The 7.4 V STS3215 then has 17.9 kg·cm, a 1.4:1 margin, which fails the gate. The 12 V version has 27.5 kg·cm (2.2:1) for about 2 € more per servo. Its rated torque, the most it can hold without overheating, is 10 kg·cm, so the arm never holds the drone stretched out: Module 6 places the pad and the cradle where holding the drone needs at most the rated torque, within about 20 cm of the base axis on the example arm. A counterweight or a spring at the shoulder adds margin.

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

| Part | Qty | Source | Price incl. 19% VAT |
| --- | --- | --- | --- |
| Feetech STS3215 12 V 30 kg·cm servo (ST-3215-C018): five for the arm, one for the bench and the torque stand | 6 | [Eckstein](https://eckstein-shop.de/Motors?mf=1602), 26.45 € each | 159 € |
| Waveshare Bus Servo Adapter (A), SKU 25514 | 1 | [Eckstein](https://eckstein-shop.de/WaveShare-Serial-Bus-Servo-Driver-Board-for-ST-SC-Serial-Bus-Servos) | 6 € |
| 12 V 5 A desktop supply with 5.5/2.1 mm plug (Mean Well GST60A12-P1J) | 1 | [Reichelt](https://www.reichelt.de/de/de/shop/produkt/tischnetzteil_60_w_12_v_5_a-169684) | 18 € |
| Mains cable, IEC C13 to Schuko (the supply has a C14 inlet and no cable) | 1 | Any electronics shop | approx. 5 € |
| Inline switch for the 12 V line, the kill switch | 1 | Any electronics shop | approx. 5 € |
| Bambu Lab A1 mini (skip it if you own a printer with a 180 mm bed) | 1 | [3DJake](https://www.3djake.de/bambu-lab/a1-mini), regular price 319 € | 189 € |
| PETG filament | 1 kg | Any filament shop | approx. 20 € |
| M3 heat-set inserts | 100 | [3DJake](https://www.3djake.de/ruthex/gewindeeinsatz-m3-100-stueck) | 9 € |
| M2 and M3 screws | 1 set | Any hardware shop | approx. 10 € |
| Raspberry Pi Pico 2 with headers: one reads the bench, one runs the model and the safety supervisor in Module 9 | 2 | [BerryBase](https://www.berrybase.de/raspberry-pi-pico-2-rp2350-mikrocontroller-board-mit-headern), 6.50 € each | 13 € |
| 1 kg load cell with HX711 | 1 | [Digi-Key](https://www.digikey.at/en/products/detail/dfrobot/SEN0160/6588549), 14.88 € net | 18 € |
| USB webcam (Logitech C270) | 1 | [Reichelt](https://www.reichelt.de/de/de/shop/produkt/webcam_logitech_c270_hd-100943) | 18 € |
| Ping-pong balls, for the throw | 6 | [Intersport](https://www.intersport.de/ausruestung/tischtennis-ausruestung/tischtennisbaelle/229384/donic-schildkroet-tischtennisball-jade-poly-40-qualitaet-6-stk.-im-blister-weiss/) | 4 € |
| 8-note children's glockenspiel, for the melody | 1 | [Kirstein](https://www.kirstein.de/Glockenspiele/Classic-Cantabile-GS-8-Glockenspiel-fuer-Kinder.html), regular price | 10 € |
| Calibration weights, 10 to 500 g, for the payloads and the pendulum bench | 1 set | [Amazon.de](https://www.amazon.de/dp/B0CKV9Y1G3) | 17 € |
| String (fishing or kite line), for the swinging weight, the payload latch's pull cord and the ball in a cup | 1 roll | Any hardware shop | approx. 3 € |
| Open SimpleFOC joint (optional, Module 8 stretch): DFRobot 2804 gimbal motor with AS5600 encoder, plus a SimpleFOCMini driver | 1 | [DFRobot](https://www.dfrobot.com/product-3005.html), 18.90 $ and [5.90 $](https://www.dfrobot.com/product-3013.html) | approx. 26 € with import VAT, plus shipping |

<!-- END parts table -->

Shop prices as of October 2026, before shipping; "approx." marks typical prices that were not looked up. The servos are the biggest cost after the printer, at 159 €. Several shops list the 12 V servo as backordered or on pre-order; Eckstein had it in stock. The adapter takes 9 to 12.6 V and should match the servo voltage, per [Waveshare's wiki](https://www.waveshare.com/wiki/Bus_Servo_Adapter_(A)), so the 12 V supply plugs straight into its barrel jack. The supply's 5 A covers one stalled servo while the others move. Two stalls, or a throw that drives several joints at full effort, can trip its overload protection, which cuts the whole arm, so the sim models the shared supply (Module 1) and the safety supervisor treats a cut like the kill switch.

## Conventions and interfaces

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

| Item | Convention |
| --- | --- |
| Frames | World frame at the base axis on the table surface: x forward, y left, z up. Joint angles are zero with the arm pointing straight up and the base facing +x; positive pitch leans the arm forward (right-hand rule about +y), and positive base rotation turns it left (about +z). The gripper's zero is its jaws closed. The CAD model draws this zero pose with every servo at the middle of its range (position 2048) |
| Joints | J0 base rotation (servo ID 1), J1 shoulder (2), J2 elbow (3), J3 wrist pitch (4), J4 gripper (5) |
| Encoder | 4096 counts per turn, 0.088° per count. A calibration file holds each servo's zero offset and sign, and adapters convert counts to radians explicitly |
| Units | SI inside all code: m, s, rad, N·m, kg |
| Action | Goal angles in rad for J0–J3 plus a gripper goal angle, held by each servo's own position loop |
| Telemetry | One block read per control step (a raw sync read, see the stack table) returns each servo's registers from present position (address 56) through present current (69–70): position, velocity, load, voltage, temperature, the status flags (65) and current. Current comes in steps of 6.5 mA |
| Observation | The network sees each servo's position, velocity, load, current and voltage, plus the last goals. Temperature is logged but not observed, because the sim models warm-up only as a torque perturbation |
| Rates | Physics at 1 kHz; controllers and policy at 100 Hz with zero-order hold; bus at the servos' default 1 Mbps |
| History | Last 50 policy steps (0.5 s) of observations and goals |
| Network budget | At most 10k float32 parameters, so the model fits onboard the Pico 2 |
| Observation model | Sim gives every observed channel its register's resolution, plus the read delay and noise measured in Module 8. Load is the firmware layer's duty cycle, and voltage is the supply's 12 V minus the cable drop at the summed current |
| Servo registers | At startup the course writes the operating mode (address 33, 0 for position control), the P, D and I coefficients (16, 32 and 0 to start: LeRobot's SO-101 follower values, P halved from Feetech's default to stop the shaking), acceleration, torque limit, angle limits and protection thresholds, reads them back, and logs them with every run. Registers in the servo's EEPROM, such as the operating mode, the P coefficient and the angle limits, persist across power cycles, so the course never trusts what is already set. The SRAM registers return to their defaults whenever a servo loses power: the torque limit (48) reloads 100% from address 16, and acceleration (41) powers up at 0, which means maximum. So the startup writes run again after every power loss, before any goal |
| Overload trip | Any of the servo's own protections firing: above 80% of its maximum torque for 2 s, after which it holds only 20%; above the protection current (address 28) for 2 s; or above 70 °C. Each sets a bit in the servo's status register (address 65). A new position command clears the overload and over-current protections ([12 V datasheet](https://www.feetechrc.com/Data/feetechrc/upload/file/20260622/6391772519923113075854851.pdf), section 7-11), and the course sends one every 10 ms, so only the safety supervisor's latch makes a trip last, and the latch log is what counts trips. The course writes the protection current as 2 A, the 12 V datasheet's value; Feetech's family table defaults to 511 × 6.5 mA ≈ 3.3 A |
| Safety supervisor | The part of the app every powered move goes through: on the laptop from the Module 6 test joint through Module 8 and in Round 1 of Module 9, on the Pico 2 in Round 2. It clips each goal to the joint limits and to 3 rad/s, and it latches instead of sending a goal whose forward kinematics put the gripper or its tool below the table or outside the workspace box. It also latches when a status bit is set, a servo runs hotter than the app's limit (below the servo's own 70 °C), every servo misses a reply at once or one misses replies for 50 ms, or the supply voltage drops below 10.5 V. A latch stops all goals and turns torque off, so the arm falls limp into its rest pose, and only the learner re-arms it. Re-arming repeats the startup writes and their read-back, then sends each servo its present position before any controller runs. The throw and the ball in a cup run without the rate limit, in a cleared zone |

The latch exists because the servo can't stop by itself. A goal write turns torque back on, so a loop that kept streaming goals through a kill-switch cut would snap the limp arm back to its last goal, at full torque and maximum acceleration, the moment power returned. The STS3215 has no command timeout, and the adapter is a USB-to-serial bridge without a processor, so if the laptop stops, the servos hold their last goal until one of their protections trips. That is why no pose may need more than the rated torque for long (Module 6), and why Round 2 adds a link timeout on the Pico 2 (Module 9).

Joint sign and zero-offset mix-ups are the most common sim-to-real bug on an arm. The round-trip test catches them on the desk in two steps. For signs, the learner moves each joint by hand with torque off and compares the direction and distance of the webcam tag's motion with the encoder-based forward kinematics. A zero offset only rotates that motion without changing its length, so for offsets a printed zero gauge holds each arm joint at its zero, where the calibrated reading must be within 1°. The Module 1 comparison with MuJoCo catches the same mix-ups 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 | Hold the gripper at x = 0.28 m, z = 0.03 m for 30 s, the stretched worst case for the shoulder. Figure-eight x = 0.20 + 0.08·sin φ, y = 0.04·sin 2φ at z = 0.03 m, where the phase φ ramps up from rest over 2 s and then advances at ω for a 6 s period (peak speed about 0.12 m/s); fast variant with a 3 s period. Pick and place: a 100 g weight from y = −0.10 m to y = +0.10 m at x = 0.20 m and back, five cycles. Raised hold: the gripper at x = 0.20 m, z = 0.25 m for 30 s, high enough for a weight on a 20 cm string to swing clear of the table |
| Sim perturbations | Payload +150 g; a payload step of +100 g, 15 s into the hold, as a weight dropped 2 cm into the held cup; gear friction ×2; servo stiffness −30% on every joint, and on the shoulder alone as its P coefficient at 70% would make it; a 50 g weight swinging from the gripper on a 20 cm string; a 1° zero-offset error on the elbow; warm-up that lowers torque per volt by up to 15% over a run (copper resistance rises about 0.4% per kelvin); sensor noise and quantization; 10 and 20 ms latency |
| Hardware conditions | Nominal; payload (150 g of weights in a printed cup held by the gripper); a payload step (a printed latch above the held cup drops a 100 g weight into it 15 s into the hold, released by a pull cord from outside the arm's reach); the swinging weight; a soft shoulder (its P coefficient register at 11, 70% of the course value of 16); and hot servos (start after 5 minutes of figure-eights with the payload) |
| Matrix | Not every condition fits every trajectory. Hold: nominal, payload, payload step, soft shoulder, hot servos. Both figure-eights: nominal, payload, soft shoulder, hot servos. Pick and place, whose jaws carry their own weight: nominal, soft shoulder, hot servos. Raised hold: nominal and the swinging weight. That makes 18 hardware cells. Sim runs the same cells, with warm-up standing in for hot servos, and adds the sim-only perturbations (friction, stiffness on every joint, the zero offset, noise and latency) on the hold and both figure-eights |
| Runs | 20 seeds per sim cell, each with its own seeded start offset. 5 runs per hardware cell and controller, controller order randomized within each repeat block |
| Ground truth | True state in sim; on hardware the webcam's AprilTag position of the gripper, scored at the frame's capture time (its arrival minus the camera delay measured in Module 8), plus the scanned pen trace for the drawing variant |
| Metrics | RMS and max gripper position error; safety latches by cause; recovery time after the payload step, measured to the last entry into a 3 mm band around the target after which the error stays inside for at least 0.5 s, or "not recovered"; peak servo temperature rise; mean absolute goal-angle rate |
| Report | Mean ± std per controller and cell, with commit, config and seed on every row |

The webcam's absolute accuracy depends on its calibration. Hardware results therefore compare controllers on the same setup, never against absolute millimeter 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`, `safety.py` (the safety supervisor every powered move goes through, with test cases the learner's Pico 2 port must also pass), `tools/mjcf.py` (the MuJoCo model of the learner's arm, generated from the same `Params` for Module 1), one executable check per module in `gates/`, one per task in `tasks/`, a `PROGRESS.md` template, and CI that runs every sim gate and sim task on push, sized to fit free CI tiers.
- **Built by the learner:** `sim/`, `controllers/`, `estimators/`, `learning/`, `cad/` and the Pico 2 app.
- **Maker/checker:** Gate code and thresholds live in `gates/` and `tasks/` and are read-only for learners. Sim gates pass or fail in CI. Hardware gates run the same checks on uploaded servo and camera logs, plus a video for trainer review.
- **PROGRESS.md:** One line per module with gate status, task 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, one benchmark that scores every controller, and the first five tasks of the ladder in sim.

### Module 1 · Write your own arm sim

The learner builds a simulator of a four-joint arm from first principles, parameterized so every physical constant can later be randomized.

- **Build:** State of joint angles and velocities for base rotation, shoulder, elbow and wrist pitch. Rigid-body dynamics M(q)·q̈ + C(q, q̇)·q̇ + g(q) = τ, either from the recursive Newton-Euler algorithm or from a Lagrangian that JAX differentiates. RK4 integration at 1 kHz. A servo layer per joint: a DC motor behind a 345:1 gearbox, with stall torque that scales with supply voltage, torque that falls with speed, Coulomb and viscous friction, rotor inertia reflected through the gearbox, and 12-bit encoder quantization. One shared 12 V supply behind them: the servo currents add up, the voltage at the servos drops with the summed current through the cables, and a sum above 5 A for more than 10 ms cuts every servo, a cautious stand-in for the supply's overload protection. A firmware layer that models the servo's position loop with the P, D and I coefficients the course writes to its registers. Feetech documents the loop only as a PID, with scale hints of 1/4 and 1/8 for the P and D registers but no units, so how a register value turns into torque stays a parameter until Module 8 measures it. Rhoban's BAM library gives a starting structure and starting values: a P controller that outputs a PWM duty cycle, with the register-to-duty gain measured on an oscilloscope, behind a ramp that rate-limits the goal, and a fitted model of the 7.4 V STS3215 whose stall torque and no-load speed the learner scales to the 12 V datasheet. The gripper is kinematic: closing on an object's handle attaches the object's mass and inertia to the last link.
- **Learn:** Forward kinematics, the manipulator equation, why reflected rotor inertia (MuJoCo's armature) can outweigh a link's own inertia behind a 345:1 gearbox, and why explicit Euler integration drifts.
- **Deliverable:** `sim/` with a pure `step()` function and a `Params` dataclass holding link lengths, masses and inertias, servo constants, friction and the loop mapping. The SO-101's link lengths, masses and inertias from MuJoCo Menagerie are the defaults until Module 6 replaces them, with its wrist roll and gripper lumped into the last link. Its servo gains are not. Menagerie keeps TheRobotStudio's gains, computed with a third-party method that assumes a P coefficient of 16, and the file says they are not a one-to-one mapping of the gains LeRobot uses. They also make the position loop far too stiff: kp ≈ 998 N·m/rad is 60 to 130 times BAM's fit of the same servo (about 8 to 15 N·m/rad). With Menagerie's gains, 150 g at the hold pose sags the gripper by about a tenth of a millimeter; with BAM's 7.4 V fit, by 6 to 12 mm. A sag of millimeters is what the payload tasks and the Module 2 and 3 gates are built on. The firmware layer therefore starts from the BAM model above.
- **Gate:** In a stretched horizontal pose, the holding torque at each pitch joint (shoulder, elbow and wrist; the base turns about a vertical axis and holds no gravity torque) equals the summed m·g·r of everything outboard of it within 1%. With friction and servos off, a passive swing conserves energy within 0.1% over 10 s, measured against the swing's peak kinetic energy so the choice of energy zero doesn't matter. The course's torque-step excitation on each joint tracks MuJoCo within 0.5° over 0.5 s, with friction and servo terms off in both (the limp arm is a multiple pendulum and diverges quickly, so keep the horizon short). The servo model reproduces the datasheet's stall torque and no-load speed at 12 V within 5%. None of these checks touches the closed position loop, which stays unvalidated until Module 8.
- **Task:** Wave hello. Send open-loop sines to the elbow and wrist, and plot how the servo model shrinks and delays the wave as the frequency rises.

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

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

- **Build:** Inverse kinematics: base rotation from the target's bearing, then a planar three-link solution with the wrist angle set by the task. The stock baseline sends the IK angles straight to the servos. Two classical controllers go on top: an outer PID per joint whose integral removes gravity sag, and gravity feedforward that offsets each goal by the nominal gravity torque divided by the servo's stiffness. The sim perturbations from the Benchmark spec, including warm-up and 10 to 20 ms latency.
- **Learn:** Inverse kinematics and the Jacobian, why a position servo sags under load, PID tuning, feedforward, and why integral action fixes steady-state sag but not transients.
- **Deliverable:** `controllers/` plus a first results table from `bench.py`, the course's harness.
- **Gate:** The best classical controller keeps the nominal RMS error on the slow figure-eight under 2 mm. At least one perturbation pushes the maximum error past 10 mm or trips an overload; a baseline that survives them all documents a separate stress sweep instead of being weakened. A written cause for each result.
- **Task:** Draw. The figure-eight, then the learner's name from an SVG path. With 150 g of weights clipped to the pen holder, the stock controller's drawing sags visibly.

`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 the payload mass and one disturbance torque per joint, measuring joint angles, velocities and servo currents. Feed the estimates back: goal offsets that cancel the payload's gravity sag, plus cancellation of the estimated disturbance. Stretch: Slotine and Li's adaptive controller, with its joint torques turned into goal offsets through the servo stiffness, as gravity feedforward does. Its convergence proof assumes torque inputs, so here it is a heuristic.
- **Learn:** Kalman filter derivation, observability, persistence of excitation (why a static hold can't tell payload mass from the servos' torque constant, and why static friction hides small payloads), disturbance observers.
- **Deliverable:** `estimators/ekf.py` plus a `bench.py` rerun.
- **Gate:** From a prior at least 50 g off, the payload estimate stays within 10 g, with 2σ within 15 g, from 2 s into a hold plus a small excitation move. On the +150 g case, tracking error after those 2 s stays within 1.5× nominal or nominal plus 1 mm, whichever is larger, with no overload trip.
- **Task:** Weigh it. The arm lifts a cup of weights, moves it briefly and reports the mass.

### Module 4 · Answer B, built

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

- **Build:** Generate runs with randomized payload, friction, servo stiffness, torque constant, backlash and latency. A history encoder (GRU or 1-D conv over the last 50 steps of observations and goals, 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 goals, given state, goals and z. Over a single 10 ms step the residual is mostly encoder quantization.
- **Ablation:** Same encoder trained with the other three objectives: A reconstructs the present sensor readings, C separates runs contrastively, D only minimizes latent size.
- **Learn:** Residual learning over a nominal model, what makes a latent identifiable, why the motor currents reveal payload mass only relative to the torque constant, linear probing.
- **Deliverable:** Trained models plus a probing notebook that regresses true parameters from z.
- **Gate:** With objective B, a linear probe recovers the ratio of payload mass to torque constant, m/k\_t, with R² above 0.9 on held-out arms, scored per history window as the median over the same three or more training seeds for every objective. A, C and D are reported side by side, whatever they show.
- **Task:** Which cup is heavier? Three identical-looking cups hold 50, 100 and 150 g. The arm wiggles each for 0.5 s, the probe reads the latent, and the arm lines the cups up by weight.

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, and an overload trip, a safety-limit hit or a supply cut ends the episode. A privileged encoder maps the true parameters to z; the policy sees the observed channels (Conventions) and z and outputs goal angles.
- **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 overload trip or safety abort, and tracking error within a non-inferiority margin of the Module 3 EKF (1.10× plus 1 mm), in every held-out cell, and no higher in total across the adaptation conditions (every sim perturbation except sensor noise and latency). Recovery within 1 s after the +100 g payload step. Variant B and RMA reported side by side.
- **Task:** Throw. The same trainer with a new reward flings a ping-pong ball from a printed spoon into a cup on the table 40 cm from the base, past the arm's reach; the Module 1 sim sets the final distance. The sim keeps the ball in the spoon until the spoon can no longer push it, then flies it with quadratic drag. Ball mass, drag and servo strength are randomized, and the shared supply limits how many joints can push at full effort at once. Stretch: ball in a cup, with the string modeled as a constraint that is either taut or slack.

**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` in sim. The frozen encoder saw narrower data than RMA's adaptation module, so treat Variant B as a transfer test.

**Constraints:** at most 10k parameters, observation noise and delay, command jitter and dropped bus replies 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. Unlike Volume 2, this volume has no rented-GPU comparison episode.

## Part 2 · Hardware

Modules 6 to 9 build the arm 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 arm in CAD

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

- **Build:** Pick components first (five servos, bus adapter, supply, Pico 2) and model each as a mass block, with the STS3215 STEP file as the servo body. Draw the harness in WireViz: the cables from the supply through the kill switch to the adapter, the servo chain in ID order, and the Pico 2 tap for Module 9. The trainer reviews it before the test joint below is powered. Then a parametric arm in build123d: link lengths, servo brackets with horn couplings, a base for the base servo, cable routing past every joint, a gripper, tool handles keyed to the jaws (spring-loaded pen holder, spoon, mallet, cup), an AprilTag mount on the gripper, a desk clamp, the dummy drone with its Volume 2 handle, the dock's pad and cradle, a zero gauge (a square that sets the shoulder upright against the base, and straight bars that line up neighboring links), a frame for the drawing sheet with printed registration marks, and a latch on a stand that drops the payload step's weight into the held cup. Design every part for how it prints: decide its orientation in the build volume while drawing it, because that sets the overhangs and where the layers sit. Lay links flat so their load runs along the printed lines, not across layers, and round inside corners with fillets: a sharp right angle printed upright snaps at a layer line. Screws can go straight into the plastic: draw the hole at the thread's core diameter plus 0.1 mm (2.6 mm for M3) and let the screw cut its own thread. That suits parts that stay closed; heat-set inserts suit joints that are opened again and again, because a self-cut thread wears out. Draw the arm in its zero pose, pointing straight up as Conventions defines it, with every servo at the middle of its range, so the calibration in Module 7 only trims small offsets. Print one joint first: the kit's sixth servo, its bracket and a lever with a weight. It tests the bracket before the whole arm is drawn, and it stays assembled as Module 8's pendulum bench.
- **Learn:** Code-CAD, design for FDM printing (layer orientation for bending loads, walls, infill, heat-set inserts), torque budgets, and why link stiffness and backlash matter to the servo 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 link masses and inertias from per-part slicer estimates and weighed servos (solid CAD overstates printed mass because of infill) and writes them into the sim's `Params`.
- **Gate:** Every printed part fits a 180 × 180 × 180 mm build volume. The gripper reaches 0.30 m from the base axis at table height, which covers the benchmark, the drawing area and the dock. Torque margin of at least 2:1 on every pitch joint with the 230 g dummy drone at full horizontal reach, from stall torque derated by 11/12 as under Hardware and budget. The pad and the cradle sit where holding the dummy drone needs at most the servo's rated torque (10 kg·cm) at every joint, and so does every pose the dock path holds, so an arm that stops anywhere on it can keep holding the drone. The Module 2 controller draws the figure-eight with the CAD arm in sim within the nominal gate. The trainer reviewed the wiring plan before the first power-on.
- **Task:** Dock the drone, in sim. The Module 2 controller moves the CAD arm's dummy drone from the pad to the cradle and back.

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

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

The learner prints and builds the arm, sets up the servos, and makes the first real moves on the servos' stock loops.

- **Build:** Print the arm, the tool handles, the payload cups and their latch, the zero gauge, the drawing frame, the dummy drone with the dock's pad and cradle, the pendulum bench, and a torque stand with a load cell and an HX711, read by the bench Pico 2, which streams to the laptop. Set each servo's ID and baud rate one at a time, move it to the middle of its range (position 2048 of 4096) before fitting the horn, with the joint at its CAD zero, so zero offsets stay small. The horn's spline only fits in steps, so the rest is a software offset in the calibration file. Set up the startup writes (Conventions), with angle limits that keep each joint inside its mechanical range. They can't keep the gripper off the table: limits per joint form a box in joint space, the table is a plane in the gripper's space, and a combination of allowed angles can still reach below it. The safety supervisor's workspace check covers that from the first powered move. Mount the webcam, calibrate it with a checkerboard, turn on the tag detector's sub-pixel corner refinement, and locate the arm's base from a tag on the base plate.
- **Safety:** The arm pinches fingers and sweeps things off the desk. Keep hands out of its reach while torque is on; the payload step's weight drops from the printed latch, released by a cord pulled from outside the reach. Every powered move goes through the safety supervisor, from the first one on (Conventions). The switch in the 12 V line cuts every servo at once, and the arm falls limp, so give it a rest pose to fall into. Closing the switch again must move nothing until the learner re-arms. If the laptop stops, the servos hold their last goal, so the switch is also the way to release a frozen arm. Stall at most one servo at a time: two stalled servos draw more than the supply's 5 A, and its overload protection cuts the whole arm. Throw only ping-pong balls, and keep faces out of the throw line.
- **Learn:** Serial bus servos, calibration, joint limits and what they can't protect, failsafes, camera calibration, and hand-eye calibration.
- **Deliverable:** A moving arm, a build log with photos, the wiring plan updated to the wiring as built, a BOM with weighed (not datasheet) masses, the calibration file, the failsafe log with its video, and videos of the first wave, the first drawing and the first melody.
- **Gate:** A fixed tag filmed for 30 s gives the camera noise for Module 8. The round-trip test passes: with torque off, each hand-moved joint moves the webcam's tag in the direction the forward kinematics predicts, and by within 10% of the predicted distance, and with the zero gauge fitted, every calibrated arm joint reads within 1° of zero. Returning to a taught pose 10 times, the gripper tag stays within 2 mm of its mean position, or within three times the fixed tag's standard deviation if that is larger. The failsafe test passes on video, with the arm holding a pose: opening the kill switch drops it limp into its rest pose and latches the supervisor; closing the switch moves nothing until the learner re-arms, after which the read-back is clean and the arm starts from where it lies; a temperature limit set below the present temperature latches; a goal below the table latches without being sent; and stopping the app leaves the arm holding until the kill switch drops it. The arm holds the stretched pose with 150 g for 30 s without an overload trip. The first figure-eight is drawn on stock settings and scanned.
- **Task:** Play a melody on stock settings and record it with the laptop's microphone. Notes after long moves land late, and Module 8 finds out by how much.

### Module 8 · Measure the real arm

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

- **Build:** On the pendulum bench, whose servo is the kit's sixth, record trajectories with known masses at known lengths under varying load and P coefficient, then fit friction (Coulomb and viscous, plus Stribeck where the data needs it), backlash, the torque-speed limit and the mapping from register gains to torque per radian. Fit BAM's models to the same data as the outside comparison. On the torque stand, torque against the torque-limit register and against the reported current, up to half of stall: a 1 kg load cell on a 15 cm lever reads at most about 15 kg·cm. The bench Pico 2 reads the load cell and the laptop reads the servo current over USB, on separate clocks, so match them by laptop time and average each settled hold. A scale for every link and the dummy drone. Swing periods for the link inertias. The command-to-motion delay and how it grows with the size of the move, the bus round trip, the resolution and noise of every observed telemetry channel, the cables' voltage drop at a known summed current, the camera's noise and delay, and how friction and torque change once 5 minutes of loaded swings have warmed the bench servo, as the payload figure-eights warm the arm's.
- **Learn:** System identification of an actuator whose firmware you can't read, physics-based servo models next to a learned actuator network, and honest error bars. Also why LeRobot halves the P coefficient from its default of 32 to stop the arm shaking, and why one of its users then found the arm stuck in static friction ([issue #3400](https://github.com/huggingface/lerobot/issues/3400)).
- **Deliverable:** `params_real.yaml`, a CAD-versus-real mismatch table, the identified servo model, the wave's sim and real joint traces overlaid, 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 identified servo model predicts held-out pendulum runs within 1° RMS. The currents measured while holding three known payloads match the model within 10%. Link masses are always measured.
- **Task:** Play the melody on the beat. Send each strike command early by the delay the identified servo model predicts for its move, record the melody again, and compare its timing with Module 7's.
- **Stretch:** Switch the bench servo to its open-loop PWM mode (address 33 = 2) and close the position loop on the Pico 2. Code the learner wrote then replaces the closed firmware's loop on the hardware itself. Mode 2 has no position hold, and its duty register (44) powers up at 100%, so a sign error in the new loop spins the lever at full speed. The Pico 2 clamps the duty it writes to a set maximum, and it zeroes the duty and turns torque off when the angle leaves the lever's swing range or stops changing while duty is applied. The stretch runs only on the bench servo, never in the arm, and it ends by writing mode 0 back; every startup writes and checks the mode anyway. Volume 2 ports the Crazyflie's open rate controller only into its sim, while the stock loop flies.
- **Stretch:** An open joint. Build one joint from the optional SimpleFOC parts (gimbal motor with AS5600 encoder and a SimpleFOCMini driver) and identify it on the same bench next to the STS3215: the same fits, on a motor whose firmware the learner can read.

### Module 9 · Run the trained model

The learner runs the Module 5 model on the printed arm, benchmarks it against the classical controllers and replays the task ladder.

- **Deploy:** Round 1: every controller runs on the laptop at 100 Hz and sends goal angles through the USB adapter, so latency is equal for all. Round 2: the learned model runs on the Pico 2, exported from JAX to C, and the Pico 2 talks to the servo bus itself, through its port of the safety supervisor.
- **Safety:** The safety supervisor from Module 7 stays on in both rounds: joint limits, the 3 rad/s rate limit (lifted only for the throw and the ball in a cup, which run in a cleared zone), a workspace box that keeps the gripper above the table and in the camera's view, the temperature limit, the latch and the kill switch. In Round 1 the servos hold their last goal if the laptop stops, as in Module 7. In Round 2 the Pico 2 is the bus master and adds a link timeout: when the laptop's heartbeat stops for 0.2 s, it lowers the arm slowly to its rest pose and turns torque off. Before the first Round 2 run, the Pico 2 repeats the Module 7 failsafe test, plus the timeout with the USB cable pulled.
- **Test protocol:** The benchmark matrix from the Benchmark spec, run by the stock loop, PID, gravity feedforward, EKF and the learned model in randomized order, five runs per cell and controller. Then the task ladder on the real arm: the wave, drawings by every controller under payload, weighing, cup sorting, the melody, 20 throws per policy, 10 dock cycles with the dummy drone, and the ball in a cup as a stretch.
- **Deliverable:** Logs, the hardware `bench.py` table, the scanned drawings overlaid per controller, and a video of every task.
- **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 run-to-run noise. Inference finishes within 2 ms worst case on the Pico 2. The Round 2 failsafe test passed on video. The learned model never causes a safety latch, and every latch has a written cause. The dock task passes 10 of 10 cycles within the Task ladder's current limit. A tuned classical controller may win on hardware, and that is a valid result.

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

The arm stays on the desk when the course moves on to the drone. In Volume 2 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 so both volumes design against it:

- The handle on the drone's frame is the one on the Module 6 dummy, and it counts toward the drone's 210 g design target. The dummy weighs 230 g, the drone's 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 0.30 m reach, where holding the drone needs at most the rated torque (Module 6). Volume 2 flies only inside a net, so the pad must sit inside it too: the arm stands at the net's edge, or the net leaves a port for it. Which of the two is an open question in both specs.
- 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. If the reports stop while the arm holds the drone, the arm stops and holds, which every dock pose allows, and the learner unplugs the drone's battery before reaching in, because switching the Bolt off leaves its ESC live. The drone arms only while the arm's latest report, at most 0.5 s old, says it is in its rest pose.
- The torque stand, with its load cell, HX711 and bench Pico 2, becomes Volume 2's thrust stand.

The drone spec states the same interface in its "Volume 1 hand-off" section, from the drone's side. Charging the drone in the cradle is an open question.

## Reading spine

One to four 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 papers and books are marked optional.

| Module | Reading | What it gives |
| --- | --- | --- |
| M1 | Lynch and Park, [*Modern Robotics*](https://hades.mech.northwestern.edu/images/2/25/MR-v2.pdf) (Cambridge, 2017; free PDF from the authors), Ch. 8; Tedrake, *Underactuated Robotics* (MIT, free online), [the manipulator equations](https://underactuated.csail.mit.edu/multibody.html#manipulator) | Open-chain dynamics with Newton-Euler, gearing and friction; the manipulator equation |
| M2 | Tedrake, *Robotic Manipulation* (MIT, free online), [Ch. 3 "Basic Pick and Place"](https://manipulation.csail.mit.edu/pick.html); Lynch and Park, Ch. 11 | Forward kinematics, Jacobians and differential IK; joint-space PID and computed torque |
| M3 | Labbe, *Kalman and Bayesian Filters in Python* (free online); Zhang, Zhou, Vasudevan, ["Provably-Safe, Online System Identification"](https://arxiv.org/abs/2504.21486) (RSS 2025). Optional: Atkeson, An, Hollerbach, "Estimation of Inertial Parameters of Manipulator Loads and Links" (IJRR, 1986); Slotine and Li, "On the Adaptive Control of Robot Manipulators" (IJRR, 1987) | Kalman filtering; identifying an unknown payload online on a real arm; the classic payload estimator and the adaptive baseline |
| M4 | Hwangbo et al., ["Learning agile and dynamic motor skills for legged robots"](https://arxiv.org/abs/1901.08652) (Science Robotics, 2019) | A network that learns a real actuator from its position error and velocity history |
| M5 | Kumar et al., ["RMA: Rapid Motor Adaptation for Legged Robots"](https://arxiv.org/abs/2107.04034) (RSS 2021); Liang, Ellis, Henriques, ["Rapid Motor Adaptation for Robotic Manipulator Arms"](https://arxiv.org/abs/2312.04670) (CVPR 2024); Peng et al., ["Sim-to-Real Transfer of Robotic Control with Dynamics Randomization"](https://arxiv.org/abs/1710.06537) (ICRA 2018); Zeng et al., ["TossingBot"](https://arxiv.org/abs/1903.11239) (RSS 2019) | The two-phase recipe, and its arm version in simulation; dynamics randomization carrying a puck-pushing arm from sim to real; throwing with a learned residual on top of ballistics |
| M6–M7 | build123d documentation; Feetech's [STS memory table](https://github.com/ftservo/ftservo-wiki/blob/a4f9de2388ade2b9d811635bc748c0c2ff9a7189/docs/en/reference/parameter/memory-sts.md); Waveshare's [Bus Servo Adapter (A) wiki](https://www.waveshare.com/wiki/Bus_Servo_Adapter_(A)) | Code-CAD; every servo register; wiring the bus over USB or UART |
| M8 | Duclusaud, Passault, Padois, Ly, ["Extended Friction Models for the Physics Simulation of Servo Actuators"](https://arxiv.org/abs/2410.08650) (ICRA 2025) and the [BAM docs](https://bam.readthedocs.io/); Tedrake, [system identification chapter](https://underactuated.csail.mit.edu/sysid.html). Optional: Ljung, *System Identification: Theory for the User* | The pendulum bench and friction models beyond Coulomb-viscous (the paper tests Dynamixel and eRob servos; BAM added the STS3215 later); fitting models to measured data |
| M9 | Kober and Peters, ["Policy Search for Motor Primitives in Robotics"](https://proceedings.neurips.cc/paper_files/paper/2008/file/7647966b7343c29048673252e490f736-Paper.pdf) (NIPS 2008); OpenAI et al., ["Solving Rubik's Cube with a Robot Hand"](https://arxiv.org/abs/1910.07113) (2019) | Ball in a cup learned on a real arm from one demonstration in about 75 trials; automatic domain randomization |

## Episode map and access tiers

Six free episodes carry the story: the hook, the first two builds, the Module 4 result, the CAD design and the task ladder on the real arm. 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 on a robot arm | Intro | Free |
| 1 | I wrote a robot arm simulator from scratch | M1 | Free |
| 2 | I broke my robot arm on purpose | M2 | Free |
| 3 | Teaching a robot arm to weigh things | 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, and a throw | M5 | Customer |
| 7 | I designed a robot arm in Python | M6 | Free |
| 8 | Printing and building it: the first wave and the first drawing | M7 | Customer |
| 9 | How wrong was my CAD? Identifying a servo I can't open | M8 | Customer |
| 10 | The model meets the printed arm | M9 | Customer |
| 11 | My arm draws, weighs, throws and docks a drone | M9 | Free |
| T | Gate rubrics and common failure modes per module | All | Trainer |

## Open questions

- [ ] Bus timing: LeRobot measured sync reads of six STS3215 positions at about 1 ms median on a 1 Mbps bus ([PR #4672](https://github.com/huggingface/lerobot/pull/4672#issuecomment-6039985869)). The course reads the longer block from address 56 to 70 from five servos and writes five goals every 10 ms. Measure that in Module 7 before fixing the rate in Conventions.
- [ ] Current resolution: the servo reports current in 6.5 mA steps ([Feetech memory table](https://github.com/ftservo/ftservo-wiki/blob/a4f9de2388ade2b9d811635bc748c0c2ff9a7189/docs/en/reference/parameter/memory-sts.md)). From the datasheet's 30 kg·cm at 2.7 A, 10 g held 28 cm out is about 4 steps, and gearbox friction is larger than that. If the steps are too coarse for the Module 3 gate and the Module 4 probe target, both fall back to position sag, and the 10 g threshold moves.
- [ ] Servo voltage range: the 12 V datasheet says 4–14 V, Feetech's wiki 9–12.6 V, Waveshare's servo page 6–12.6 V. The kit runs at 12 V, inside all three.
- [ ] Servo stock: several shops list the 12 V servo as backordered or on pre-order. Check stock before ordering learner kits.
- [ ] Throw range: a rough estimate puts 50 cm from the base near the limit. At the 12 V servo's no-load speed of 45 RPM (0.222 s per 60°, [datasheet](https://www.feetechrc.com/Data/feetechrc/upload/file/20260622/6391772519923113075854851.pdf)), one joint moves a spoon 30 cm out at most 1.4 m/s, so only a whip of several joints gets that far, and the 5 A supply limits how many of them can push at full effort at once. The task starts at 40 cm, where an SO-101 project already landed throws (see the next item), and the Module 1 sim sets the final distance.
- [ ] Task feasibility: throwing and xylophone strikes have precedents on these servos. An SO-101 student project threw a ball into a basket 47–60% of the time at 40 cm with an imitation-learned policy ([repo](https://github.com/nxp0999/Robotics-SO-101-Imitation-Learning)), and an SO-100 dataset strikes eight xylophone notes ([dataset](https://huggingface.co/datasets/ArjunPrasaath/training_xylophone)). Pen drawing and the ball in a cup have none yet. Prototype both before recording them.
- [ ] Open joint: keep the SimpleFOC joint as a Module 8 stretch, make it core, or drop it? It adds about 26 €, taking the kit to about 530 €.
- [ ] Supply headroom: the sim cuts every servo when the summed current stays above 5 A for 10 ms. How strict the real supply's overload protection is, and whether the throw needs a larger supply, shows only in the first hardware throws.
- [ ] SO-101 track: a follower with the stock 7.4 V servos runs them from 5 V and fails the torque gate at 230 g, while a 12 V follower is checked against the gate like the course arm. Either way its sixth servo turns the wrist. Give owners a separate track, or leave them out?
- [ ] Outside reference for Module 9: Volume 2 races RAPTOR, and this volume has no counterpart yet. One candidate is the same policy trained in MuJoCo with a BAM servo model fitted to the learner's bench data.
- [ ] Dock layout: does the arm stand at the edge of Volume 2's net, or does the net leave a port for it?
- [ ] Charging the drone in the Volume 2 cradle.
