Skip to content

fix(tide-display): load bitmap fonts at their own size, and document everything - #370

Merged
ChuckBuilds merged 3 commits into
mainfrom
docs/tide-display-readme
Sep 3, 2026
Merged

fix(tide-display): load bitmap fonts at their own size, and document everything#370
ChuckBuilds merged 3 commits into
mainfrom
docs/tide-display-readme

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Tenth plugin in the README pass.

The font picker worked by coincidence

A .bdf exists at exactly one pixel size and FreeType rejects any other. So 4x6.bdf loaded — its native 6 happens to equal the default font_size: 6 — while 5x7.bdf silently fell back to the default face. Set font_size: 7 and the two would swap over.

before   66b4f94a29: ['4x6-font.ttf', '5x7.bdf']    <- 5x7 falling back
after    5 of 5 distinct

Same fix as #362 and #369. That's four plugins now; mqtt-notifications and youtube-stats still carry it — happy to sweep both in one PR if you'd prefer that to finding it plugin by plugin.

The README was a 70-line stub

None of the plugin's four screens were shown. It now covers all 18 settings with real screenshots of every screen, from recorded NOAA responses for Boston, and leads with the thing you actually have to do first — find your station ID.

Two behaviours that were undocumented and look like faults:

  • A station with no live water-level sensor falls back to predictions rather than erroring. That's the common case, and it's logged at debug rather than as an error.
  • Unit conversion is done by NOAA, not locally. The plugin asks the API for english or metric, so both are authoritative figures rather than one rounded from the other.

A mistake the tooling caught

The HTTP replay can now match on query parameters, not just the URL — NOAA puts three endpoints behind one URL and tells them apart by params, so without it every request got whichever fixture matched first.

That directly caught an error of mine. With the fixture ignoring units, metric mode rendered the imperial numbers with an "m" suffix — 9.7ft became "9.7m". It looked exactly like a missing unit conversion, and I was a step from writing it up as a bug. Reading the code showed the plugin correctly maps imperialenglish and lets NOAA convert; the fault was entirely in my fixture. Recording both unit systems shows the real values (9.7ft / 3.0m).

One observation, not a diagnosis

On a 32-row panel the stats screen draws the range line and the percentage close enough to overlap at some values. It's in the README with images showing it, described as what it is rather than diagnosed — the chart and schedule screens are unaffected, and 64 rows clears it.

A note on render_preview.py

The plugin ships its own preview generator that re-implements the screens, with a header noting its palette "must match manager.py" — so its preview_*.png files can drift from real output without anything failing. The README images here come from running the actual plugin instead. I've left the script and its PNGs alone and just noted the difference; say the word if you'd like them removed, since they also ship to users' Pis on install.

Verification

  • Audits: none dropped, 18/18 schema leaves documented, no broken anchors, every old section accounted for
  • check_plugin.py --plugin tide-display32/32 PASS (8 sizes × 4 modes)
  • render_docs_assets.py --plugin tide-display --check — images match
  • manifest bumped 1.2.0 → 1.3.0

🤖 Generated with Claude Code

…everything

The font picker worked or not by coincidence. A .bdf exists at exactly one
pixel size and FreeType rejects any other, so 4x6.bdf loaded because its native
6 happens to equal the default font_size, while 5x7.bdf silently fell back to
the default face -- and the two would swap over if font_size were set to 7.
Hashing one render per face showed 4x6-font.ttf and 5x7.bdf producing the same
image; after the fix all five are distinct. Same fix already landed for
clock-simple and news, which share this loader.

The README was a 70-line stub with none of the plugin's four screens shown. It
now covers all 18 settings with real rendered screenshots of every screen,
taken from recorded NOAA responses for station 8443970 on 2 September 2026, and
explains the thing you actually have to do first: find your station ID.

Two behaviours that were undocumented and look like faults: a station without a
live water-level sensor falls back to predictions rather than erroring, which is
the common case; and unit conversion is done by NOAA rather than locally, so the
metric heights are authoritative rather than a rounding of the imperial ones.

The 32-row stats screen draws the range line and the percentage close enough to
overlap at some values. Recorded as an observation with the images to show it,
not diagnosed -- the chart and schedule screens are unaffected and 64 rows
clears it.

Tooling: the HTTP replay can now match on query parameters, not just the URL.
NOAA puts three endpoints behind one URL and distinguishes them by params, and
without this every request returned whichever fixture matched first. It also
caught a mistake of mine: with the fixture ignoring the units parameter, metric
mode rendered the imperial numbers with an "m" suffix and looked exactly like a
missing conversion. Recording both unit systems and matching on the parameter
shows the real metric predictions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

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: 16fedf0a-c200-4193-80f7-4ecfd83e1970


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.

Comparing against the old README by eye turned up three things my heading
heuristic passed over -- it matched incidental words rather than content, so
'Popular Station IDs' looked covered because the new file happens to say
'station' elsewhere.

Restored, each verified against the source first:

- Disabling every screen shows all four rather than nothing. _build_enabled_modes
  returns 'modes or list(self.MODES)', with a comment saying the plugin should
  never go dark. Worth stating, since the obvious reading of four booleans is
  that turning them all off leaves a blank panel.
