Skip to content
led-ticker

Hardware: Smallsign reference build

The smallsign is led-ticker’s reference build for short single-line displays: a Raspberry Pi 4 driving five chained 32x16 RGB matrix panels through Adafruit’s RGB matrix bonnet for a 160x16 canvas. It’s the target the project was originally written against, and the dimensions the smallsign-flavored examples in config/ assume. You don’t have to match it exactly — any HUB75 panel chain that fits a similar shape (Pi 4 with the adafruit-hat mapping, 16-tall canvas) will run the same configs — but the parts list and tuning notes below are what we built and tested against.

  • Raspberry Pi 4 Model B
  • 5x chained 32x16 RGB matrix panels = 160x16 logical canvas
  • ~20 fps (50ms per frame)
  • default_scale = 1 — drawing logic and panel pixels are 1:1
  • Same Docker image as the bigsign reference build; the rgbmatrix library detects the SoC at runtime
ComponentQuantityNotes
Raspberry Pi 4 Model B12 GB is plenty. A microSD card and a USB-C power supply for the Pi itself.
32x16 RGB matrix panel5HUB75 panels with the standard 16-pin IDC connector. Matched panels render most consistently.
Adafruit RGB matrix bonnet1Sets hardware_mapping = "adafruit-hat" in the config. Bonnet and HAT are Adafruit’s two names for boards that do the same job — plug onto the Pi’s GPIO header and drive HUB75 panels; the bonnet is the low-profile option.
5V power supply1Size for the chain — five 32x16 panels at full white draw several amps. A 5V / 10A brick is a comfortable headroom pick. Do NOT power the panels off the Pi’s USB rail.
16-pin IDC ribbon cables4Flat ribbon cable with HUB75 plugs — carries data panel-to-panel. One between each pair of panels. Most panels ship with one short cable; you may need extras to reach all four gaps.
Power pigtails / barrel jack1+Whatever your panels expect. Each panel has its own 2-pin power input.
Shroud / case (optional)1Acrylic diffuser + a 3D-printed frame is the typical finish. Not required to run.

Total cost (rough): ~$150–200 USD depending on panel sourcing and shipping. The Pi 4 is the largest single line item; the Adafruit bonnet or HAT runs $20–25; matched 32×16 panels run $15–25 each at retail (cheaper in lots from AliExpress). Cross-check current pricing before ordering — these numbers were verified in early 2026 and panel prices fluctuate.

The config tells the rgbmatrix library you’re using the bonnet/HAT via hardware_mapping = "adafruit-hat". If you swap to a bare wiring harness, that flag changes too — see the upstream hzeller/rpi-rgb-led-matrix README (the base for the jamesawesome/rpi-rgb-led-matrix fork led-ticker ships) for alternatives.

Before wiring, finish Adafruit’s own assembly steps for the bonnet — solder the GPIO header if yours shipped unsoldered (see Adafruit’s bonnet guide). That product prep is Adafruit’s territory; everything after it is covered here.

Chain the five panels left-to-right with the IDC ribbon cables. Chain order matters: the first panel in the chain (where the bonnet’s data output plugs in) is the LEFTMOST panel on screen, and the last panel in the chain is the RIGHTMOST. If your text shows up mirrored or split across the wrong panels, you’ve got the chain reversed.

Each panel has two HUB75 connectors, labeled IN and OUT on the silkscreen — data flows bonnet → panel 1 IN, then panel 1 OUT → panel 2 IN, and so on down the chain.

Each panel also needs its own 5V power lead — the data ribbon does not carry panel power. Most builds share one splitter or busbar from the 5V supply, with a short 2-pin lead to each panel’s power connector.

Seat the bonnet so its 40-pin socket covers all 40 pins of the Pi’s GPIO header — it only fits flush one way, with the bonnet’s board overhanging the Pi’s USB-port end when seated correctly. The bonnet’s HUB75 output plugs into the first panel’s data input. The chain_length field in the config (next section) tells led-ticker how many panels are linked.

The minimal [display] block for the smallsign — straight from config/config.example.toml:

[display]
rows = 16 # panel height
cols = 32 # panel width
chain_length = 5 # number of panels chained left-to-right
brightness = 60 # 0-100, not 0-255
gpio_slowdown = 2 # mandatory on Pi 4 — bump to 3+ if flicker
hardware_mapping = "adafruit-hat" # Adafruit bonnet or HAT

The 160x16 logical canvas is cols * chain_length wide by rows tall. Every widget on the smallsign draws to that canvas at native pixel resolution — no ScaledCanvas wrapper, no logical-to-physical block expansion.

Without it (or with it set to 0) the rgbmatrix library updates the GPIO faster than the Pi 4 can keep up, and you’ll see panels glitch, flicker, or display garbage. Start at 2 — that’s the example config’s default — and bump it to 3 or higher if you still see flicker. Higher values trade refresh rate for stability.

