What is this?
A 64x64 RGB LED matrix driven by a Raspberry Pi 4B that displays the album cover of the currently playing Spotify song. A web app on the Pi enables control via the browser — from anywhere, thanks to VPN routing and OIDC authentication.
In addition to the Spotify mode, 13 different C++ demos are available (Game of Life, 3D cube, color evolution, and more), which can be launched directly from the dashboard.
The Hardware
The panel consists of three components: a Raspberry Pi 4B, an Adafruit RGB Matrix Bonnet, and a 64x64 HUB75 LED panel with 4096 RGB pixels.
The bonnet sits directly on the Pi’s GPIO pins and translates the GPIO signals into the HUB75 protocol. The driving is handled by the C++ library rpi-rgb-led-matrix, which operates the panel at a high refresh rate with minimal latency.
Soldering Work for 64x64
The bonnet does not work out of the box with a 64x64 panel. This is because such panels use a 1/32 scan and require a fifth address line (“E”) to address all 64 rows. Smaller panels like 32x32 or 64x32 get by with lines A through D (1/16 scan). The bonnet has a solder pad for this that connects GPIO 8 to the E pin — this jumper must be manually bridged with solder. Without this bridge, the panel can only address half of its rows, resulting in a distorted image.
I have practically no knowledge of electrical engineering — which is exactly why I chose this project. Always learning something new. I only discovered that the solder jumper is needed for 64x64 panels through numerous tutorials and forum posts. I would not have figured it out on my own. Since I do not own a soldering iron, my friend Sven kindly took care of it — thanks for that!
The challenge with HUB75 : the protocol multiplexes the 64 rows — at any given moment, only a few rows are active. The library must cycle through the rows fast enough for the human eye to perceive a stable image. On the Pi, this works reliably as long as no other process is blocking the CPU cores.
Spotify Integration
The most interesting part: the panel displays the album cover of the song currently playing on Spotify.
The OAuth Flow
Spotify does not offer simple API keys. Instead, the API uses OAuth 2.0 with the Authorization Code Flow:
- The user clicks “Connect with Spotify” and is redirected to Spotify
- After login, Spotify redirects back with an authorization code
- The backend exchanges the code for an access token and a refresh token
- The tokens are persisted in SQLite
From this point on, no further login is required. The access token expires after one hour but is automatically renewed via the refresh token. The Spotify app runs in development mode — that is sufficient for a single account.
From API Call to LED Pixel
When a new song starts, the following happens:
- The backend polls Spotify’s
/v1/me/player/currently-playingevery 5 seconds - On track change, the smallest available cover image is downloaded (64x64 JPEG)
led-image-viewerfrom the rpi-rgb-led-matrix library displays the image on the panel
The last step uses child_process.spawn to start the image viewer as a root process — GPIO access requires root privileges. A process manager ensures that only one process controls the matrix at a time: before a new cover is displayed, the previous process is cleanly terminated.
Auto Mode
In auto mode, the backend compares the current track with the last displayed one on every poll. A new cover is only loaded and displayed on an actual track change. This saves API calls and avoids unnecessary process restarts.
Auto mode runs server-side — so the cover changes even with the browser closed, as long as Spotify is playing on any device.
The Path to the Pi: Network Routing
The Pi is on the local network and is only reachable via WireGuard VPN. The web app, however, needs to be publicly accessible (with login). The solution is a three-stage routing chain:
Browser → Nginx (Hetzner server, SSL) → socat (VPN server) → Pi (172.30.0.4:3005)
The problem: the Hetzner server and the Pi are on different networks. The Hetzner server does not know the VPN subnet. A socat port forward on the VPN server bridges this — it listens on the Hetzner private network and forwards traffic through the WireGuard tunnel to the Pi.
Process Manager: One Process, One Matrix
The central hardware constraint: only one process can control the GPIO pins at a time. The process manager encapsulates this logic:
- Start — Checks if a process is already running, terminates it if so, then starts the new one
- Stop — Sends SIGTERM, waits up to 5 seconds, then SIGKILL
- Status — Returns the current mode (
demoorimage), the PID, and the uptime
Every call — whether starting a demo, displaying a cover, or stopping — goes through the process manager. This prevents zombie processes and ensures the matrix is always in a defined state.
Deployment: GitHub Actions → SSH Jump → Pi
The deployment is unusual because the target is not a cloud server but a Pi behind a VPN. The GitHub Action uses an SSH jump via the VPN server:
GitHub Actions → SSH → VPN Server (Jump Host) → SSH → Pi (172.30.0.4)
On the Pi, the application is built and restarted — no Docker, directly Node.js with a systemd service. This saves the build time for Docker images, which would take significantly longer on a Pi.
What I Learned
- GPIO access requires root — and therefore a clean process manager that tracks and cleans up processes. Orphaned root processes on a Pi without a monitor are hard to debug.
- Spotify has no API keys — only OAuth 2.0. For a personal use case, it is overhead in setup, but the refresh token makes it maintenance-free afterward.
- 64x64 pixels are surprisingly expressive — album covers are designed for small resolutions and look remarkably good even on an LED matrix.
- Multi-hop SSH deployments work reliably with GitHub Actions but require proper key management and a stable VPN tunnel as a foundation.