fix(display): end the scroll state at scroller-to-static handovers (#716)

A static plugin screen that follows a scroller no longer starts with the ticker's lagging rows on scan-compensated panels, and the 1 Hz loop's second frame is no longer recorded as a ~1 s mid-scroll freeze / Render stall. The display controller calls DisplayManager.end_scroll_for_static_screen() before a static screen's first display() (clears the scan history; _scan_segments passes its frames through in one swap) and set_scrolling_state(False) after it; the scroller's hold stays until then, so late-frame counts are unchanged. A screen's first frame is tagged 'handover': gaps of 250 ms or more before it go to the additive handover_freezes (frame_soak prints 'Handover gaps'), not freezes. The display thread is named display-<plugin id>. The WiFi notice and the schedule-off blank are not covered yet (docs list them as a follow-up).

ledpi A B B A soak (20 min each, --preview): main 0.118% / 0.113% late with 6 / 3 freezes; with this and #717 0.107% / 0.104% late, 0 freezes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-01 20:13:57 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 41db488c73
commit 34be83d595
12 changed files with 1041 additions and 38 deletions
+21
View File
@@ -141,6 +141,27 @@ class TestPluginExecutor:
assert result is True
mock_plugin.display.assert_called_once()
def test_execute_display_runs_on_a_thread_named_for_the_plugin(self):
"""A screen's first frame is presented from this thread, so stack
dumps (the frame-timing stall watchdog's) should name the plugin."""
import threading
from src.plugin_system.plugin_executor import PluginExecutor
executor = PluginExecutor()
seen = []
class Plugin:
def display(self, display_mode=None, force_clear=False):
seen.append((threading.current_thread().name, display_mode))
return True
# Both ways display() is called: without a mode, and with one (most
# multi-mode plugins, the scoreboards among them).
assert executor.execute_display(Plugin(), "clock-simple") is True
assert executor.execute_display(Plugin(), "clock-simple",
display_mode="clock") is True
assert seen == [("display-clock-simple", None),
("display-clock-simple", "clock")]
def test_execute_display_exception(self):
"""Test display execution with exception."""
from src.plugin_system.plugin_executor import PluginExecutor