# Sim-to-real arm · trainer guide

Volume 1 of the sim2real course takes a learner 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. Nine modules end in measurable gates, and eight
tasks put the modules' deliverables to work. The starter kit ships the benchmark
(`bench.py`), the safety supervisor (`safety.py`) and one automated check per
module and per task. Each gate also has a part that only a person can judge.
This guide says, per module, what the automated check computes, what you review
by hand and where learners usually go wrong.

Use one kit version for a cohort: `2026.10.23`, recorded as `kit_version` in
every evidence file. The checks reject evidence, benchmark manifests, reports and
safety cases from another version, so the results of one cohort are comparable.
Every threshold is a starting value. The course author'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 of Modules 2 to 5) after his Module 8 has identified a real servo. Each
recalibration ships as a new kit version with its reason. Record the version
each learner used before you compare learners.

An automated PASS means only that the supplied evidence met the checks. The
checks recompute every measurable number from the raw evidence: logs, traces,
part lists, per-window predictions. They never trust a summary number the
learner wrote where the raw data can be checked. They can only hash videos and
review records, and their PASS message says so. Your approval covers the
implementation, the provenance of the data and the physical work. Do not publish
invented timings, runs, R² values or gate passes.

For every review, record the learner and reviewer, the date, the implementation
commit, config hash, seeds, kit version, artifact hashes, the raw log location,
the automated result, your review result and any required retry. Preserve failed
trials. Read the standard deviation and the latch counts together with the mean.
A result that disagrees with the course's expected story is not a failed course
if the experiment is sound.

Three reviews gate the hardware:

1. The wiring plan, before the Module 6 test joint is first powered.
2. The Module 7 failsafe video, filmed before the arm's first wave, drawing and
   melody.
3. The Round 2 failsafe video on the Pico 2, with the link timeout, before the
   first Round 2 run in Module 9.

## The safety supervisor, in short

Every powered move goes through the supervisor: on the laptop from the Module 6
test joint through Module 8 and in Round 1 of Module 9, and as the learner's
port on the Pico 2 in Round 2. It clips each goal to the joint limits and to
3 rad/s. It latches instead of sending a goal whose forward kinematics put the
grip point or a rigid tool's tip within 1 cm of the table or outside the
workspace box. A spring-loaded pen touches the paper by design, so its tip is
not checked. It also latches on any servo status bit, a temperature above the
app's limit (set below the servo's own 70 °C), every servo missing a reply at
once (a kill-switch cut) or one missing replies for 50 ms, and a supply below
10.5 V. The throw and the ball in a cup run without the rate limit, in a cleared
zone.

A latch stops all goals and requires torque off, so the arm falls limp into its
rest pose. Only the learner re-arms it. Re-arming repeats the startup register
writes and their read-back, needs a clean telemetry read and a pose inside the
limits, and then starts from the present positions. In Round 2 the Pico 2 adds a
link timeout: 0.2 s without the laptop's heartbeat lowers the arm at half the
rate limit to its rest pose, then torque goes off.

The latch exists because the servo cannot stop by itself. A goal write turns
torque back on, so goals streamed through a kill-switch cut would snap the limp
arm back to its last goal at full torque and maximum acceleration when power
returns. The STS3215 has no command timeout, and the bus adapter has no
processor, so a stopped laptop leaves the servos holding their last goal until
a protection trips. The kit's `safety_cases.json` holds the reference scenarios
for every rule, and the learner's Pico 2 port must reproduce every step of them.

## Module 1 · Write your own arm sim

**Automated:** in a stretched horizontal pose, the sim's holding torque at the
shoulder, elbow and wrist must be within 1% of Σ m·g·r, which the check
recomputes from the learner's part list with g = 9.81 m/s². With friction and
servos off, a passive swing of at least 10 s, logged every control step, may
drift in total energy by at most 0.1% of its peak kinetic energy. The spec
measures against the peak kinetic energy because no choice of energy zero can
change it. The MuJoCo comparison runs exactly 0.5 s of the kit's canonical
torque-step excitation, written by `bench.py m1-sequence` and checked by hash.
Both simulators start at its initial pose, and every joint must agree within
0.5° at every step. The check reads the servo model's stall torque and no-load
speed from its torque-speed curve at 12 V; both must be within 5% of the
datasheet's 2.942 N m and 4.712 rad/s. The course writes the MuJoCo model, not
the learner: the check regenerates the comparison model from the learner's
Params JSON with the kit's `tools/mjcf.py` and requires the submitted model to
match byte for byte. The sim source is hashed.

