Arrivals is a Kotlin Multiplatform project for real-time public transit info. The most recent exploration was a Raspberry Pi driving an LCD. Now I’m going for character: a 128x32 dot-matrix board built from two chained LED panels, which feels like a miniature of the real thing.

LED matrix displaying public transit arrivals

If you’re interested in building this, the project README is comprehensive, so this post will be a quick run through the approach and gotchas.

Kotlin/Python bridge

Python is the standard language for talking to hardware via the Raspberry Pi’s GPIO headers. We need a way to connect the Kotlin app to Python LED display drivers.

Arrivals CLI output (human readable version)

Luckily I’d already built a CLI target, so the approach was:

  1. Add a --json output flag to the CLI
  2. Build Kotlin/Native CLI artifacts to run with no JVM overhead on the Raspberry Pi Zero 2’s 512MB of RAM
  3. Call the CLI from a Python rendering script and parse the JSON results

The Kotlin Multiplatform compiler doesn’t run on the Raspberry Pi’s linuxArm64 architecture, but the CLI binary can be cross-compiled from macOS or Linux x86 via the :cli:linkReleaseExecutableLinuxArm64 Gradle task.

Bitmap fonts

I got something working pretty quickly using a font bundled with the LED display samples. It looked… extremely disappointing. The next step was to reproduce the classic TfL font.

Baker Street rendered in the 9px TfL font reproduction

The LED matrix is a tiny 128x32 resolution, so we need a bitmap font where every pixel is fully on or off with no anti-aliasing. I used Claude Code to transcode a scalable TrueType reproduction of the London Underground dot matrix font into BDF, a 38-year-old font format. The resulting bitmap font has a height of 9px, which means we can just about squeeze in 3 rows.

Assembly

The build itself is very simple:

The challenge was connecting the two LED units to form a single, seamless panel. I couldn’t find technical drawings for the panels, but I was able to use caliper measurements to 3D-print a bracket. The model is included in the project, along with a couple of risers to attach different Raspberry Pi boards to the back.

Raspberry Pi 5 wired to the back of the LED matrix panel

Flicker

Once everything was up and running, I noticed a subtle flicker whenever data was fetched, due to the Linux scheduler preempting the LED refresh thread. The fix was to reserve a core: adding isolcpus=3 to /boot/firmware/cmdline.txt keeps core 3 off-limits so the LED driver runs there exclusively. On the Pi Zero 2, the rgbmatrix library picks this up automatically; on the Pi 5, I’m running a patched Piomatter branch that pins the blit thread to the isolated core.

Conclusion

This was a great weekend project, and a bit of a rollercoaster: getting the CLI bridge working, almost writing the project off over the font, chasing down the flicker, and finally seeing the finished result. 🤩