Every summer I help run a week of AI dětem camp, and one of the reliable joys is sumo-fighting the little Otto robots against the kids. My unfair advantage was always driving mine from a laptop keyboard instead of poking a phone screen.
Then a new version of the remote quietly removed the key bindings. Rude.
So I wrote them back. The result lives at /otto — install a userscript, calibrate once, and you’re driving with W A S D (or arrows), space to stop.
The part where I went down a rabbit hole
My first instinct was the obvious one: put the remote in an <iframe> on my own page and forward key presses into it. That doesn’t work, for two independent reasons that are worth knowing:
- Same-origin policy. My page and
app.hprobots.comare different origins, so my JavaScript can’t read or dispatch events into that iframe at all. This is the browser rule that stopsevil.comfrom framing your bank and clicking “transfer” — no header flips it off. - There are no buttons. The remote is a Flutter app rendered to a
<canvas>. The D-pad is painted pixels, not DOM elements. There’s nothing toquerySelector, nothing to attach a handler to.
The way through both walls is a userscript that runs as the remote’s own page. Same origin, so no iframe barrier — and instead of targeting elements, it synthesizes a real pointer “press” at a screen position. Since Flutter can’t tell a synthetic press from a finger, the button does whatever it normally does. You just have to tell it where the buttons are, which is the one-time calibration click.
It’s about 200 lines, one file, no dependencies. Setup and keymap are on the /otto page. See you in the ring.