Skip to content
led-ticker

storefront plugin

The storefront plugin is provided by the storefront package in the led-ticker-plugins monorepo. It paints an always-visible OPEN or CLOSED badge as a corner overlay on top of whatever the ticker is showing, flipped by a weekly business-hours schedule and the Pi’s clock — no external service, works offline.

It’s the sibling of the core busy light overlay: same mechanism (a frame overlay hook + a background poller that flips shared state; paint() stays paint-only), richer content — text, colors, orientation, animated “neon” color.

OPEN: a shimmering green vertical badge in the top-right corner, composited over a scrolling rainbow ticker message.
OPEN: a shimmering green vertical badge in the top-right corner, composited over a scrolling rainbow ticker message.
CLOSED: an opaque red horizontal badge, same corner, same underlying ticker content.
CLOSED: an opaque red horizontal badge, same corner, same underlying ticker content.

Add the package to your plugin manifest and restart:

# First plugin? Create your manifest from the example:
cp -n config/requirements-plugins.example.txt config/requirements-plugins.txt
# Add the package (one name per line), then restart:
echo "led-ticker-storefront" >> config/requirements-plugins.txt
docker compose restart

Other install paths (web UI Store tab, dev checkout) are on the Plugins page.

Unlike a widget, storefront isn’t added to a playlist section — it’s a top-level [storefront] block, present anywhere in config.toml:

[storefront]
corner = "top_right"
[storefront.open]
text = "OPEN"
color = [0, 255, 0]
[storefront.closed]
text = "CLOSED"
color = [255, 0, 0]
[storefront.schedule]
mon = "09:00-17:00"
tue = "09:00-17:00"
wed = "09:00-17:00"
thu = "09:00-17:00"
fri = "09:00-17:00"
sat = "10:00-14:00"
# sun omitted = closed all day

The badge appears the moment the config loads (an eager first evaluation runs at startup, before the first poll), and flips automatically as the clock crosses a schedule boundary.

Each day of the week (monsun) maps to a schedule string:

  • A single range: "09:00-17:00" — 24-hour HH:MM-HH:MM.
  • Multiple ranges (e.g. a lunch closure): "09:00-12:00,13:00-17:00" — comma-separated; the day is open if any range matches.
  • Closed: "closed", or simply omit the day key — both mean closed all day.
  • Open all day: "00:00-24:00"24:00 (1440 minutes) is only valid as a range end, meaning end-of-day.
  • Overnight wrap: a range whose end is less than or equal to its start ("18:00-02:00") crosses midnight and belongs to its start day, per the caution above.

Override specific dates — a holiday, a special event, a one-off closure — without touching the weekly [storefront.schedule]:

[storefront.exceptions]
"12-25" = "closed" # recurring: every Christmas
"2026-11-26" = "closed" # one-off
"12-24" = "09:00-13:00" # recurring short day
"2026-12-31" = "20:00-02:00" # special hours; wraps allowed

Keys are "MM-DD" (recurring — matches that date every year) or "YYYY-MM-DD" (one specific date, zero-padded); values reuse the same day-string grammar as the weekly schedule ("closed", a single range, comma-separated multiple ranges, "00:00-24:00").

Precedence is specific date > recurring date > weekly day — a matching exception key replaces that day’s hours outright; it never merges with the weekly schedule underneath it. Quoting the keys is optional — TOML accepts them bare too (12-25 = "closed").

Every open/closed flip logs an enriched line naming the next change, so you can confirm an exception took effect without waiting for the next boundary:

storefront: OPEN (matched mon 09:00-17:00; closes 17:00; now 12:30 America/New_York)
storefront: CLOSED (no matching range; opens 09:00 tomorrow; now 20:15 America/New_York)
[storefront]
background = [0, 0, 0]
padding = 2
# font = "..."
font_size = 16
# timezone = "America/New_York"
corner = "top_right"
orientation = "horizontal"
[storefront.open]
text = "OPEN"
color = [0, 255, 0]
# corner and orientation fall back to the shared values above
[storefront.closed]
text = "CLOSED"
color = [255, 0, 0]
[storefront.schedule]
# mon = "09:00-17:00" ...one key per weekday (mon..sun); see above
[storefront.exceptions]
# "12-25" = "closed" ...one key per date override ("MM-DD" or "YYYY-MM-DD"); see above
FieldTypeDefaultNotes
background[r, g, b] or "none"[0, 0, 0]Opaque box behind the text. "none" paints only lit glyph pixels over live content (transparent).
paddingint2Pixels of background box around the text. Only visible with an opaque background.
fontstringbundled defaultBDF font name or hi-res font name. Hi-res is supported on the bigsign.
font_sizeint16Real (physical) pixels — block-scaled for BDF, native size for hi-res. See Sizing on large panels for the vertical-badge cap.
timezonestring (IANA name)system local clocke.g. "America/New_York". See Caveats.
cornertop_left / top_right / bottom_left / bottom_right"top_right"Shared default corner for both badges. Same four-value vocabulary as busy light.
orientationhorizontal / vertical"horizontal"Shared default orientation for both badges.
[storefront.open].textstring"OPEN"
[storefront.open].color[r, g, b] / string / table[0, 255, 0] (green)Flat color or a color provider — see Neon glow.
[storefront.open].cornercorner valueinherits shared cornerOnly set this to split the two badges into different corners.
[storefront.open].orientationorientation valueinherits shared orientation
[storefront.closed].textstring"CLOSED"
[storefront.closed].color[r, g, b] / string / table[255, 0, 0] (red)
[storefront.closed].cornercorner valueinherits shared corner
[storefront.closed].orientationorientation valueinherits shared orientation
[storefront.schedule].<mon..sun>schedule stringomitted = closedSee Schedule syntax.
[storefront.exceptions].<key>schedule stringomitted = no exceptionsKey is "MM-DD" (recurring) or "YYYY-MM-DD" (specific); value is a schedule string. Replaces the weekly day outright. See Exceptions.