**Review:** read the pure `step` and the `Params` dataclass. Check forward
kinematics against the course conventions by hand: the zero pose points straight
up, a positive shoulder pitch moves the gripper toward +x, and with the shoulder
pitched forward a positive base rotation moves it toward +y. Check that M(q) is
symmetric and positive definite, that RK4 runs at 1 kHz with each 100 Hz action
held for ten steps, and that the explicit Euler drift plot was kept. In the servo
layer, look for the 345:1 gearbox, stall torque that scales with voltage, torque
that falls with speed, Coulomb and viscous friction that really sticks, reflected
rotor inertia and 12-bit quantization. Check the shared supply (summed currents,
the cable drop, the cut above 5 A for more than 10 ms) and that overload trips
are flagged as the conventions define them, because Modules 2 and 3 count them.
The firmware layer must start from BAM's structure, with its 7.4 V fit scaled to
12 V, the loop mapping as a Params entry and the course's startup P, D and I
(16, 32 and 0). Compare the learner's sag plot with BAM's stiffness against
Menagerie's gains. Confirm that the Params JSON is the one the sim itself loads,
that friction, servo terms and armature were off in the learner's sim as they
are in the comparison model, and that nothing was rescaled afterwards to match. Ask the learner what
the checks leave open: none touches the closed position loop, and the MuJoCo
match validates the dynamics code, not the parameters or the servo model.

**Common failures:** Menagerie's servo gains imported with its link masses. Its
kp of about 998 N·m/rad sags 150 g at the hold pose by about a tenth of a
millimetre instead of 6–12 mm, and every payload task looks solved. Stall torque
left in kg·cm or speed in RPM, about ten times too large. A smoothed Coulomb term
that lets joints creep under small loads; stick or slip must be decided once per
physics step, outside the RK4 stages. A joint sign flipped consistently through
kinematics and dynamics: it still conserves energy and passes a magnitude-only
holding-torque check, and only the MuJoCo comparison catches it. A comparison
horizon stretched past 0.5 s, which measures chaos, not bugs.

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

**Automated:** a complete sim benchmark report for the stock servo loop, outer
PID and gravity feedforward: 36 cells, 20 seeds each, every log rehashed and
rescored. Among the classical controllers whose nominal slow figure-eight did not
latch, the best mean RMS must be under 2 mm. At least one perturbed cell needs an
overload trip (a status-bit or supply-cut latch) or a mean maximum error above
10 mm. If none does, a separate stress-sweep log must reach one. Every classical
cell needs a written cause.

**Review:** check the inverse kinematics: the base from the target's bearing, a
planar three-link solution with the wrist angle set by the task, one elbow branch
per trajectory, and unreachable targets that fail loudly. The stock baseline
sends the IK angles straight to the servos. Check the PID's anti-windup, and that
both controllers were tuned on trajectories outside the benchmark and then
frozen. Confirm that every perturbation acts on the plant while the controllers
keep their nominal assumptions: the soft shoulder through the firmware layer's
register-to-torque mapping, warm-up as falling torque per volt, the payload step
at 15 s, latency on the observations. Read each cause against its time series:
sag, lag, windup, delay, an overload trip or a supply cut, nominal cells included.
If the baseline survived the prescribed set, check that the stress sweep raised
one perturbation's magnitude and left the scored controller as it was. Failed
and latched runs stay in the report.

**Common failures:** an integrator that removes the hold's sag and hides a wrong
model, which transients and the payload step expose. Windup while a servo cannot
follow, after saturation or a supply cut, then overshoot. Feedforward that trusts
its nominal stiffness and falls short on a soft or warm servo. An IK solver that
switches elbow branches near the stretched pose, so the goals jump and the goal
rate and error spike together. A controller retuned per benchmark condition. A
robust baseline weakened to manufacture a failure.

## Module 3 · The analytical fix

**Automated:** a payload-estimate trace with its 1σ, logged every control step
from 0 s to at least 3 s. The prior at 0 s is at least 50 g from the true mass.
From 2 s on, every estimate is within 10 g and every 2σ within 15 g. On the hold
with +150 g, the EKF's mean RMS after 2 s is at most 1.5× its nominal value or
nominal plus 1 mm, whichever is larger, with no latch in either cell. The
excitation log is hashed, and the EKF is in a complete sim report.

