Overview
The ProtoBot is a small self-balancing robot kit. Out of the box it holds balance for a second or two. After a 2-day hacking session it holds 60+ seconds, both ways up. The approach: telemetry first, classical control up to its physical limit, then a small policy trained in simulation and distilled back into a linear formula.
The loop
- Release the robot, auto-archive the full telemetry of every run, let the AI read the traces and adjust the firmware, flash, release again. Dozens of cycles a day.
- Training ran on the same machine: PPO in MuJoCo, CPU only, about 30 minutes per policy generation.
- Builds with PlatformIO, flashed over USB in about 20 seconds.
Telemetry first
You can't debug a robot you can't see.
- 50 Hz telemetry over BLE into a Web Bluetooth dashboard: live charts, a pose view, about 20 live-tunable parameters.
- Every run archived with its exact settings, which kept the tuning honest.
- One day was lost to a BLE dongle that shipped with its antenna unscrewed.
The classical ceiling
Careful tuning alone cannot fix the stock controller. The loop physics get in the way.
- The sensor-to-motor delay is 20-25 ms, measured, and it caps every gain: past kp of about 8 the loop rings at 5 Hz no matter what.
- The motors do not move below a breakaway duty of about 48. Pulse-density modulation, full-strength 10 ms pulses at varying density, keeps average torque linear down to zero.
- There is no encoder, so wheel speed is modeled from the command history (first-order lag, fitted tau of 0.3 s). Without this model any small bias accelerates the robot to its 26 cm/s ceiling and it falls within a second.
- The true balance point sits about 0.55 degrees off vertical from mass asymmetry, found by running two setpoints and interpolating the zero-drift point.
- Classical control with all of the above: 15-18 second balance sessions.
The robot measures itself
Every guessed constant turned out wrong, so the firmware grew a 25-second self-test bench: breakaway staircases, loaded step trials, battery sag, and an open-loop fall test.
| Quantity | Measured | Note |
|---|---|---|
| Sensor-to-motor delay | 20-25 ms | the binding constraint on gains |
| Loaded breakaway duty | 48 ± 4 cold | wanders 36-56 warm, asymmetric |
| Command-to-motion lag | 15-30 ms | electrical rise + stiction |
| Thermal derating | -20-25% torque | warm vs cold, two independent methods |
| Open-loop fall pole | ω ≈ 7-17 rad/s | warm tires fall slower; rigid theory says 15.5 |
| Balance point offset | ≈ -0.55° | per-unit mass asymmetry |
| Top speed | 0.26-0.28 m/s | a 2° standing error falls in ~0.75 s |
A 1.2 KB policy
- A 7-32-32-1 network trained with PPO, int8-quantized: 2.7 ms per inference on the FPU-less C6.
- It balances 60+ seconds hands-free, recovers from driving over a cable, and works upside-down, a pose it saw in only 20% of training.
- Two failed generations taught the honesty lesson: one trained on motors 3x too weak learned bang-bang violence and died in 2 seconds; one trained on rigid tires learned over-stiff reactions and wobbled at 4 Hz.
- The fix that stuck is a calibration gate: training refuses to start unless the sim reproduces the measured torque curve and fall speed.
What the linear model does
A plain linear regression over the policy's good runs explains R² = 0.99 of its behavior near upright. The readout doubles as a review of the hand tuning:
| Term | Network policy | Hand-tuned PD |
|---|---|---|
| Error gain (duty/°) | 5.3 | 6 - rediscovered |
| Rate damping | 0.42 | 0.24 - hand tuning was 2x under-damped |
| Command memory | +0.35 · u(k-1) | missing - its own delay compensation |
| Wheel-speed feed | pushes with motion | opposed it |
- Refitted without the accelerometer term (R² drops only 0.992 to 0.987), the law fits in about 20 stock-shaped lines of firmware.
- Configuration ladder: stock holds 1-2 seconds, the snippet alone holds tens of seconds, and pulses plus the measured setpoint (or the neural policy) hold 60+.
- [TODO: status of suggesting the snippet upstream to microbots]