Skip to content

fix(display): a non-numeric plugin duration no longer stops the display; narrow scroll strips no longer raise - #739

Merged
ChuckBuilds merged 2 commits into
mainfrom
fix/display-loop-crashes
Oct 4, 2026
Merged

ChuckBuilds merged 2 commits into
mainfrom
fix/display-loop-crashes

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Summary

Two crashes in the render path.

1. A plugin duration that isn't a number stopped the display

Several plugins return display_duration straight from config.json: clock-simple, calendar and countdown. A value saved as "20" or null (from the raw Config Editor or a hand edit) reached _resolve_durations. There, max_duration <= 0 raised TypeError, which no handler in the loop catches, so run() exited.

Reproduced on ledpi (plain main ef69201): I set clock-simple's display_duration to "20" and started it on demand.

TypeError: '<=' not supported between instances of 'str' and 'int'
systemd[1]: ledmatrix.service: Deactivated successfully.
systemd[1]: ledmatrix.service: Scheduled restart job, restart counter is at 1.

The panel went dark and the service restarted 10 s later. The same build with this fix kept running with the same PID and showed the clock.

Fix: _get_display_duration runs the plugin's get_display_duration() in a try block and takes its value through _finite_seconds.

  • A numeric string counts, as in BasePlugin.
  • None, other strings, bools, NaN, inf, or a raise get the 30 s fallback, with one warning per plugin.
  • 0 or a negative number still goes to _resolve_durations' existing 15 s rule.

2. ScrollHelper raised on every frame for a strip narrower than the panel

Cause: the wrap branch copied the strip's tail and then "the rest" of the frame from its head, assuming the head was panel-wide. A narrower strip raised could not broadcast ... at every position. Vegas composes such a strip, with no lead-in, when its content is narrower than the chain.

Fix: a frame that runs off the strip takes columns with np.take(..., mode='wrap', out=frame_buffer). Frame column j is strip column (start + j) mod width. A wide strip still shows tail then head as before; a narrow one repeats across the panel.

  • Both fast paths now also require start_x >= 0.
  • A zero-width strip is a black frame.

Tests

  • New: test_display_duration_not_a_number.py (25, including the real run() loop via the run-loop harness, which returned at t=30 on main) and test_scroll_helper_narrow_strip.py (24). On main, 22 of the first and 12 of the second fail.
  • Related suites: display controller suites 209 passed; scroll/Vegas suites 1003 passed. One real-socket sync test also fails on main, Windows only.

Part of a bug sweep

This is one of 10 independent fix PRs from one sweep, all based on main ef69201.

  • Merge order: any. 45 pairwise test merges gave 0 conflicts, and each PR also merges cleanly with fix(ipc): ticks carry the volatile timestamps, so current-status stays known over the socket #737.
  • CHANGELOG: each PR adds its bullet at a different place in Unreleased → Fixes, so squash-merging them one after another needs no conflict fixing.
  • Full suite (Windows), all 10 merged together vs plain main: the same 62 failures and 6 errors on both. These are the known Windows path and file-locking tests. 191 more tests pass.
    • One extra failure in that run, test_backup_manager.py::test_create_backup_contents (os.replace → WinError 5 on a temp zip), was a Windows file-lock flake. It passes on rerun, and nothing here touches create_backup.
  • ledpi: all 10 together ran on ledpi (Pi 4) on top of main, with a clean start and no errors or render stalls in the journal. ledpi is back on plain main.

🤖 Generated with Claude Code

ChuckBuilds and others added 2 commits October 3, 2026 20:49
…the display

DisplayController._get_display_duration returned whatever the plugin's
get_display_duration() gave back. clock-simple, calendar and countdown
return their display_duration setting straight from config.json, so a
value saved as "20" or null reached _resolve_durations as a string or
None, and its `<= 0` check raised a TypeError. Nothing in the loop caught
it: run()'s outer handler logged "Unexpected error in display controller"
and cleanup() ended the service when that plugin's screen came up, and
systemd restarted it into the same crash.

The plugin's answer is now read as seconds: a finite number or a numeric
string is used (as BasePlugin.get_display_duration already accepts), a
number at or below zero still goes to _resolve_durations' 15 s rule, and
anything else -- None, a non-numeric string, a bool, NaN, infinity, or a
get_display_duration() that raises -- gets the 30 s a mode without a
plugin gets. The warning is logged once per plugin, not at every screen.

Tests: test/test_display_duration_not_a_number.py, including the real
run() on the run-loop harness, which returned at t=30 before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y frame

ScrollHelper._get_visible_portion_integer handled a frame that runs off
the end of the strip by copying the strip's tail and then the rest of the
frame from its head, which assumed the head was at least that wide. For a
strip narrower than the panel that raised "could not broadcast input
array" at every position, so get_visible_portion() never returned a frame
and the caller logged a traceback each frame. Vegas composes such a strip
(lead_in_width defaults to 0) when its content is narrower than the chain.

A wrapping frame is now taken column by column modulo the strip's width
(np.take, mode='wrap', into the reused frame buffer): the tail then the
head, as before, and a narrow strip repeated across the panel. The same
path takes a position before the start of the strip, whose [-n:m] slice
was empty and made frombytes raise; the integer and sub-pixel fast paths
now leave a negative start to it. A zero-width strip is still a black
frame.

Tests: test/test_scroll_helper_narrow_strip.py.

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

coderabbitai Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 29 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c1ec299d-fba8-4fe3-b74a-51c217bb798c
📥 Commits

Reviewing files that changed from the base of the PR and between ef69201 and d357311.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/common/scroll_helper.py
  • src/display_controller.py
  • test/test_display_duration_not_a_number.py
  • test/test_scroll_helper_narrow_strip.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 4 complexity

Metric Results
Complexity 4

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 064b9c9 into main Oct 4, 2026
15 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/display-loop-crashes branch October 4, 2026 02:18
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