background, padding, font, font_size, and timezone are shared between the two badges — there’s no per-state override for those (a documented future).

  • Corner anchoring: the badge’s bounding box (text extent + padding) anchors flush to its corner, same vocabulary as busy light’s four corners.
  • orientation = "horizontal" draws the word on one line. orientation = "vertical" stacks each character upright, one per row, centered in the column — good for a narrow strip down the edge of the panel.
  • Opaque vs. transparent background: the default background = [0, 0, 0] paints a solid box first for a clean reserved-strip look. Set background = "none" to paint only the glyph pixels directly over whatever else is on screen.
  • Hi-res fonts and font_size: a hi-res font paints at its native real-pixel size; a BDF font block-scales up to font_size. See Sizing on large panels for the vertical-badge ceiling and how to get past it.
  • Large hardware panels (bigsign): the plugin’s own examples/*.bigsign.toml smoke fixtures use a simplified single-canvas [display] block for render-demo GIFs — they are not a wiring reference. For the real 8-panel chain and pixel_mapper_config, see config/config.bigsign.example.toml in the core repo.

The badge computes its bounding box (text extent + padding) before drawing and, if it doesn’t fit the panel, logs a warning and skips the draw entirely rather than rendering a clipped or garbled badge:

storefront: badge 'OPEN' (16x64) does not fit the 64x32 panel at font_size=16; skipping. Use a smaller font_size or a shorter label.

The trap this catches most often: vertical orientation + a large font_size + the default BDF font, on a 64px-tall bigsign. BDF block-scales in whole-cell-height multiples, so a multi-letter word like OPEN stacked vertically only fits up to roughly font_size = 16 on a 64px-tall panel — larger sizes overflow and get skipped. That ceiling is relative to the panel’s real height, not a fixed number: it’s the panel height that constrains BDF’s block-scale math, not font_size itself.

There is no font_size ceiling at all with a hi-res font — font = "Inter-Regular" (or any bundled/user hi-res font) paints at its native real-pixel size and isn’t bound by BDF’s cell-height math. This is verified directly: test_hires_badge_renders_on_large_panel in the plugin’s test suite draws a 64px hi-res badge on a 512×128 headless canvas and asserts it renders with no oversize warning — a size that would have been well past the BDF ceiling on a real bigsign. BDF badges at font_size 24 and 96 on that same large canvas also block-scale up without clipping (test_bdf_block_scale_badge_on_large_panel) — the constraint tracks panel height, not the font system.

So: if the skip-warning above fires and you actually want a large badge (rather than a smaller one), the fix it names — “a smaller font_size or a shorter label” — isn’t your only option. Switching to a hi-res font via font = "..." gets you the large badge you wanted instead of shrinking it.

color on either badge accepts a flat [r, g, b] or a color provider string/table ("shimmer", "rainbow", "color_cycle", or an inline table), animated once per frame via a shared frame counter — the badge visibly breathes or sweeps rather than sitting static.

  • Timezone / NTP dependency. All schedule comparisons happen against the configured timezone (or the system local clock if omitted). A wrong clock — bad timezone, missing NTP sync, a DST edge — produces a wrong badge, and it will look like a plugin bug rather than a clock problem. The startup log line always states the time the plugin thinks it is:

    storefront: OPEN (matched mon 09:00-17:00; closes 17:00; now 14:23 America/New_York)
    storefront: CLOSED (no matching range; opens 09:00 tomorrow; now 18:05 America/New_York)

    and logs again on every state flip — if the badge looks wrong, check this line before anything else.

  • No led-ticker validate coverage. validate only checks playlist widgets, not top-level plugin overlay blocks. A malformed [storefront] block (bad time string, unknown corner/orientation, unknown weekday key, a badge too large for the panel) is caught at startup instead: the error is logged, plugin-load isolation disables the badge, and the rest of the panel keeps running undisturbed.

  • Not covered by config hot-reload. The plugin reads its [storefront] block once at startup, so editing it while the display is running does not take effect — hot-reload reports “restart required” and names the storefront block; restart the process to apply the change.