Ten Games, One LED Panel, and a Radio That Would Not Share

I bought a 16x16 LED matrix for no reason, left it in a drawer for a year, then turned it into a ten-game arcade driven by a controller Google abandoned. Here is how it came together, including the bug that cost me an evening.

ESP32-powered 16x16 WS2812B LED matrix in a black frame playing Breakout, with a Stadia controller in front

Every project worth doing starts with a part you bought for no particular reason. Mine was a 16x16 WS2812B LED matrix: 256 individually addressable pixels of pure "I will definitely find a use for this," which then spent the better part of a year in a drawer doing an excellent impression of a coaster.

This post is the use.

What it actually is

It is a tiny arcade. An ESP32-C6 drives the panel, a Google Stadia controller talks to it over Bluetooth, and there are ten games loaded: Snake, Tetris, Breakout, Pong, Space Invaders, Flappy Bird, Simon Says, Frogger, Conway's Game of Life, and a River Raid clone I am unreasonably proud of. You scroll a menu on the panel with the d-pad, you pick a game, you play. That is the entire interface, and it turns out that is exactly the right amount of interface.

The game names take some getting used to. A 16 by 16 grid does not leave much room for text, so the menu has strong opinions about abbreviation: Breakout shows up as BRKOUT, Frogger as FROGR, and Flappy Bird as a curt FLAP. You learn to read them the way you learn to read a doctor's handwriting. It was that or nothing, and nothing is not much of a menu.

I put the whole thing in a black shadowbox frame so it reads as "intentional object on the side table" rather than "science fair project that wandered off." It mostly works. This is not my first WS2812B panel. The last one scrolled Meshtastic messages, but that one you could only read, not play.

Breakout running on the 16x16 WS2812B LED matrix panel
Breakout, mid-rally. The paddle is a few pixels of false confidence and the ball does not care about your feelings.

The controller is a small act of spite

The gamepad is a Google Stadia controller. You may remember Stadia as the game streaming service Google launched, swore it was deeply committed to, and then shut down roughly fifteen minutes later, give or take. The controllers were left behind as expensive paperweights with a USB-C port.

Except not quite. There is a one-time firmware switch that turns them into standard Bluetooth gamepads, and a library called Bluepad32 that speaks to them from an ESP32. So the controller Google abandoned now drives an LED panel in my dining room. I find this satisfying in a way I am choosing not to examine too closely. I have written before about hardware you buy but do not fully own, and this is the happier version of that story.

Full disclosure: I was not an innocent bystander here. I bought a Stadia, used it exactly as intended, and was genuinely disappointed when Google pulled the plug. Then Google did the one thing that makes it hard to stay mad: they refunded everything, every controller and every game I had bought, right down to the dollar. It is tough to hold a grudge against a company that hands your money back, so I mostly did not. I just quietly kept the hardware.

I did not hold onto it out of sentiment, though. The day Google flipped the switch that let you convert the controller to Bluetooth, I jumped on it, because I was already planning a Batocera build and wanted a good gamepad that would not cost me a thing. (That build is a post for another day. Consider this your hint.)

The Stadia pad then put in two solid years of service on Batocera before I decided to stretch its usefulness a little further. This panel is what “a little further” turned out to mean.

The part where the hardware fought back

Nothing here worked on the first try. Or the second. A representative sample of the indignities:

  • The boot loop. The whole thing crashed and rebooted the instant a controller connected, over and over. The main task stack was about a paperclip wide, and the Bluetooth callbacks needed considerably more room than a paperclip.
  • The mirror world. Every game rendered backwards. Text scrolled in from the wrong side and read like it was meant for someone standing behind the panel. One flipped coordinate later, reality was restored.
  • BT WAIT. The panel would sit forever waiting to connect to a controller that it had, in fact, already connected to. It was holding hands with the gamepad and still filing a missing person report.

These are the ordinary taxes of embedded work. You pay them, you write them down so you never pay them twice, you move on. The next one was more interesting.

The radio that would not share

I wanted to update the firmware without dragging the panel back to my desk and plugging it in like an animal. So I added an over the air updater: the panel hosts a small web page, you upload a new firmware file from your phone, and it flashes itself and reboots. If the new firmware is broken, the bootloader quietly rolls back to the version that worked. You cannot brick it from the web page, which was the whole point.

I themed the web page after this blog, because of course I did.

And then it did not work. The panel would bring up its WiFi network, the network would appear in the list on my phone, and the phone would flatly refuse to connect. It could see the party. It could not get through the door.

Here is the catch with the ESP32-C6: it has exactly one radio, and WiFi and Bluetooth have to take turns using it. The Bluetooth side was still scanning for controllers, full time, and that scanning was hogging the radio at the precise moment WiFi needed it to finish the handshake that lets a phone join. The network could shout its name across the room all day, but it could not hold a single conversation.

The fix is one line that amounts to "stop looking for controllers while the web server is up." Obvious in hindsight. Most things are, right after they cost you an evening.

River Raid, because the screen was right there

Sixteen pixels across is not a lot of river. The banks meander, the fuel depots glow yellow and you genuinely do not want to miss them, and your jet is, if we are being honest, two pixels having a good day. It should not work as well as it does.

River Raid never quite holds still for a clean photo, so for now you will have to take my word for it, or build one and watch it yourself. Here are two of its better-behaved neighbors instead:

Pong on the 16x16 LED matrix, showing two paddles, a center net, and the ball
Pong, which on a 16 by 16 grid is right at home: two paddles, a center net, a ball, and not one pixel of anything else.

And here is Tetris, in motion:

0:00
/0:09

Tetris on the panel, filmed on a phone at the dining room table, which is the correct venue for this.

A confession about how it got built

If you look closely at the photos, the laptop in the background is running Claude Code and a GitHub page. I am not going to pretend otherwise: a good chunk of this was built by talking to an AI agent and arguing with it about stack sizes at length. It is still my project, my soldering, and my deeply questionable idea about a coaster, but the boilerplate and the three-hour bug hunts went a lot faster with a tireless pair programmer that never once judged me for the number of terminal windows I had open.

Take it and build your own

The whole thing is open source: github.com/zero7608/matrixgame. Ten games, the over the air updater, the wiring notes, and a README that documents every trap I stepped in so that you do not have to. Bring your own LED panel and a controller nobody else wanted anymore.

And that, at long last, is what the part in the drawer was for.