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
|
### 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
|
- A plugin whose `display()` raises now opens its circuit breaker. The first
|
||||||
frame of each screen goes through the plugin executor, which caught the
|
frame of each screen goes through the plugin executor, which caught the
|
||||||
exception and returned False. The display read that as "no content" and
|
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
|
swap each, instead of with the lagging half taken from the scroller's last
|
||||||
frame.
|
frame.
|
||||||
|
|
||||||
Other screens that follow a scroll still are, while the scroll state lasts (it
|
The controller's own screens -- the blank shown when the schedule turns the
|
||||||
expires 2 s after the scroller's last frame). Their first frame takes its
|
panel off, and the WiFi status message -- end the scroll state before they are
|
||||||
lagging half from the scroller's last frame: for one refresh after a held
|
drawn, so they go out as drawn and are timed as static frames, not as freezes
|
||||||
scroll, and after a scroll at one frame per refresh until the next frame
|
of the old scroll. A scroller that resumes after a WiFi notice sets the state
|
||||||
replaces it. They are:
|
again on its next frame.
|
||||||
|
|
||||||
- the blank shown when the schedule turns the panel off. It is redrawn once a
|
One screen that follows a scroll is still composed while the scroll state lasts
|
||||||
minute while the panel is off, so half of the scroller's last frame can stay
|
(it expires 2 s after the scroller's last frame): a screen that runs the
|
||||||
lit for up to 60 s;
|
high-FPS loop without scrolling (an older `static-image`, which is forced into
|
||||||
- the WiFi status message, until the next pass half a second later;
|
it). Its first frame takes its lagging half from the scroller's last frame, for
|
||||||
- a screen that runs the high-FPS loop without scrolling (an older
|
one refresh after a held scroll and otherwise until its next frame.
|
||||||
`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.
|
|
||||||
|
|
||||||
Checked on hdpi (4×128×64 on one chain, rotated 180, 2026-09-24) before it was
|
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
|
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
|
# Clear display when schedule makes it inactive to ensure blank screen
|
||||||
# (not showing initialization screen)
|
# (not showing initialization screen)
|
||||||
|
self._end_scroll_before_core_screen()
|
||||||
try:
|
try:
|
||||||
self.display_manager.clear()
|
self.display_manager.clear()
|
||||||
self.display_manager.update_display()
|
self.display_manager.update_display()
|
||||||
@@ -2692,6 +2693,7 @@ class DisplayController:
|
|||||||
wifi_status_data = self._check_wifi_status_message()
|
wifi_status_data = self._check_wifi_status_message()
|
||||||
if not wifi_status_data:
|
if not wifi_status_data:
|
||||||
return False
|
return False
|
||||||
|
self._end_scroll_before_core_screen()
|
||||||
if not self._display_wifi_status_message(wifi_status_data):
|
if not self._display_wifi_status_message(wifi_status_data):
|
||||||
# Display failed, clear the status and continue normally
|
# Display failed, clear the status and continue normally
|
||||||
return False
|
return False
|
||||||
@@ -2798,6 +2800,22 @@ class DisplayController:
|
|||||||
end_scroll()
|
end_scroll()
|
||||||
return static_screen
|
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:
|
def _note_screen_handover(self) -> None:
|
||||||
"""Tag the frame the first dispatch is about to present.
|
"""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.
|
# Undecidable, so the broken screen's turn left the scroll state alone.
|
||||||
_, turn = _turn(c.events, "broken")
|
_, turn = _turn(c.events, "broken")
|
||||||
assert ("scrolling", False) not in turn
|
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