**Review:** the state is the arm's four angles and four velocities plus the
payload mass and one disturbance torque per joint. The filter measures joint
angles, velocities and servo currents. Check the Jacobians against finite
differences and the initial covariance, Q and R with their units. Look at the
estimate ± 2σ, the disturbance estimates and the normalized innovations on one
time axis, over many seeds, from a prior both too high and too low. The
excitation move is what makes the mass observable: a still hold cannot separate
payload mass from the torque constant, and static friction hides small payloads.
Confirm that the estimates feed back as goal offsets, and that the estimate and
the tracking are scored separately. Compare EKF feedback with PID and
feedforward per cell. If the learner did the Slotine and Li stretch, it must be
labelled a heuristic, since its convergence proof assumes torque inputs.

**Common failures:** with a free disturbance torque on every joint, a still hold
splits the load between mass and disturbance by their priors; a mean near the
truth with a wide σ is luck. Q or R set too small, so 2σ shrinks fast around a
biased estimate. Good tracking with a wrong mass, because the goal offset uses
the combined gravity and disturbance estimate. Current treated as white noise in
a still hold although the reading sits on one 6.5 mA step. A prior started at
the true mass. The spec lists the current resolution as an open question: if the
6.5 mA steps prove too coarse, Modules 3 and 4 fall back to position sag, and the
10 g threshold moves in a new kit version.

## Module 4 · Answer B, built

**Automated:** per-window predictions of m/kₜ on held-out arms for objectives A,
B, C and D, with the same encoder architecture hash and the same three or more
training seeds. At least ten held-out arms, none also listed as a training arm,
at least three windows per arm and seed, and a latent of 8–16 dimensions over 50
steps. The check computes each held-out arm's target from its true parameters,
R² per seed, and the median over seeds. B's median must be above 0.9. A, C and D
are reported with no threshold. The models and the probing notebook are hashed.

**Review:** the four encoders share architecture, capacity, data, training steps
and seeds, with separately trained weights. A reconstructs the present sensor
readings, B predicts the residual 10 policy steps (0.1 s) ahead against the
nominal model's rollout, C separates runs contrastively and D only minimizes
latent size. The encoder must be causal: changing any sample after step t leaves
the latent at step t unchanged. The split must be by whole arms, made before any
window was cut and kept in git. The arm ids in the evidence are the learner's
own, so check them against the split manifest. The probe and its normalization
are fitted on training arms only. Look for the constant-z and shuffled-z controls
for every objective and seed, and reproduce a few raw predictions from the
notebook. A failed B is a diagnosis to write up, not a reason to change the
split. A strong A or C deserves an investigation, not a footnote.

**Common failures:** windows from one run on both sides of the split. A sim that
hands the encoder float currents and exact angles instead of readings at
register resolution. A forward model that ignores the latent because state and
goals already predict the residual. A contrastive objective that separates runs
by trajectory, start offset or noise seed instead of physics. A probe fitted on
held-out labels. The best seed reported alone.

## Module 5 · RMA

**Automated:** a complete sim report for the EKF, RMA and Variant B. RMA never
latches in any of the 36 cells, and its mean RMS is at most 1.10× the EKF's plus
1 mm in every cell. Summed over the cells of the adaptation conditions (every sim
perturbation except sensor noise and latency), RMA's RMS is no higher than the
EKF's. Every RMA payload-step run recovers within 1 s: the last entry into the
3 mm band, after which the error stays inside for at least 0.5 s. The deployed
policy and adaptation module hold at most 10,000 float32 parameters. The history
is 50 steps, the policy is frozen in phase 2, and Variant B trains on the frozen
Module 4 encoder. Observation noise, observation delay, command jitter and
dropped replies are randomized over ranges of nonzero width, physics is
resampled within episodes, and training takes under 2 hours on a laptop CPU, with
the machine named. The declared training seeds share none of the benchmark's run
seeds, so every scored cell is held out.

