A full-screen bus arrival display that runs on a Raspberry Pi 2, showing the next buses due at a nearby stop using the TFL Countdown API, along with the current weather and time.
- Linux (Pi): renders to the display via DRM/KMS if available, falling back to
/dev/fb0— no X11, Wayland, or GPU driver required. - macOS (dev): serves a live preview at
http://localhost:8080, rendering to PNG on each request.
The display shows the stop name and direction at the top, up to three upcoming buses (route number + minutes until arrival), weather conditions at the bottom left, and a clock at the bottom right.
If a second bus stop is configured, tapping the Raspberry Pi touchscreen toggles between the two stops.
On Linux the program tries /dev/dri/card0, card1, card2 in order and uses the first that supports modesetting. On Pi 4/5 with the KMS driver, card0 is a render-only V3D node; the display controller is typically card1. It finds the first connected connector, picks its preferred mode, allocates a 32 bpp dumb buffer in XRGB8888 format, and sets the CRTC directly. The pixel conversion from the internal RGBA image is a simple R/B channel swap — no dithering or bit-shifting — which is significantly faster than the fbdev RGB565 path.
This requires the process to be DRM master (i.e. running on the console with no display server). On a Pi running Pi OS, enable the KMS driver by adding dtoverlay=vc4-kms-v3d to /boot/config.txt.
If DRM setup fails for any reason (wrong permissions, no device, display server already active), the program logs a message and falls back to the fbdev path automatically.
Portability notes:
- Works on Pi 2/3/4/5 with
vc4-kms-v3doverlay enabled. - Falls back silently on desktop Linux where a display server holds DRM master.
- On a Pi 4 with both HDMI ports connected, HDMI0 is always used (first connected connector).
- The encoder must already be bound to the connector at startup; if the Pi booted headless with no display attached, fbdev fallback is used instead.
If DRM is unavailable, the program writes directly to /dev/fb0. It supports both 16 bpp (RGB565 with Bayer 4×4 ordered dithering to reduce colour banding on anti-aliased text) and 32 bpp (XRGB8888).
CPU profiling showed fbDevice.blit consuming ~15% of total CPU. The blit was optimised in three stages:
Stage 1 — fbdev inner loop (fbdev path)
- Hoist the bpp switch outside the x loop — bpp is constant for the device lifetime, so each colour depth now has its own tight loop.
- Replace per-pixel multiplies with stepping offsets —
srcOffanddstOffadvance by addition rather than recomputingx*4anddstX*bytesPerPixeleach iteration. - Cache the Bayer dither row —
bayer4x4[y&3]is constant across the entire x loop for a given row.
Stage 2 — DRM/KMS path
The DRM path avoids RGB565 dithering entirely, replacing it with a plain R/B channel swap (one uint32 load and store per pixel).
Stage 3 — eliminate bounds checks (DRM path)
binary.LittleEndian.Uint32/PutUint32 performs a bounds check on every pixel (384,000 per frame) that the compiler cannot eliminate when using stepped byte offsets. Replacing them with unsafe.Slice []uint32 row views lets the compiler prove the bounds statically, removing all per-pixel checks. The rotate branch was also hoisted outside both loops.
| Path | Time/frame | vs original |
|---|---|---|
| fbdev 16bpp (original) | 53.2 ms | baseline |
| fbdev 16bpp (stage 1) | 39.6 ms | −26% |
| fbdev 32bpp (stage 1) | 34.6 ms | −35% |
| DRM XRGB8888 (stage 2) | 28.7 ms | −46% |
| DRM XRGB8888 (stage 3) | 8.6 ms | −84% |
go build
No CGO, no system dependencies beyond a standard Go toolchain. The DRM/KMS and fbdev paths use raw Linux ioctls via the Go syscall package.
./bus [flags]
-stop int TFL bus stop code (default 74640)
-stop2 int secondary bus stop code; touch screen toggles between the two
-touch str touch input device path (auto-detected if empty)
-debounce dur minimum interval between touch-triggered stop switches (default 100ms)
-rotate rotate display 180 degrees (default true)
-weather-key str weatherapi.com API key
Weather is fetched from weatherapi.com. Sign up for a free API key and pass it via -weather-key. The location is derived automatically from the GPS coordinates returned by the TFL API for the configured bus stop.
Originally written in Python a decade ago, it stopped working when TFL dropped support for old TLS ciphers and Python 2.7 was no longer maintained on the Pi. It was rewritten in Go, and later migrated from Fyne (which required CGO and a GPU driver) to a pure-Go framebuffer renderer. The old Python code lives in the pybus directory.