- The ten-station starter table. Finding a station ID is the one thing a new
  user has to do, and a handful of known-good IDs saves a trip to NOAA's map.
- Caching, and what happens when NOAA is unreachable. Predictions sit under a
  stable per-station key and refetch on a date change; the live level is cached
  for six minutes; and STALE_MAX_DAYS serves the last good predictions for up to
  two days offline before falling back to a placeholder, because tides shift
  about fifty minutes a day and older times would be confidently wrong.

The audit is a floor, not a guarantee -- it catches dropped config keys reliably
and dropped prose only sometimes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 medium

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
Security 1 medium

View in Codacy

🟢 Metrics 14 complexity

Metric Results
Complexity 14

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.

…ted the screens

render_preview.py drew its own mock versions of all four screens, with a header
asking that its palette "must match manager.py". Nothing enforced that, so its
seven committed preview_*.png files could drift from what the plugin actually
draws and no check would notice -- and they shipped to every Pi on install,
about 115KB of images the display never reads.

The README's screenshots now come from scripts/render_docs_assets.py, which
runs the real plugin against recorded NOAA responses and is verifiable with
--check, so the mock generator no longer buys anything the repo does not have a
better version of.

Worth contrasting with geochron, which has a render_preview.py of its own and
does it correctly: it imports geochron_renderer, the same module manager.py
draws with, so its previews cannot disagree with the plugin. That is the
pattern to copy if a standalone generator is wanted here again -- factor the
drawing out and have both call it, rather than keeping a second copy of the
palette.

Nothing else in the repo referenced the removed files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ChuckBuilds

Copy link
Copy Markdown
Owner Author

Took you up on the offer and removed render_preview.py plus its seven preview_*.png files, since the README now has real renders that --check verifies.

The case for removing rather than keeping: the script drew its own mock versions of all four screens, with a header asking that its palette "must match manager.py". Nothing enforced that, so its committed previews could drift from actual output with no check to notice — and they shipped to every Pi on install, ~115KB the display never reads.

Worth contrasting with geochron, which has a render_preview.py of its own and does it right: it imports geochron_renderer, the same module manager.py draws with, so its previews cannot disagree with the plugin. If you want a standalone generator here again, that's the pattern — factor the drawing out and have both call it, rather than keeping a second copy of the palette. I've said as much in the commit message so the reasoning survives.

Nothing else in the repo referenced the removed files (checked across .md, .json, .py, .yml).

Separately, per your ask, I've started filing findings as issues rather than leaving them in PR descriptions — #371, #372, #373 here and ChuckBuilds/LEDMatrix#517 for the core font root cause.

@ChuckBuilds
ChuckBuilds merged commit 9094d7c into main Sep 3, 2026
2 of 4 checks passed
ChuckBuilds added a commit that referenced this pull request Sep 3, 2026
…ns (#374)

Completes the sweep. A .bdf exists at exactly one pixel size and FreeType
rejects any other with "invalid pixel size", so every bitmap face in these two
pickers failed at the configured font_size and silently fell back to the
default -- choosing one appeared to do nothing, with only a log warning nobody
reads. Bitmap faces are now retried at the size the file's PIXEL_SIZE header
declares.

cozette.bdf is also removed from mqtt-notifications' picker. The file is
shipped by neither the core nor the plugin, so selecting it could only ever
fall back. youtube-stats never listed it.

Same fix already landed for clock-simple (#362), news (#369) and tide-display
(#370), which carry copies of this loader. That is five plugins with one bug,
so the root cause is filed against the core as ChuckBuilds/LEDMatrix#517:
FontManager.resolve_font degrades to the default face for any .bdf without
signalling it, which is why each plugin had to work around it separately.

Verification is unit-level rather than by render, because neither plugin draws
text without credentials -- mqtt-notifications needs a broker and youtube-stats
an API key, so the hash-comparison used on the other plugins produces identical
"no data" screens either way. Instead the loader's own path is exercised
directly: _bdf_pixel_size returns 7 for 5x7.bdf and 6 for 4x6.bdf,
ImageFont.truetype fails for both at size 8, and succeeds at the reported
native size. That is exactly the branch the fix takes.

The harness passes 8/8 for both. mqtt-notifications could not be harnessed at
all before this: it fails to instantiate without paho-mqtt, which is in its
requirements.txt but not in the core's environment, so anyone running
check_plugin without installing it first sees eight load errors rather than a
result.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 3, 2026
The replay shim gained a feature on each side and they had to be combined
rather than chosen between: main added params_contain (#370, for an API that
puts several endpoints behind one URL), while this branch added body_file and
changed _match to return a response object rather than a bare body (so poster
art can be served as bytes).

Resolved so _match filters on params first, then serves either a file as bytes
or the JSON body, always wrapped in a _ReplayResponse; both callers pass the
request params through. Also dropped a sentinel line the merge duplicated.

Verified after resolving: --check reproduces jellyfin's five body_file-driven
images and tide-display's params_contain-driven ones byte-identically, so
neither side's feature was lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to 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