**Review:** check the reward. It pays a survival bonus larger than the largest
per-step cost, and episodes end on an overload trip, a safety-limit hit or a
supply cut, logged by cause. Read the return and episode-length curves together.
The check compares the declared training seeds with the benchmark's run seeds;
confirm in the training log that training used exactly the declared seeds.
Trace the deployed path from
observation to goal: it must never receive a true parameter, the teacher's
latent or a sim-only signal such as the warm-up factor. Phase 2 must train on the
frozen policy's own rollouts. Count the parameters of both deployed modules, and
check the training log against the claimed wall time from the first command.
The throw env must keep the summed-current cut. Treat Variant B as a transfer
test: its frozen encoder saw narrower data than RMA's adaptation module. This
volume has no rented-GPU comparison.

**Common failures:** learning to end the episode. With costs alone, tripping the
supply in the first second scores best; a rising return with falling episode
lengths is the sign. A privileged shortcut left in the deployed path, which looks
perfect in sim and helpless on the arm. A throw env that lets every joint push at
full effort at once, so the policy trips the real 5 A supply on its first try.
A good average return that hides a slow recovery after the payload step. Physics
fixed for a whole episode. The best seed chosen.

## Module 6 · Design the arm in CAD

**Automated:** every printed part's STL fits 180 × 180 × 180 mm as exported. The
gripper reaches at least 0.30 m from the base axis at table height, computed from
the link lengths and the shoulder's height and offset. The check recomputes each
pitch joint's torque from part masses and lever arms. With the 230 g dummy drone
at full horizontal reach, the stall torque derated by 11/12 gives a margin of at
least 2:1. At the pad, the cradle and every pose the dock path holds, the
required torque is at most the rated 10 kg·cm. The CAD arm's nominal
figure-eight in sim runs without a latch at an RMS under 2 mm. A wiring review
record names the reviewer, the hash of the reviewed plan and a date before the
first power-on. The check verifies that record, not the wiring.

**Review the wiring plan before the first power-on.** The rendered WireViz
diagram must show the 12 V supply through the inline kill switch to the bus
adapter, the servo chain in ID order from J0 (ID 1) to the gripper (ID 5), and
the Pico 2 tap on the adapter's UART header with the jumper that chooses USB or
UART. The adapter takes 9–12.6 V and matches the 12 V servos. Nothing is powered
before your review, the test joint included. Write your findings in the review
notes. If the plan changes after your review, its hash no longer matches and the
check fails: review the new version before power.

**Review the model:** the arm is drawn in its zero pose, straight up with every
servo at position 2048. Each part has its print orientation, with links laid flat
and inside corners filleted. Holes for self-cut M3 threads are 2.6 mm where parts
stay closed; joints that are opened again and again get heat-set inserts. Parts
print in PETG. Read `cad_to_params.py`: per-part slicer estimates with supports,
brim and prime tower off, weighed servos, the parallel-axis theorem, and
millimetres converted to metres in one place. Check the torque table, one row
per pose and one column per joint. Confirm that the test joint was powered only
through the reviewed harness, with the startup writes and the supervisor, and
checked for flex and play. Module 5's randomization ranges must now be centred on
the CAD parameters. The props must exist: the dummy drone with its Volume 2
handle, the pad and cradle, the zero gauge, the drawing frame with registration
marks, and a latch that drops the payload 2 cm.

**Common failures:** a link printed standing up, whose layers the bending load
pulls apart, often at a sharp inside corner. Millimetres in a metre world, which
puts inertia off by a factor of 10⁶. Solid-CAD mass, or a slicer estimate with
supports, brim or prime tower included. A dock pose that passes a short sim cycle
but needs more than the rated torque; a stopped laptop would hold it, heating,
until a protection trips. An STL exported in metres.

## Module 7 · Print, build, first motion

**Automated:** a fixed tag filmed for at least 30 s gives the camera noise, and
the check computes its standard deviation. In the round trip, each arm joint is
moved by hand with torque off: the tag's displacement must point where the
forward kinematics predicts, and its length must be within 10% of the
prediction. With the zero gauge fitted, every arm joint reads within 1° of zero,
computed from the raw counts with the zero and sign of the learner's calibration
file, the one the app uses. Ten returns to a taught pose
stay within 2 mm of their mean, or within 3× the fixed tag's standard deviation
if that is larger. Five failsafe items are flagged as passed, with a video and a
log. A 30 s stretched hold with 150 g shows no status bit and no latch in its
100 Hz log. The first figure-eight is drawn on stock settings and scanned. The
build log, weighed BOM and as-built wiring plan are hashed.
The check verifies the failsafe video's hash, not what it shows.

**Review the failsafe video** item by item. The arm holds a pose throughout.

