4. Flash and score standalone
The sketch carries its own copy of the operating point, and it must agree
with the policy’s config.json — the header only carries weights, not the
loop shape. Defaults already match the canonical recipe, so this is a
one-time check per rig (RLControl.ino).
The “flashed” column below is read out of RLControl.ino and
policy_weights.h when this page is built, so it is what your checkout
actually contains rather than what the docs once claimed:
| sketch constant | must equal | flashed |
|---|---|---|
CONTROL_FREQUENCY_HZ | control_freq_hz | 50 |
ACTION_SMOOTH_WINDOW | action_smooth_window | 4 |
OBS_FRAMES | obs_history_len | 4 |
MAX_VELOCITY_RAD_S | max_velocity_rad_s | 3.5 |
MICROSTEPS | your driver wiring (not training) | 32 |
# Export PROGMEM weights into the sketch directory (scratch header — the# committed per-rig champions stay untouched)python export_weights.py \ --student runs/<run>_async/distill_h16_dagger_dev/student.pt \ --header ../firmware/RLControl/policy_weights_dev.h \ --source-name <run>/distill_h16_dagger_dev
# Flash, selecting the header at compile timecd ../../..arduino-cli compile --upload -p /dev/cu.usbserial-1130 \ --fqbn arduino:avr:nano:cpu=atmega328 \ --build-property 'build.extra_flags=-DPOLICY_WEIGHTS_H="policy_weights_dev.h"' \ firmware/RLControlWith no --build-property, the sketch compiles its committed default —
the reference rig’s champion (policy_weights_rig1_tmc2209_32.h). Champions for
other rigs live alongside it, named
policy_weights_<rig>_<driver>_<microsteps>.h; to promote a new champion, export
to the named header for your rig and commit it. The rig comes first because it is
what actually distinguishes two champions — each is fine-tuned against its own
measured friction, so the same driver and microstep setting on a different rig is
a different policy.
Keep the pendulum hanging straight down through the 1 s settle delay after boot (LED solid HIGH) — that pose becomes the encoder zero for the engagement. The sketch then swings up and balances autonomously.
Score it with the honest metrics
Section titled “Score it with the honest metrics”The same gate as every other deployment. Opening the port resets the Nano, so hang the pendulum still before launching:
cd policypython analyze_onboard.py --port /dev/cu.usbserial-1130 --duration-s 60 \ --log recordings/onboard_<run>.npzRead the header line first: it must say ~50.0 Hz, matching
CONTROL_FREQUENCY_HZ above. An off-rate capture (the script warns loudly)
means inference exceeded the tick budget and the numbers evaluate a broken
deployment, not the policy.
The script also reports the sample→command latency the sketch measured — mean, p95 and max — against the nominal the simulation assumes. That is the one number here describing the sim-to-real gap rather than the policy: if it sits outside the range the curriculum trained over, fix the model before blaming the controller. Detail in transport delay.
2026-07-22 reference: balanced fraction 0.892, 5 s streaks, verdict
BALANCED. The current champion scores 1.000 over five minutes — a 299.6 s
unbroken hold with zero drops, |action| 0.239 and pendulum σ 1.90°.
Boot also prints [boot] policy(hanging/upright) = … on serial (500 kbaud) —
compare against the PyTorch student on the same reference frames if you
suspect an export/PROGMEM bug.