Skip to content
led-ticker

Hardware: Building your own

led-ticker ships two reference builds: the smallsign (Pi 4 + 5× 32×16 panels) and the bigsign (Pi 5 + 8× P3 32×64 panels). They’re starting points, not requirements — anything in between (or beyond) is a viable build, and the engine is the same.

The two reference builds sit at very different points on the cost / complexity / use-case curve. Use them as starting points and adjust panel count, panel pitch, or layout to fit your space.

Reference buildCost & complexityUse case
Smallsign — 5x 32x16, 160x16Lowest. One Pi 4, five panels, a bonnet, a 10A 5V supply. One evening of soldering and a weekend of config work.A scrolling marquee for a desk, a shop window, or a single-line live feed (RSS, weather, countdowns). 16 rows is one BDF text line tall.
Bigsign — 8x P3 32x64, 256x64High. Pi 5, eight P3 panels, a HAT, a 30A+ supply, a rigid frame, a 2x4 serpentine wiring run, hi-res font tuning.Storefront-scale display. Fits hi-res emoji, two-row handles + promos, animated GIFs, and Inter / Beloved Sans rendered at native pixels.

Anything between those two — three panels in a row, six panels in a 2x3 grid, a custom non-rectangular layout — is fine; the rgbmatrix library and the led-ticker engine don’t assume the reference shapes. You’ll edit the [display] block (rows, cols, chain_length, pixel_mapper_config) to match your wiring. For pixel_mapper_config specifically, don’t hand-write the Remap string — use panel-map to derive it from your physical layout.

You don’t need any of this hardware to start. The render-demo tool runs the real ticker engine against a test stub canvas and writes the output to a gif, so you can develop your config end-to-end with nothing but a laptop.

RGB matrix panels draw current proportional to the number of LEDs lit at any moment. The rule-of-thumb upper bound is:

amps_max = rows × cols × number_of_panels × 0.06A (per LED, full white)

That’s the theoretical maximum at 5V with every pixel cranked to white at the same instant. In practice that never happens — text, emoji, and animations only light a fraction of the panel — so the practical draw is roughly 30-40% of the theoretical max.

Reference buildTheoretical maxPractical pick
Smallsign5V × ~150A5V × 10A
Bigsign5V × ~980A5V × 60A

A 5V / 10A brick covers the smallsign with comfortable headroom; the bigsign wants a 5V / 30-60A supply with thick busbars (split across multiple injection points if your panels have multiple power inputs). Adafruit’s product pages publish per-panel current draw curves — cross-reference those for whichever panels you actually buy, since brightness, panel revision, and pixel pitch all shift the number.

The Pi itself runs off its own USB-C supply; do not try to power panels off the Pi’s 5V rail. The math doesn’t work for one panel either, let alone five or eight.

You can develop a complete led-ticker config without owning a single panel. Two paths — pick whichever fits your workflow:

Browser preview (Docker, no Python needed) — the fastest way to see the sign rendering:

Terminal window
make try # build + start; open http://localhost:8080 → live preview tab

make try starts a headless engine and the web UI in Docker. Stop with Ctrl-C then make try-down.

GIF renderer (Python dev path) — render any config to a GIF at panel resolution, useful for CI, scripted demos, and configs with live-data widgets:

Terminal window
git clone https://github.com/JamesAwesome/led-ticker.git
cd led-ticker
make dev # install Python dev deps (requires uv)

Run the test suite — no hardware, no Docker:

Terminal window
make test

make test runs the full suite — pyproject’s [tool.pytest.ini_options] pythonpath = ["tests/stubs"] puts the stub canvas on the path automatically, so no env var is needed. ~1450 tests, finishes in ~2 minutes.

Render any config to a gif at panel resolution:

Terminal window
make render-demo CONFIG=mysign.toml OUT=mysign.gif

A minimal config you can render right now:

[display]
rows = 16
cols = 32
chain_length = 5
default_scale = 1
brightness = 60
[[playlist.section]]
mode = "slideshow"
loop_count = 1
hold_time = 4.0
[[playlist.section.widget]]
type = "message"
text = "Hello, panel"
font_color = "rainbow"

The output gif is upscaled 4x by default so individual LED pixels read clearly. See render-demo for the full set of flags and the long-running pipeline used for data-fetch widgets.