1. Opening the kill switch drops the arm limp into its rest pose, and the
   supervisor latches.
2. Closing the switch moves nothing until the learner re-arms. After re-arming,
   the read-back is clean and the arm starts from where it lies, with no snap
   back to its last goal.
3. A temperature limit set below the present temperature latches.
4. A goal below the table latches without being sent.
5. Stopping the app leaves the arm holding until the kill switch drops it.

Match the failsafe log to the video, latch by latch. Then review the rest. The
startup writes (operating mode 0, P 16, D 32 and I 0, acceleration, torque
limit, angle limits inside each joint's mechanical range, protection current
2 A) are read back and compared, not only logged, and redone after every power
loss. Every powered move goes through the supervisor, quick tests included.
The horns were fitted at position 2048 with each joint at its CAD zero, so the
calibration file holds only small offsets. The camera was calibrated with a
checkerboard, sub-pixel corner refinement is on, and the base is located from its
tag at the start of every session. The hold log was taken in the benchmark's
hold pose (x = 0.28 m, z = 0.03 m) with a weighed 150 g. Compare the weighed BOM
and the as-built wiring plan with the build photos. Check that the bus traffic of
one control step was timed, as the spec asks before the rate is fixed. The
videos must show hands out of the arm's reach while torque is on. The stock
melody has no threshold; file its recording and video.

**Common failures:** a quick script that writes goals straight to the bus and
skips the supervisor; after a power loss its first goal turns torque on with a
100% torque limit and maximum acceleration. A gripper squeezing a handle hard,
which is a stalled servo: it can trip after 2 s, and it uses the one stall the
5 A supply allows. A read-back that is logged but never compared, so a spare
servo's old operating mode, P coefficient or angle limits go unnoticed. A webcam
bumped after calibration, which shifts every tag position. Two servos stalled at
once, which cuts the whole arm. A wiring plan left unchanged after the build.

## Module 8 · Measure the real arm

**Automated:** every quantity that Module 5 randomized is measured with a value,
an interval and a method, declared unresolved with an assumed range and a named
source, or listed as a task variable with a reason. Link masses are measured on
the scale. Each interval or assumed range lies inside its Module 5 range. A
widened range must contain the old one and needs a retrain log. Every held-out
pendulum run is predicted within 1° RMS, recomputed from the logged measured and
predicted angles. For three different known payloads, the mean holding current
is within 10% of the model. The real params, the mismatch table and the servo
model are hashed.

**Review:** inspect the raw bench data. The pendulum bench, on the kit's sixth
servo, records trajectories with known masses at known lengths under varying
load and P coefficient. Look at the fits of friction (Coulomb and viscous, plus
Stribeck where the data needs it), backlash, the torque-speed limit and the
register-gain mapping, with BAM's fit of the same data beside them. Held-out runs
must have been set aside before fitting, at settings the fit never saw. On the
torque stand, the load cell (read by the bench Pico 2) and the servo current
(read over USB) run on separate clocks: check that they were matched by laptop
time, that only settled holds were fitted, and that the stand stayed below half
of stall. Check the swing periods for link inertias, the command-to-motion delay
across move sizes, the bus round trip, the resolution and noise of every
observed channel, the cable drop at a known summed current, the camera's noise
and delay, and the refit after 5 minutes of loaded swings. A parameter is
unresolved only if the instruments cannot observe it, with a defensible range
rather than a convenient one. A widened range must come with a real retrain, so
the model artifact changes. Look for the wave's sim and real traces overlaid,
the sim benchmark rerun with measured values, and the melody on the beat next to
Module 7's. If the learner did the PWM stretch, it ran on the bench servo only,
the duty clamp and both cut-offs were tested before the loop was closed, and it
ended in mode 0.

**Common failures:** a hysteresis loop that is only clock skew between the two
streams. A value near the edge of its range whose error bar crosses it; the fix
is a wider range and a retrain, never a smaller error bar. Held-out runs with the
same mass, length and P coefficient as the fitting runs. A sign error in PWM
mode, which spins the lever at full speed, because the duty register powers up
at 100%. A measurable parameter marked unresolved. An edited ranges file with the
old model kept.

## Module 9 · Run the trained model

