mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
fix(display): end the scroll before the schedule-off blank and WiFi notice (#721)
The schedule-off blank and the WiFi notice are drawn by the display controller, not dispatched to a plugin, so #716's handover never reached them. Drawn while the last scroll's state was still set, the blank went out with the ticker's lagging rows on a scan-compensated panel and stayed up for its 60 s dwell, and the notice's redraws (which #712 now shows over a running scroller or Vegas) were timed as 0.5-1 s freezes and logged as a mid-scroll Render stall. The controller now calls set_scrolling_state(False) before drawing either; a scroller that resumes sets the state again on its next frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,15 @@ policies are unchanged.
|
||||
|
||||
### Fixes
|
||||
|
||||
- The schedule-off blank and the WiFi notice no longer start with a
|
||||
scroller's leftovers. Both are drawn by the display controller rather than
|
||||
dispatched to a plugin, so #716's handover never reached them: drawn while
|
||||
the last scroll's state was still set, the blank went out with the
|
||||
ticker's lagging rows on a scan-compensated panel and stayed up for its
|
||||
60 s dwell, and the notice's redraws (which #712 now shows over a running
|
||||
scroller or Vegas) were counted as 0.5-1 s freezes and logged as a
|
||||
`Render stall ... mid-scroll`. The controller now ends the scroll state
|
||||
before drawing either.
|
||||
- A plugin whose `display()` raises now opens its circuit breaker. The first
|
||||
frame of each screen goes through the plugin executor, which caught the
|
||||
exception and returned False. The display read that as "no content" and
|
||||
|
||||
+10
-17
@@ -554,24 +554,17 @@ its first `display()`, so the frames that call presents go out as drawn, in one
|
||||
swap each, instead of with the lagging half taken from the scroller's last
|
||||
frame.
|
||||
|
||||
Other screens that follow a scroll still are, while the scroll state lasts (it
|
||||
expires 2 s after the scroller's last frame). Their first frame takes its
|
||||
lagging half from the scroller's last frame: for one refresh after a held
|
||||
scroll, and after a scroll at one frame per refresh until the next frame
|
||||
replaces it. They are:
|
||||
The controller's own screens -- the blank shown when the schedule turns the
|
||||
panel off, and the WiFi status message -- end the scroll state before they are
|
||||
drawn, so they go out as drawn and are timed as static frames, not as freezes
|
||||
of the old scroll. A scroller that resumes after a WiFi notice sets the state
|
||||
again on its next frame.
|
||||
|
||||
- the blank shown when the schedule turns the panel off. It is redrawn once a
|
||||
minute while the panel is off, so half of the scroller's last frame can stay
|
||||
lit for up to 60 s;
|
||||
- the WiFi status message, until the next pass half a second later;
|
||||
- a screen that runs the high-FPS loop without scrolling (an older
|
||||
`static-image`, which is forced into it), until its next frame.
|
||||
|
||||
The frame stats still time those two as frames of the old scroll: a WiFi
|
||||
notice that preempts a scroller records up to three 0.5-1 s freezes, and the
|
||||
schedule-off blank a `Render stall ... mid-scroll`. Ending the scroll state
|
||||
before the schedule-off blank and the WiFi message is a follow-up, the
|
||||
schedule-off blank first.
|
||||
One screen that follows a scroll is still composed while the scroll state lasts
|
||||
(it expires 2 s after the scroller's last frame): a screen that runs the
|
||||
high-FPS loop without scrolling (an older `static-image`, which is forced into
|
||||
it). Its first frame takes its lagging half from the scroller's last frame, for
|
||||
one refresh after a held scroll and otherwise until its next frame.
|
||||
|
||||
Checked on hdpi (4×128×64 on one chain, rotated 180, 2026-09-24) before it was
|
||||
written: `scan_mode: 1` (interlaced) made the step vanish but turned moving
|
||||
|
||||
@@ -2592,6 +2592,7 @@ class DisplayController:
|
||||
"""
|
||||
# Clear display when schedule makes it inactive to ensure blank screen
|
||||
# (not showing initialization screen)
|
||||
self._end_scroll_before_core_screen()
|
||||
try:
|
||||
self.display_manager.clear()
|
||||
self.display_manager.update_display()
|
||||
@@ -2692,6 +2693,7 @@ class DisplayController:
|
||||
wifi_status_data = self._check_wifi_status_message()
|
||||
if not wifi_status_data:
|
||||
return False
|
||||
self._end_scroll_before_core_screen()
|
||||
if not self._display_wifi_status_message(wifi_status_data):
|
||||
# Display failed, clear the status and continue normally
|
||||
return False
|
||||
@@ -2798,6 +2800,22 @@ class DisplayController:
|
||||
end_scroll()
|
||||
return static_screen
|
||||
|
||||
def _end_scroll_before_core_screen(self) -> None:
|
||||
"""End any scroll before the controller draws a screen of its own.
|
||||
|
||||
The schedule-off blank and the WiFi notice are drawn by the
|
||||
controller, not by a plugin, so they never pass through
|
||||
_dispatch_first_frame and its handover. Drawn while the last scroll's state is still set, the blank
|
||||
went out with the scroller's lagging rows (on a scan-compensated
|
||||
panel) for its 60 s dwell, and the notice's redraws were timed as
|
||||
freezes of the old scroll. Ending the state first sends them out as
|
||||
drawn, at hold 1, as static frames. A scroller that resumes sets the
|
||||
state again on its next frame.
|
||||
"""
|
||||
set_scrolling_state = getattr(self.display_manager, 'set_scrolling_state', None)
|
||||
if set_scrolling_state is not None:
|
||||
set_scrolling_state(False)
|
||||
|
||||
def _note_screen_handover(self) -> None:
|
||||
"""Tag the frame the first dispatch is about to present.
|
||||
|
||||
|
||||
@@ -639,3 +639,66 @@ class TestRunLoop:
|
||||
# Undecidable, so the broken screen's turn left the scroll state alone.
|
||||
_, turn = _turn(c.events, "broken")
|
||||
assert ("scrolling", False) not in turn
|
||||
|
||||
|
||||
def _core_screen_controller(dm):
|
||||
"""Just enough of a DisplayController to draw its own screens on ``dm``."""
|
||||
from src.display_controller import DisplayController
|
||||
c = MagicMock()
|
||||
c.display_manager = dm
|
||||
c.on_demand_active = False
|
||||
c._end_scroll_before_core_screen = types.MethodType(
|
||||
DisplayController._end_scroll_before_core_screen, c)
|
||||
return c
|
||||
|
||||
|
||||
def _record_scrolling(dm, monkeypatch):
|
||||
"""The ``scrolling`` flag each presented frame is timed with."""
|
||||
flags = []
|
||||
real = dm.frame_timing.record
|
||||
|
||||
def record(*args, **kwargs):
|
||||
flags.append(args[3])
|
||||
return real(*args, **kwargs)
|
||||
monkeypatch.setattr(dm.frame_timing, "record", record)
|
||||
return flags
|
||||
|
||||
|
||||
class TestTheControllersOwnScreens:
|
||||
"""The schedule-off blank and the WiFi notice are drawn by the controller,
|
||||
not dispatched to a plugin, so they end the scroll themselves."""
|
||||
|
||||
def test_the_schedule_off_blank_shows_none_of_the_ticker(self, dm, monkeypatch):
|
||||
from src.display_controller import DisplayController
|
||||
dm._scan_lag_bands = [(24, 48, 1)]
|
||||
dm.set_scrolling_state(True, 1)
|
||||
_push(dm, (255, 0, 0)) # the ticker's last frame
|
||||
flags = _record_scrolling(dm, monkeypatch)
|
||||
c = _core_screen_controller(dm)
|
||||
DisplayController._blank_while_scheduled_off(c)
|
||||
assert dm._presented[-1].getpixel((10, 30)) == (0, 0, 0)
|
||||
assert flags == [False] # a static frame, not a freeze
|
||||
assert not dm.is_currently_scrolling()
|
||||
c._sleep_with_plugin_updates.assert_called_once_with(60)
|
||||
|
||||
def test_the_wifi_notice_shows_none_of_the_ticker(self, dm, monkeypatch):
|
||||
from src.display_controller import DisplayController
|
||||
dm._scan_lag_bands = [(24, 48, 1)]
|
||||
dm.set_scrolling_state(True, 1)
|
||||
_push(dm, (255, 0, 0))
|
||||
flags = _record_scrolling(dm, monkeypatch)
|
||||
c = _core_screen_controller(dm)
|
||||
c._check_wifi_status_message.return_value = {"message": "x", "expires_at": 1e12}
|
||||
c._display_wifi_status_message.side_effect = lambda _s: _push(dm, (0, 0, 255)) and True
|
||||
assert DisplayController._show_wifi_notice(c) is True
|
||||
assert dm._presented[-1].getpixel((10, 30)) == (0, 0, 255)
|
||||
assert flags == [False]
|
||||
|
||||
def test_no_notice_leaves_the_scroll_alone(self, dm):
|
||||
from src.display_controller import DisplayController
|
||||
dm.set_scrolling_state(True, 2)
|
||||
c = _core_screen_controller(dm)
|
||||
c._check_wifi_status_message.return_value = None
|
||||
assert DisplayController._show_wifi_notice(c) is False
|
||||
assert dm.is_currently_scrolling()
|
||||
assert dm._frame_hold == 2
|
||||
|
||||
Reference in New Issue
Block a user