fix(ledmatrix-stocks): make the bitmap fonts work, and document every setting - #394
Merged
Conversation
… 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>
Contributor
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 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. Comment |
Up to standards ✅🟢 Issues |
| Metric | Results |
|---|---|
| Complexity | 9 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewerTIP This summary will be updated as you push new changes.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 3, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The font bug
A
.bdfis a bitmap face that exists at exactly one pixel size. Choosing5x7.bdfor4x6.bdfat any other size raised inside FreeType, anddisplay_renderer.pycaught it and substituted PressStart2P: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_SIZEwhen the requested size is unavailable — the same fix already merged forclock-simple(#362),countdown(#363),news(#369),tide-display(#370),mqtt-notifications/youtube-stats(#374) and proposed forjellyfin-now-playing(#388).cozette.bdfis 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:
display_modescrollvsswitchtoggle_charton and offThese use the
framesoption 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_deltahas notext_colorbecause 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.pypasses 8/8, but every size reports "drew nothing but display() returned None". The plugin ships notest/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