An effects server for a fake traffic light built from 3× Adafruit 3649 (64×64 RGB LED) panels, daisy-chained and driven by a Raspberry Pi Zero via an Adafruit RGB Matrix Bonnet/HAT.
The panels are a vertical stack — top = red, middle = yellow, bottom = green — exposed to effects as a single 64×192 canvas of three 64×64 tiles.
A Flask server runs effects in a background render loop and serves a phone-friendly web UI + REST API to switch effects, tweak settings, and auto-rotate. The same code runs on your laptop using a Pillow emulator (live PNG preview in the browser) so you can develop without the hardware.
┌── RED ───┐ tile 0 y[0:64]
│ 64×64 │
├── YELLOW ┤ tile 1 y[64:128]
│ 64×64 │
├── GREEN ─┤ tile 2 y[128:192]
│ 64×64 │
└──────────┘
Local dev uses uv — it creates an isolated
.venv and never touches your global Python.
make deps # uv sync → .venv with Flask + Pillow
make run-sim # emulator backend
# open http://localhost:8080 — live preview + controlsmake smoke renders one frame of every effect to catch errors fast.
The light can be driven in either of two modes (toggle in the UI, or
POST /api/mode):
- Whole light — one effect spans the full 64×192 stack (traffic sequence, fish swimming across all three, a tall marquee, …).
- Per lamp — each 64×64 screen runs its own effect independently. Set
the top lamp solid red, the middle to scrolling text, the bottom to fish —
every screen is individually controllable. (This is also how you get
different scrolling text on each panel: just pick
texton each lamp.)
| Effect | Scope | What it does | Key settings |
|---|---|---|---|
| traffic | whole | Solid red/yellow/green, or a timed sequence |
state, red_s/green_s/yellow_s |
| solid | both | Solid colour / lamp circle, optional pulse | color, style, pulse |
| off | both | Blank (black) | – |
| text | both | Horizontal scrolling marquee | text, color, bg, speed, font_size |
| fish | both | Fish swimming with bubbles | count, bubbles, seed |
| image | both | Static image or animated GIF from assets/ |
file, fit, target, fps |
scope: whole = full-light only, both = usable per-lamp or full.
Add a new effect by dropping a module in app/effects/,
decorating the class with @register, and setting scope (see
solid.py).
| Method | Path | Body | Purpose |
|---|---|---|---|
| GET | /api/effects |
– | List effects + scope + param schemas |
| GET | /api/assets |
– | List files in assets/ |
| GET | /api/state |
– | Mode, fps, backend, unified + per-panel state |
| POST | /api/mode |
{"mode":"unified"|"panels"} |
Switch control mode |
| POST | /api/effect |
{"name","params":{}} |
Set the whole-light effect |
| POST | /api/params |
{"params":{}} |
Tweak the whole-light effect |
| POST | /api/panel |
{"index","name","params":{}} |
Set one lamp's effect (index 0=top) |
| POST | /api/panel-params |
{"index","params":{}} |
Tweak one lamp's effect |
| POST | /api/rotate |
{"enabled","interval","order":[]} |
Auto-rotate (whole-light) |
| GET | /preview.png?scale=N |
– | Latest frame as PNG (real & emulator) |
# whole light: classic green
curl -X POST localhost:8080/api/effect -H 'Content-Type: application/json' \
-d '{"name":"traffic","params":{"state":"green"}}'
# per lamp: top=red, middle=scrolling text, bottom=fish
curl -X POST localhost:8080/api/panel -d '{"index":0,"name":"solid","params":{"color":"#ff3014"}}' -H 'Content-Type: application/json'
curl -X POST localhost:8080/api/panel -d '{"index":1,"name":"text","params":{"text":"WALK"}}' -H 'Content-Type: application/json'
curl -X POST localhost:8080/api/panel -d '{"index":2,"name":"fish"}' -H 'Content-Type: application/json'- 3× Adafruit 3649 — 64×64, 2.5mm pitch, 1/32 scan (5-address, needs the E line).
- Adafruit RGB Matrix Bonnet (recommended) — handles the E line + 3.3→5V level shifting. Or the RGB Matrix HAT with the 64×64 solder jumper bridged.
- Flicker fix: solder GPIO 4 ↔ 18 and keep
MATRIX_GPIO_MAPPING=adafruit-hat-pwm. - Power: each panel pulls up to ~4 A @ 5 V → get a 5 V / 15 A+ supply. Power the panels from it directly (not through the Pi); common the grounds.
- Wiring: daisy chain OUT→IN through all three panels; Bonnet exposes one
chain (
MATRIX_CHAIN=3).
git clone https://github.com/hzeller/rpi-rgb-led-matrix.git && cd rpi-rgb-led-matrix
make -C examples-api-use
sudo examples-api-use/demo -D0 --led-rows=64 --led-cols=64 --led-chain=3 \
--led-gpio-mapping=adafruit-hat-pwm --led-slowdown-gpio=2 --led-brightness=50All three panels lighting as one field = chain good. (Ansible builds the lib for you; this is just the manual smoke test.)
The Pi must be reachable over WiFi by hostname/IP with SSH key auth.
- Edit ansible/inventory.ini — set the hostname/user.
- Tune hardware vars in ansible/group_vars/all.yml.
- Deploy:
make ping # confirm SSH/Ansible reach the Pi
make flash # full provision + deploy (make flash PI=192.168.1.42)
make deploy # re-push app code + restart only
make logs # tail the service journalmake flash runs three roles:
- system — apt packages; blacklists
snd_bcm2835(its PWM conflicts with the matrix — required by hzeller) and reboots if needed. - matrix_lib — clones & builds hzeller's python bindings into system Python.
- app — copies the code to
/opt/traffic-light, installs a systemd service (traffic-light, runs as root for GPIO), enables and starts it.
The Zero is single-core ARMv6 — the weakest Pi for this. If you see issues,
adjust in group_vars/all.yml and make deploy:
- Flicker → raise
matrix_slowdown_gpio(try 3–4), or lowermatrix_pwm_bits. - Ghosting / wrong colors → lower
matrix_slowdown_gpio. - Layout wrong (panels scrambled / sideways) → adjust
matrix_pixel_mapper(e.g."V-mapper:Z", or addRotate:90) to match your physical cabling.
app/
config.py env-driven config (canvas = 64×192)
display.py RealMatrix (rgbmatrix) + EmulatorMatrix (Pillow) backends
controller.py render loop, whole-light + per-panel modes, compositing
server.py Flask: REST API + web UI + /preview.png
effects/ traffic, solid, off, text, fish, image (+ base/registry)
web/ index.html + app.js control deck (signal-cabinet UI)
ansible/ inventory, group_vars, playbook, roles (system/matrix_lib/app)
assets/ your images & GIFs
scripts/smoke.py render-every-effect sanity check
Makefile run-sim / smoke / flash / deploy / logs / ...