Once your config renders the way you want, push it to a Pi. Deployment is Docker Compose — one command and the sign is running:

Terminal window
make setup # Docker preflight + config/.env seed + bring-up (first time)
make update # rebuild the image (real version) + recreate services (subsequent deploys)

make setup (or make setup MODE=deploy) checks that Docker is installed, seeds config/config.toml and .env if they don’t exist yet, and runs docker compose up -d. After that, make update rebuilds the image (with a real version baked in) and recreates the running services for every subsequent deploy.

Key behaviors built into compose.yaml:

  • restart: unless-stopped — the ticker comes back up on power loss or crash.
  • Config mounted read-only (./config:/code/config:ro) — a typo in a hot-edit can’t crash the ticker into a half-applied state.
  • Hot-reload is on by default: edit config/config.toml on the host and most changes (sections, widgets, schedule) take effect within one playlist cycle — no restart needed.

Panel bring-up — after wiring up the hardware and the first boot:

  1. Run make panel-test to cycle the panel through solid R/G/B/White/Black. If colors are missing or wrong, check your wiring and the led_rgb_sequence config knob before going further.
  2. Use the panel-map reveal → derive → verify workflow to build the pixel_mapper_config Remap string for your exact panel layout.

To take the sign down for a diagnostic, docker compose stop; bring it back with docker compose start.

Repo hygiene — your running config/config.toml is gitignored (it’s yours to customize per sign). Private media — images, GIFs, custom fonts — goes in config/local/ (also gitignored) and is referenced in TOML as path = "local/<file>".

The rgbmatrix library is hardcoded to a fork in the Dockerfile

Section titled “The rgbmatrix library is hardcoded to a fork in the Dockerfile”

If you deploy via Docker (the recommended path), you don’t need to do anything — the image already builds the correct fork.

For source builds: the Dockerfile pins jamesawesome/rpi-rgb-led-matrix — our fork of hzeller/rpi-rgb-led-matrix. It tracks upstream’s Pi 5 RP1 support (hzeller#1886) and adds three patches the engine depends on: a GCC-10 build fix, a Pillow shim, and a SubFill Python binding that upstream doesn’t ship. Because of that last one, stock upstream is not a drop-in replacement. If you apt install or pip install a different rgbmatrix on the Pi outside the Docker image — or check out upstream hzeller/master and make install it — your Pi will diverge from what the engine was tested against, and you’ll spend an afternoon chasing flicker that isn’t a config bug. Rebuild from source against the pinned fork.

USB-C “5V 3A” power supplies aren’t dedicated panel supplies

Section titled “USB-C “5V 3A” power supplies aren’t dedicated panel supplies”

A phone charger or a generic USB-C brick marketed as 5V 3A does not sustain rated current under inductive matrix load — the voltage sags, panels glitch, and you blame the config. Use a dedicated 5V supply sized per the power-budget table above (10A for the smallsign, 30-60A for the bigsign), with thick power leads to each panel’s 2-pin input.

P3 panels at brightness 60+ get warm to the touch within minutes; the smallsign is more forgiving but still climbs over time. For sustained or always-on installs add a fan behind the panels, or design a shroud with airflow gaps. Heat shortens LED lifespan and can warp panel substrates if it accumulates with no ventilation.

Running from a source checkout (dev / evaluation)

Section titled “Running from a source checkout (dev / evaluation)”

If you want to run led-ticker directly from a Python virtualenv rather than Docker — for development, custom patches, or experimentation — install the package and its deps with make dev (which runs uv sync --extra dev), then invoke led-ticker --config config/config.toml directly.

Inspecting the running async loop (Python 3.14+)

Section titled “Inspecting the running async loop (Python 3.14+)”

led-ticker is one long-lived asyncio loop plus a background task per data widget (run_monitor_loop) and the optional busy-light HTTP server. On Python 3.14 you can print the live task tree of a running process with no code changes or restart:

Terminal window
# pid of the led-ticker process (inside the container or in a dev checkout)
python -m asyncio pstree <pid>
# flat list with awaited-by edges
python -m asyncio ps <pid>

If a data widget’s update() task has died (the tell-tale: its periodic ”… updated: N stories” INFO log stopped), it will be absent from the tree — turning “is the poller still alive?” into a direct answer.