Skip to content

fix(ledmatrix-stocks): make the bitmap fonts work, and document every setting - #394

Merged
ChuckBuilds merged 1 commit into
mainfrom
docs/stocks-readme
Sep 3, 2026
Merged

fix(ledmatrix-stocks): make the bitmap fonts work, and document every setting#394
ChuckBuilds merged 1 commit into
mainfrom
docs/stocks-readme

Conversation

@ChuckBuilds

@ChuckBuildsChuckBuilds commented Sep 3, 2026

Copy link
Copy Markdown
Owner

The font bug

A .bdf is a bitmap face that exists at exactly one pixel size. Choosing 5x7.bdf or 4x6.bdf at any other size raised inside FreeType, and display_renderer.py caught it and substituted PressStart2P:

exceptException:
self.logger.warning(f"Could not load BDF font {font_name} with PIL, using default")
# Fall through to default

So the font menu appeared to work while silently ignoring the choice at twelve of the thirteen sizes the schema offers. Bitmap faces now load at their declared PIXEL_SIZE when the requested size is unavailable — the same fix already merged for clock-simple (#362), countdown (#363), news (#369), tide-display (#370), mqtt-notifications/youtube-stats (#374) and proposed for jellyfin-now-playing (#388).

cozette.bdf is removed from all six font menus. The core ships no file of that name, so selecting it could never work — see #387.

Same MINOR-vs-MAJOR judgement as #388: removing an enum value is a "removed option", but no working configuration can break because the value never loaded. 2.8.0 → 2.9.0.

Images

The plugin had none. All of these are real output, rendered from a recorded quote seeded into the cache the plugin already reads:

  • a gain, a loss, and a crypto entry — showing the separate stock/crypto colour blocks
  • display_modescroll vs switch
  • toggle_chart on and off
  • four panel sizes

These use the frames option from #369, which is what made them possible: frame 0 of a scrolling plugin is blank, because the ticker starts off-panel. My first render came out empty for exactly that reason.

The crypto row is rendered at 256 wide rather than 128 — the -812.00 (-1.3%) line is genuinely wider than a 128px panel, so no single frame of it fits there. The caption says so rather than showing a half-cropped line.

Settings

The eight per-element colour keys were covered by one sentence — "green for positive deltas and red for negative". They are now a table, including that price_delta has no text_color because its colour is picked by the sign of the change. 41 of 41 settings are documented; the config-token audit drops nothing.

One thing worth knowing

check_plugin.py passes 8/8, but every size reports "drew nothing but display() returned None". The plugin ships no test/harness.json, so the harness has no quote data, and a scroll's first frame is blank regardless — meaning the safety harness has never actually exercised this plugin's rendering. Filed separately as #395.

🤖 Generated with Claude Code

… setting
A .bdf is a bitmap face that exists at exactly one pixel size. Choosing
5x7.bdf or 4x6.bdf at any other size raised inside FreeType, and the renderer
caught it and substituted PressStart2P with a log warning -- so the font menu
appeared to work while quietly ignoring the choice at twelve of the thirteen
offered sizes. Bitmap faces now load at their declared PIXEL_SIZE instead.
cozette.bdf is removed from all six font menus: the core ships no file of that
name, so it could never load at any size (#387).
README: real rendered screenshots throughout -- a gain, a loss, a crypto entry,
both display modes, the inline chart on and off, and four panel sizes -- all
from a recorded quote seeded into the cache the plugin already reads.
The eight per-element colour keys were previously one sentence ("green for
positive deltas and red for negative"); they are now a table, including the
detail that price_delta has no text_color because its colour comes from the
sign of the change. That takes the plugin to 41 of 41 settings documented.
The scroll shots use the frames option from #369: the ticker starts off-panel,
so frame 0 of a scrolling plugin is blank.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 589378f6-f66d-4e75-8ae2-81fe95fbde20


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics9 complexity

MetricResults
Complexity9

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds
ChuckBuilds merged commit 99cddf9 into mainSep 3, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the docs/stocks-readme branch September 3, 2026 13:29
ChuckBuilds added a commit that referenced this pull request Sep 8, 2026
* fix(tests): honour LEDMATRIX_CORE instead of a developer's home directory
Twenty test files across sixteen plugins probed
`/home/rackpi/projects/LEDMatrix` first, then guessed a sibling checkout,
and inserted whichever hit at `sys.path[0]`. Both beat the PYTHONPATH that
`scripts/run_plugin_tests.py --core` sets, so the flag whose whole purpose
is choosing a core was ignored whenever either candidate happened to exist
— tests ran against the wrong checkout and reported pass or fail with equal
confidence.
They now read `LEDMATRIX_CORE`, which the runner sets to an absolute path
for exactly this reason, and only guess at the sibling layout when nobody
told them. The hardcoded home directory is gone from the repo.
Closes#467
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(sports): build card dates portably instead of with glibc-only strftime
`%-m`, `%-d` and `%-I` are glibc extensions. On Windows and on musl
(Alpine) `strftime` raises `ValueError`, so the fourteen call sites across
ten plugins could not render a game card anywhere but a glibc host.
In baseball-scoreboard's scroll renderer the damage was visible rather than
fatal: the enclosing `except` swallowed the error, dropped the start time
and drew the raw ISO date in its place, which is what made three of its
upcoming goldens the only cards failing `test_scroll_card_renders.py` off
glibc.
Output is byte-identical on glibc, so no goldens move. `sports.py` is the
copied-not-shared lineage, so all nine siblings are fixed here per
docs/plugin-development/08-shared-sports-code.md.
Closes#414Closes#447
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(scoreboards): make declared settings reach the code that draws with them
Six settings-level defects across the scoreboard lineage, all the same shape:
the schema and the web UI promise something the runtime never reads, or reads
with a different default.
* nrl: four `manager.py` fallbacks disagreed with `config_schema.json`
(`recent_games_to_show` 5→1, `upcoming_games_to_show` 10→1, `show_odds`
false→true, `update_interval_seconds` 300→3600), plus the same `show_odds`
drift in `get_info()`. They only fire for a hand-written or partial config
— and a fresh install before the UI has saved once. (#427)
* lacrosse: `defaults.display_duration` fell back to 30 where the schema says
15, and `game_display_duration` was read but declared nowhere, so the UI
could not offer it. Declared now, beside `display_duration`. (#429)
* every scoreboard: `other_games_min_quality`'s help text described a
`broadcast` value the enum does not offer — it was retired, and the code
migrates it to `ranked`. 29 descriptions and three README references
corrected. (#426)
* afl and nrl: the `scroll_settings` block was unreachable. The scroll classes
read `scroll_mode`, and `additionalProperties: false` meant a user could not
write that name either, so all twelve settings were dead. `SCROLL_CONFIG_KEY`
now names the block the schema declares; `scroll_speed`'s default moves 1.0 →
50.0 to match the core default the block had been shadowing, so nothing
changes for an existing install. (#422)
* hockey and lacrosse: the scroll and Vegas cards gated the shots line on a
flat `show_shots` key at the league root that the schema declares nowhere,
so the line never drew there whatever the user set. Both now walk the same
ladder `manager.py` resolves for the switch card. (#432)
* soccer: `leagues.<slug>.display_options` was read by nothing — 30 settings
in the UI doing nothing, and the opposite precedence to every sibling. The
league block now wins with the plugin-level key as fallback. Note the web UI
writes a value into every league block, so after a save the per-league copy
is the one in play; the README says so. (#435)
* afl, nrl, soccer, ufc: `dynamic_duration.min_duration_seconds` was declared
and never applied — only the cap was. A floor is resolved on the same ladder
and applied after the cap, so an explicit minimum wins, matching what
ledmatrix-leaderboard and cricket-scoreboard already do. `max_workers` is now
passed to `get_background_service` instead of a hardcoded 1; its schema
default moves 3 → 1 so behaviour is unchanged, and its description says the
service is a process-wide singleton whose first caller decides. (#373)
Closes#426Closes#427Closes#429Closes#422Closes#432Closes#435Closes#373
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(static-image, flights): implement date_based rotation; drop a stranger's LAN IP
* static-image: `rotation_mode: "date_based"` was a stub returning the first
image, so it looked like broken rotation rather than an unimplemented
feature — no log line, the display simply never advanced. It now picks by
day-of-year, the same indexing `of-the-day` uses: a set of seven repeats
weekly, a set of 365 walks the year, and a given date always shows the same
image. (#372)
* ledmatrix-flights: `skyaware_url` defaulted to `192.168.86.30` — one
particular receiver on one home network — in the schema and four code
paths. Every install that did not set it polled that address on its own LAN
and reported "no data received" rather than "not configured", and
`check_plugin.py` spent ~40s per run waiting out connect timeouts. The
default is empty now, and the fetcher says which setting is missing instead
of guessing. (#400)
Closes#372Closes#400
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(fonts, stocks): load bitmap faces at their own size; give stocks harness data
* Six plugins still asked FreeType for a .bdf face at an arbitrary point
size. BDF is a fixed-size bitmap format, so that raises "invalid pixel
size" for every size but the file's own strike, and the loader then fell
back to a different face with only a log warning — twelve of the thirteen
offered sizes failed that way, so picking a bitmap font in the web UI
appeared to do nothing. afl, basketball, nrl, soccer and ufc (shared
sports.py lineage) and ledmatrix-music now retry at the size the file's
PIXEL_SIZE header declares, the same fix already carried by baseball,
football, clock-simple, news, tide-display, jellyfin-now-playing,
mqtt-notifications and youtube-stats. (#387)
`cozette.bdf` is already gone from every schema, and f1-scoreboard already
pins bitmap faces to their native size through its own table.
* ledmatrix-stocks shipped no `test/harness.json`, so the safety harness had
no quote data, `stock_data` stayed empty, and all eight sizes passed
against a blank frame — nothing about the ticker layout, fonts, colours or
inline chart was being checked. It now has one, built on the recorded
fixture from #394: a gain, a loss and a crypto entry. The base run uses
switch mode, because frame 0 of a scroll is legitimately blank; a `scroll`
variant keeps the scroll path under the harness too. All eight sizes now
render real content with no empty warning. (#395)
Closes#387Closes#395
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat!: remove seventeen configuration settings that nothing reads
Every setting below rendered in the web UI with a description promising
specific behaviour, saved to config.json, and did nothing — verified against
both the plugin source and the LEDMatrix core.
BREAKING CHANGE: these keys are gone from the schemas, and all six plugins set
`additionalProperties: false`. A config.json still carrying them fails schema
validation until it is saved again from the web UI; the plugin still loads,
but the UI flags its config as not matching the schema.
* `transition.*` in christmas-countdown, nfl-draft, olympics and web-ui-info,
plus `high_performance_transitions` in the first and third. Nothing in any
of the four reads them, and the core implements no display transitions —
every occurrence of "transition" under `src/` is a comment about plugin
state machines or Vegas cycle boundaries. There is no `fade`, `slide`,
`wipe`, `dissolve` or `pixelate` anywhere. If transitions are wanted later
they belong in the core display controller, where every plugin gets them,
not in five copied schema blocks. (#381, #377)
* masters-tournament's whole `notifications` and `branding` blocks (12
settings), plus five leaves inside `display_modes` that the generic
mode-handling never reaches: `player_cards.show_headshots`,
`player_cards.duration_per_player`, `course_tour.show_animations`,
`course_tour.duration_per_hole` and `course_tour.featured_holes`. The last
one is not the `masters_featured_holes` display mode, which is live. (#418)
* march-madness's `display_options.show_bracket_progress`, and the attribute
it was assigned to — read once in `__init__`, never referenced again. (#406)
`christmas-countdown`'s `tree_size` stays: it is read and validated, just not
applied, which is a different defect and still documented in its README.
Closes#381Closes#377Closes#418Closes#406
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(render): pin the text layout engine, refresh four golden sets, stop host fonts
**Why the goldens travelled badly.** `ImageFont.truetype` picks its layout
engine at load time: Raqm where the host Pillow was built with libraqm, Basic
otherwise, and the two round fractional glyph advances differently.
`PressStart2P-Regular.ttf` at 8px has whole-pixel advances so both engines
agree — which is why fifteen plugins' goldens matched everywhere and hid this.
`4x6-font.ttf` at 6px does not: glyph positions drift cumulatively along a
run. The four plugins that draw body text in it are exactly the four with
open drift reports, and of-the-day's own issue records the tell — its
PressStart2P title is pixel-identical while its 4x6 body is not.
geochron, of-the-day, ledmatrix-weather and christmas-countdown now pin the
Basic engine where they load their own faces, and the four golden sets are
regenerated against a core doing the same (ChuckBuilds/LEDMatrix
fix/deterministic-text-rendering). All 32 plugins now pass `check_plugin.py`
here; the other fifteen golden sets are untouched and still pass, which is the
check that this pinned the right thing rather than papering over it.
**Host fonts in the documentation renderers.** `scripts/render_docs_assets.py`
and `plugins/pomodoro-timer/test/render_readme_assets.py` both picked their
caption face from a list of host font paths — DejaVu on Linux, Segoe UI on
Windows, and for the pomodoro one no Windows path at all, so it fell through
to PIL's tiny default and rewrote every labelled contact sheet on a plugin
nobody had touched. Both now use `ImageFont.load_default(size=...)`, which
Pillow >= 10.1 serves from bundled Aileron: no font files, no host lookup.
Aileron ships one cut, so bold is a point larger, and it has no em dash or
multiplication sign, so the pomodoro captions are ASCII — both rendered as a
.notdef box. Its four images are regenerated and now reproduce byte-for-byte
across runs.
The committed composites under `docs/assets/` are left as they are: their
captions will change on the next regeneration of each, which is a mechanical
pass better done per plugin than as one 217-image commit.
Closes#371Closes#375Closes#378Closes#391Closes#383
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(store): bump the 23 changed plugins and regenerate plugins.json
Six majors for the schema removals — christmas-countdown, nfl-draft,
olympics, web-ui-info, masters-tournament and march-madness — because a
config.json still carrying a removed key fails validation until it is saved
again. Eight minors where a declared setting starts working or a default
changes, nine patches for portability and rendering fixes that leave every
existing panel drawing what it drew before.
`plugins.json` regenerated with `update_registry.py`; it is never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(geochron): draw the sidebar 1-bit, so its goldens stop being host-specific
Pinning the layout engine fixed glyph *positions*, and CI showed what it did
not fix: the sidebar was still drawn anti-aliased, and edge coverage varies
with the host FreeType build. geochron draws straight onto the display
manager's canvas with its own font, so it never inherited the core's 1-bit
text setting the way plugins going through `draw_text` do.
The text is now drawn 1-bit for the duration of the readout, restoring the
previous `fontmode` afterwards so nothing else on that shared canvas changes.
That is what an LED grid wants regardless — a half-lit pixel is a dim pixel,
not a smooth edge — and it removes the last variable from the render: the
sidebar is exactly three colours now, background plus the two text colours.
Goldens regenerated.
Refs #375
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Revert "fix(geochron): draw the sidebar 1-bit, so its goldens stop being host-specific"
This reverts commit f906a93.
* revert: take the golden refresh out of this PR
CI is the only machine that can settle this, and it says the refresh is not
finished. This runner has libraqm and mine does not, which is why the drift
reports existed at all — and after pinning the layout engine plugin-side,
geochron still drifted here by 135px at max Δ=60–83, unchanged by the 1-bit
experiment in the commit reverted above (geochron's text was already 1-bit;
that change was a no-op and is gone).
So the four golden sets and the plugin-side engine pins come out, restoring
geochron, of-the-day, christmas-countdown and ledmatrix-weather to exactly
what `main` has, and their three version bumps come out with them. What stays
is everything that does not depend on a Linux render: eighteen issues, and the
two documentation renderers from #383, whose determinism is about host *font
files* rather than golden images and which pomodoro's harness already passes.
The diagnosis stands and is recorded on the four issues. The core half is
ChuckBuilds/LEDMatrix#539, which is independently correct and green. The
golden refresh wants doing from a Linux run — a `--update-golden` on this
runner — rather than guessed at from Windows, which is the mistake this
reverts.
Refs #371, #375, #378, #391
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(afl, nrl): keep the card gap at 24 now that scroll_settings is reachable
Caught reviewing #422 for merge safety rather than by a test, because no test
covers it and the harness renders frame 0, where the gap between cards does
not show.
Making the block reachable means every key in it becomes an override, and one
of them disagreed with the value the code had been resolving. Both plugins set
`gap_between_games: 24` in `scroll_settings_defaults()`, overriding the core's
48; the schema block declared 48. So the fix for #422 would have silently
doubled the gap between game cards on every afl and nrl install — the same
trap as `scroll_speed`, which I caught only because its factor was 50x rather
than 2x.
The schema now declares 24, and every key in the block agrees with what the
resolver produced while it was unreachable:
scroll_speed 50.0 (core default)
scroll_delay 0.01 (core default)
gap_between_games 24 (plugin override) <- this commit
show_league_separators true (core default)
dynamic_duration true (core default)
game_card_width 128 (already guarded as "unchosen")
`game_card_width` needed nothing: both plugins already treat a value equal to
the schema default as unchosen and substitute their computed width, which is
the same guard this needed and did not have.
Reachability is now behaviour-neutral for a default config, which is what
#422 should have been all along. READMEs corrected.
Refs #422
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ChuckBuilds