Roomba 40k

July 27, 2026

A black Roomba with a Raspberry Pi and a camera bolted on top, wiring exposed, on a tiled floor

A Create2-compatible iRobot driven from a browser in real time, with a Raspberry Pi 3B+ riding on top and talking to the robot over a USB-TTL serial adapter. A phone or a desktop opens the page and drives it, with live video from the CSI camera.

The decision the project rests on is architectural: a single control thread owns the serial port, and the web layer never touches it. HTTP handlers write desired state into a shared object under a lock, and the control loop drains that at roughly 50 Hz. Serial is blocking, asyncio is not, and mixing them is how you earn intermittent freezes nobody can reproduce. Every new hardware action, songs and wake included, goes through the same path.

It is meant to feel like an RC car rather than a command queue: the latest command wins and nothing is queued. A 300 ms heartbeat means losing Wi-Fi or closing the tab stops the robot instead of letting it continue on its last order. One client drives at a time; the rest join as spectators and can claim control.

The interface offers a D-pad with a 50 to 500 mm/s speed slider, an analog joystick with arcade mixing, and a keyboard mode. The HUD shows the bump sensors and all four cliff sensors, pushed over WebSocket whenever they change.

Video was the hard part

The CSI camera runs through libcamera, encodes H.264 in hardware, and is served by MediaMTX over WebRTC on UDP. The policy is explicit: a current image beats a continuous one. Visible hitches are acceptable, buffered stale video is not, because driving on half a second of delay is driving in the past.

Two findings that only show up if you measure:

MediaMTX queues RTP fragments, not displayed frames. A warning about “discarding 35 frames” does not mean 35 camera frames were lost. With the default queue, latency grew; with too small a queue, every keyframe overflowed it on its own, because the encoder emits an IDR as a burst of 38 to 47 packets. The right value is the smallest one that still fits an entire burst.

And the bug that looked like a software regression: the hardware encoder failed with ioctl(VIDIOC_QBUF) failed because the system default reserved 76 MB of GPU memory. Below 128 MB, hardware H.264 will not start. No code had changed.

Stack: Python, aiohttp, WebSocket, pycreate2, pyserial, MediaMTX, WebRTC, Raspberry Pi 3B+.

GitHub Repository

Technologies: Python, aiohttp, WebSocket, pycreate2, pyserial, MediaMTX, WebRTC, Raspberry Pi

← Back to all projects