Do not set default_scale to anything other than 1 here

Section titled “Do not set default_scale to anything other than 1 here”

The smallsign is 16 logical rows tall and 16 physical rows tall — there is no logical-to-physical wrapping to do. The ScaledCanvas wrapper only kicks in when default_scale > 1 and assumes the panel is taller than 16 logical rows, so cranking it on the smallsign just clips your content.

Setting brightness = 255 doesn’t make the panels brighter — the rgbmatrix library reads it as “the maximum, plus a lot more” and behavior is undefined. Stay in 0-100; 60 is a typical indoor value.

Wiring or driver problem? Run panel-test first

Section titled “Wiring or driver problem? Run panel-test first”

If your widgets render with the wrong colors, only one panel lit, or visible flicker during scrolling, the issue is most likely in the hardware layer (led_rgb_sequence, chain_length, gpio_slowdown) — not in your config. The panel-test diagnostic isolates the hardware layer by painting flat R/G/B/W/B colors, so you can verify wiring before debugging widgets.

A complete working config for this build. Drop this into config/config.toml and adjust the per-widget content for your sign.

Complete config.example.toml (160×16 smallsign)
# led-ticker configuration — smallsign starter (160×16 canvas)
#
# 5× chained 32×16 panels = 160×16 logical canvas. Driven by a Raspberry Pi 4
# through an Adafruit RGB Matrix bonnet. No pixel_mapper_config needed — panels
# are a single horizontal chain_length, left-to-right.
#
# This starter runs entirely on BUILT-IN widgets — no plugins required, so it
# boots straight from `make setup`. Want live data (weather, RSS, crypto, MLB
# scores, pool temps)? Those are first-party plugins — see "Add live data with
# plugins" near the bottom and https://docs.ledticker.dev/plugins/ .
#
# Copy this to config.toml and adjust the playlist sections for your content.
# Full config-options reference: https://docs.ledticker.dev/reference/config-options/
[display]
rows = 16
cols = 32
chain_length = 5
brightness = 60
gpio_slowdown = 2 # mandatory on Pi 4 — bump to 3+ if you see flicker
hardware_mapping = "adafruit-hat"
# hot_reload = true # live-reload config.toml edits without a restart (default on; set false to disable)
# --- Optional: brightness schedule (uncomment + edit to use) ---
# Example: full brightness by day, a softer evening, and a dim overnight
# (10pm–7am, every day). Times are local wall-clock — set `timezone` or they
# follow the Pi's system clock (often UTC). Any hour not covered by a window
# uses the [display] `brightness` above.
# [display.schedule]
# enabled = true
# timezone = "America/New_York" # set this! defaults to the Pi's system time (often UTC)
#
# [[display.schedule.windows]]
# start = "07:00"
# end = "18:00"
# brightness = 100
#
# [[display.schedule.windows]]
# start = "18:00"
# end = "22:00"
# brightness = 40
#
# [[display.schedule.windows]] # dim overnight, 10pm → 7am, every day
# start = "22:00" # 10pm — wraps past midnight to 7am
# end = "07:00"
# brightness = 15 # dim, not off (set brightness = 0 to switch the panel off)
# # days omitted = every day; e.g. days = ["mon", "tue", "wed", "thu", "fri"] for weekdays only
[title]
delay = 5
# --- Transitions ---
# Global transition defaults. Full transition catalogue and per-family tuning
# lives at https://docs.ledticker.dev/transitions/. The values below give a
# 0.5-sec wipe between widgets within a section and a dissolve between sections
# — readable on a 160×16 smallsign without being distracting.
[transitions]
default = "wipe_alternating"
duration = 0.5
easing = "ease_out"
between_sections = "dissolve"
# --- Playlist ---
# Each [[playlist.section]] displays in order, then the whole playlist loops.
# Sections, modes, and timing knobs: https://docs.ledticker.dev/concepts/sections-and-modes/
# Welcome / brand messages.
[[playlist.section]]
mode = "ticker"
loop_count = 1
[playlist.section.title]
type = "message"
text = "#DevOps News"
font_color = "random"
[[playlist.section.widget]]
type = "message"
text = "May the uptime be with you!"
[[playlist.section.widget]]
type = "message"
text = "Always be shipping!"
# Counts down to any date. Message + remaining days scroll across the panel.
[[playlist.section]]
mode = "ticker"
loop_count = 2
[playlist.section.title]
type = "message"
text = "Count Downs"
font_color = "random"
[[playlist.section.widget]]
type = "countdown"
text = "Days Until Spring"
countdown_date = 2027-03-20 # ← change to your own date
[[playlist.section.widget]]
type = "countdown"
text = "Days Until Summer"
countdown_date = 2027-06-20
# Counts up from any past date. Widget disappears before countup_date arrives.
[[playlist.section.widget]]
type = "countup"
text = "Open for"
countup_date = 2024-01-01
# Local time — the built-in clock widget. `format` is "12h"/"24h" or a strftime
# template (e.g. "%I:%M %p %Z"); set `timezone` to an IANA name or it follows
# the Pi's system clock. Knobs: https://docs.ledticker.dev/widgets/clock/
[[playlist.section]]
mode = "slideshow"
hold_time = 5
loop_count = 1
[playlist.section.title]
type = "message"
text = "Local Time"
font_color = "random"
[[playlist.section.widget]]
type = "clock"
format = "12h"
timezone = "America/New_York"
font_color = "rainbow"
# A bundled sample animation — no plugin, no API key, and no media of your own
# required. config/assets/ ships a few CC0 clips; swap `path` for your own art
# in config/local/ (gitignored). Image/gif knobs: https://docs.ledticker.dev/widgets/gif/
[[playlist.section]]
mode = "slideshow"
hold_time = 5
loop_count = 1
[[playlist.section.widget]]
type = "gif"
path = "assets/phoenix.gif"
fit = "pillarbox"
image_align = "center"
# ── Add live data with plugins ────────────────────────────────────────
# Everything above is built in. For live feeds, install a first-party plugin
# (https://docs.ledticker.dev/plugins/) — copy the requirements file, add the
# plugin you want, and restart (no image rebuild):
#
# cp config/requirements-plugins.example.txt config/requirements-plugins.txt
# # uncomment the plugin line(s) you want, then:
# docker compose restart
#
# (or use the web UI Store tab, which writes that file for you). Then add a
# section using the plugin's widget type:
#
# led-ticker-weather weather.current current conditions (WeatherAPI.com)
# led-ticker-rss rss.feed RSS / Atom headlines
# led-ticker-crypto crypto.coingecko crypto prices (CoinGecko)
# led-ticker-baseball baseball.scores / .standings / … + baseball.roll* transition
# led-ticker-calendar calendar.events calendar (.ics) agenda / next event
# led-ticker-pool pool.monitor pool water temperature (InfluxDB)
#
# Example — current weather (needs led-ticker-weather + WEATHERAPI_KEY in .env;
# location can be a city name, zip code, or "lat,lon"):
#
# [[playlist.section]]
# mode = "slideshow"
# loop_count = 1
#
# [playlist.section.title]
# type = "message"
# text = "Weather"
# font_color = "random"
#
# [[playlist.section.widget]]
# type = "weather.current"
# text = "NYC"
# location = "New York"
# units = "imperial"
# ──────────────────────────────────────────────────────────────────────
# --- Images and media ---
# Bundled sample art lives in config/assets/ and is referenced as
# path = "assets/<file>" (e.g. path = "assets/phoenix.gif").
# Your own media goes in config/local/ (gitignored), referenced as
# path = "local/<file>" (e.g. path = "local/logo.gif").
# The entire config/ directory is mounted into the container, so both
# locations resolve correctly at runtime.
# Busy light — a persistent corner dot that lights up while a local file
# exists. The dot composites over every section and transition. (Real
# calendar/Slack sources are a future addition behind the same overlay.)
#
# DOCKER (the usual deploy): `~/.busy` is the CONTAINER's home, which your
# host can't reach. Point file_path at the bind-mounted config dir instead
# and toggle the file on the host:
# file_path = "/code/config/.busy" # in-container path (./config:/code/config:ro)
# touch config/.busy # ON rm config/.busy # OFF
#
# [busy_light]
# enabled = true
# file_path = "/code/config/.busy"
# poll_interval = 5.0
# corner = "top_right" # top_left | top_right | bottom_left | bottom_right
# color = [255, 0, 0] # 3 ints, each 0-255
# size = 4 # dot side in physical px: ~4 on the 16-tall
# # smallsign, ~8-10 on the 64-tall bigsign/longboi
# token = "" # fallback only — prefer LED_TICKER_BUSY_TOKEN in .env
# Web status UI — serves a live dashboard of what's on the panel at
# http://<pi>:8080. Presence of this block enables status publishing by the
# display process; the sidecar reads status.json and handles HTTP.
#
# DOCKER (the usual deploy): the compose.yaml webui service sits behind the
# `webui` compose profile — start with `COMPOSE_PROFILES=webui docker compose up -d`
# (or add COMPOSE_PROFILES=webui to your .env to make it sticky).
# Docs: https://docs.ledticker.dev/concepts/web-status-ui/
#
# [web]
# http_host = "0.0.0.0"
# http_port = 8080
# token = "" # fallback only — prefer LED_TICKER_WEB_TOKEN in .env (secrets don't belong in config.toml)
# status_path = "/run/led-ticker/status.json" # default works in Docker too (shared volume mounts here)
# allow_restart = true # enables the "Restart to apply" button in the web UI; requires a process supervisor
# # to bring the display back — Docker's `restart: unless-stopped` (shipped in compose.yaml)
# # handles this automatically. Defaults to false.