Skip to content
led-ticker

Display

The [display] block describes your panel’s physical geometry: pixel dimensions per panel, chain length, and scaling. These values flow into every layout calculation, so a mismatch between config and hardware clips content or centers it on the wrong axis.

Two hardware configurations ship as ready-to-use examples — the maintainer’s day-to-day rigs that the demos and example configs are written against. You can match them, scale them, or build something different; the engine only cares about what [display] declares. The smallsign is a Raspberry Pi 4 with five 32×16 panels chained horizontally for a 160×16 canvas. The bigsign is a Raspberry Pi 5 with eight P3 32×64 panels arranged in a 2×4 vertical-serpentine layout for a 256×64 canvas.

Smallsign — Pi 4 + 5× 32×16 panels = 160×16 logical

[display]
rows = 16
cols = 32
chain_length = 5
default_scale = 1
brightness = 60
gpio_slowdown = 2

Bigsign — Pi 5 + 8× P3 32×64 vertical-serpentine 2×4 = 256×64 logical

[display]
rows = 32
cols = 64
chain_length = 8
parallel = 1
pixel_mapper_config = "Remap:256,64|192,32n|192,0n|128,32n|128,0n|64,32n|64,0n|0,32n|0,0n"
default_scale = 4
brightness = 60
gpio_slowdown = 3
pwm_bits = 8

When default_scale > 1, the engine wraps the real panel in a ScaledCanvas — the layer that expands logical pixels onto a big / scaled sign. All drawing logic stays at “16-tall logical content,” so widgets never need to know what scale they are running at; the wrapper expands each logical pixel to a scale×scale block on the real canvas and centers the content vertically. For developers and plugin authors: that expansion happens per SetPixel call.

There is one hard ceiling you must respect: content_height × scale ≤ panel_h_real. For the bigsign at scale = 4, this means content_height ≤ 16. Push above the ceiling and the logical canvas is taller than the real panel; content placed near the top or bottom edges silently clips. If you need per-section breathing room, use text_y_offset on the widget rather than over-specifying content_height.

scale and content_height can also be set per section, overriding the display-level defaults. The classic case is a two_row widget: its two text rows need more horizontal room to fit handles like @firebird.demo, so it typically runs at scale = 2, giving 128 logical pixels of width instead of the default 64.

[[playlist.section]]
mode = "slideshow"
scale = 2
content_height = 24
hold_time = 4.0

[display.schedule] lets you automatically dim or darken the sign during certain hours — useful for not blasting full brightness at 3 a.m. The schedule is driven by a lightweight background task that wakes every ~30 seconds and adjusts matrix.brightness to match whichever window is active right now.

A schedule is a list of [[display.schedule.windows]] entries. Each window covers a time range and specifies a brightness:

[display.schedule]
enabled = true
timezone = "America/New_York" # set this — fresh Pis are often UTC
[[display.schedule.windows]]
start = "07:00"
end = "18:00"
brightness = 100
[[display.schedule.windows]]
start = "18:00"
end = "23:00"
brightness = 40
[[display.schedule.windows]]
start = "23:00" # wraps past midnight
end = "07:00"
brightness = 0 # dark — panel off overnight

brightness = 0 makes the LEDs go dark (≈ zero panel power draw). This is not a power or sleep mode — the Pi keeps rendering at full cadence; only the LED brightness output goes to zero.

At each check the scheduler finds the last matching window in the list whose time range includes the current wall-clock time. “Last” means highest index, so place more-specific or higher-priority windows after the general ones.

Because the last matching window wins, place a more specific window after a general one to override it. For example, a normal day with a dimmer lunch hour:

[[display.schedule.windows]]
start = "07:00"
end = "23:00"
brightness = 100
[[display.schedule.windows]]
start = "12:00" # overrides the window above during lunch
end = "13:00"
brightness = 40

A window wraps past midnight when end < start (e.g. start = "23:00", end = "07:00"). That window is active from 23:00 until 06:59.

Add a days list to a window to restrict it to specific weekdays:

[[display.schedule.windows]]
start = "08:00"
end = "18:00"
brightness = 100
days = ["mon", "tue", "wed", "thu", "fri"] # weekdays only
[[display.schedule.windows]]
start = "10:00"
end = "22:00"
brightness = 60
days = ["sat", "sun"] # weekends on a gentler setting

An empty days list (the default) matches every day of the week.

  • Boundary latency: brightness changes land within approximately 30 seconds of the scheduled time, not instantly at the second.
  • Step, not fade: brightness jumps in one step when a window becomes active — there is no fade animation.
  • Schedule changes need a restart: the schedule is read at startup; there is no live-reload. Restart the display process after editing [display.schedule].
  • Timezone: timezone accepts any IANA name (e.g. "America/New_York", "Europe/London"). Leave it empty to use the Pi’s system time — but set it explicitly if you can; freshly imaged Pis default to UTC, which will shift your windows by several hours.

Run led-ticker validate config.toml to see the resolved schedule printed alongside the usual validation output. This is useful for confirming window coverage and catching gaps before deploying.

The full field reference — every knob, default value, and Pi-version note — lives at Reference: Config options. The most-touched fields are above (rows, cols, chain_length, default_scale, pixel_mapper_config, gpio_slowdown); see the reference page when you need pwm_bits, pwm_lsb_nanoseconds, rp1_pio, or the other Pi-tuning options.