**Automated:** complete hardware reports. The off-board one (Round 1) has the
stock loop, PID, gravity feedforward, the EKF and the learned model; the onboard
one (Round 2) has the learned model. Each has five runs per cell and controller
over the 18 cells, and every log is rescored. Both name the deployed model's
hash, and they share no log. The table is published at an HTTPS URL. At least
three recorded parity sequences of at least 50 steps, each from a reset with
varying inputs, agree with JAX within 1e-4. The slowest of at least 3,000 timed
inferences takes at most 2 ms. The six Round 2 failsafe items, the link timeout
included, are flagged as passed, with a video. The Pico 2 port of the supervisor
reproduces every reference case. The learned model never latches, and every
latch has a written cause. Per cell, the onboard RMS may exceed 1.10× the
off-board result only within run-to-run noise, a one-sided 95% Student t bound.
The hardware dock evidence passes 10 of 10 cycles under the current limit. The
check verifies the videos' hashes, not what they show.

**Review the Round 2 failsafe video** case by case: the five Module 7 items on
the Pico 2, then the USB cable pulled with the arm holding a pose. The arm must
lower slowly to its rest pose and then go limp. It must not drop from where it
was, and it must not keep holding. Confirm that the video predates the first
Round 2 run.

**Review the runs:** every off-board controller used the same laptop app, USB
adapter, supervisor, 100 Hz rate, trajectories and webcam setup, so latency was
equal for all. Compare the planned randomized order with the actual run order and
every reschedule. Each run wrote its registers at its start, so the soft
shoulder's P of 11 (70% of the course's 16) never outlived its cell; check the
logged read-backs. Each run logged its start temperature, and the servos cooled before a
cold cell. Parity sequences must cover the start, while the history fills, and
realistic inputs. The inference time is the worst case over long runs, not the
mean. The exported model must be the one benchmarked off-board. Throws ran in a
cleared zone, with ping-pong balls only and faces out of the line. Review a video
of every task on the ladder. The published table must be complete, whatever it
shows. Hardware results compare controllers on the same setup, never against
absolute millimetre thresholds, because the webcam's accuracy depends on its
calibration.

**Common failures:** a soft shoulder that stays soft. The P coefficient lives in
EEPROM, so the next run inherits a P of 11 instead of 16 unless every run writes
its own registers.
Heat carried over from the hot-servo cell into a nominal run. Parity tested one
step at a time with a reset on every call, which hides recurrent drift. A
supervisor port that counts loop iterations instead of time, so its 50 ms and
0.2 s change with the loop rate. Cherry-picked runs or a missing cell. A tuned
classical controller winning on hardware is a valid, publishable result, not a
failure.

## The task ladder

Each task except the melody runs in sim first, and Module 9 replays the whole
ladder on the printed arm. Sim task checks run in CI. A hardware task check
hashes the task's video, and you review it.

| Task | Automated check | Review |
| --- | --- | --- |
| Wave hello | Sine fits on the elbow and the wrist at three or more frequencies; amplitude ratio and lag reported, no threshold | In Module 8, the sim and real traces overlaid at the same joints and frequencies |
| Draw | RMS deviation of the drawn path from the command; on hardware, three or more registration marks and their residual | The scan registered by the sheet's printed marks; in Module 9, drawings by every controller under payload |
| Weigh it | Within 10 g in sim; on hardware, the error against the 0.1 g scale reported | The cup's contents weighed on the scale; a brief move before the reading |
| Which cup is heavier? | Exactly 10 sim trials with 50, 100 and 150 g cups, shuffled, at least 9 right | Identical-looking cups, each wiggled for 0.5 s |
| Throw | At least 20 throws per policy, the cup at least 0.40 m from the base axis; a sim hit rate of at least 60%; the real rate reported | The measured distance, every throw counted, a cleared zone |
| Play a melody | Hardware only; one constant offset removed, RMS error under 20 ms after compensation | Strike times taken from the microphone recording that goes with the video |
| Dock the drone | 10 cycles without a latch, the shoulder's RMS current under its rated-torque current (0.9 A) | The 230 g dummy lifted by its handle from the pad to the cradle and back |
| Ball in a cup (stretch) | At least 20 tries; success rate reported | Every try counted |

## Accept, retry or pending

Record **accept** only after the automated result and the applicable human
review both support it. Record **retry** with the failed criterion and a concrete
diagnostic experiment. Record **pending** for missing artifacts, unperformed
hardware runs or unresolved equipment questions. A green CI run with PENDING
templates is never a completed course. Do not invent a trainer signature, an
effort estimate or a hardware result to fill a blank in the record.
