mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix(display): live games take over within a second, and straight from Vegas (#713)
A game that goes live takes over within about a second (_check_live_takeover in the frame loops and the dwell sleep, throttled to 1 s, never during on-demand, scheduled-off, live_in_ticker or an already-live screen); an interrupted Vegas iteration switches straight to the game; has_live_content() is asked once per plugin per scan. Goldens: live_priority, vegas. Merged with #712: after an interrupted Vegas iteration the WiFi-notice check runs before the live switch (WiFi outranks live). Adds test/test_run_loop_wifi_and_live.py, pinning that a notice and a game arriving during the same screen (1 Hz, 125 Hz, Vegas) show the notice first, then the game, and neither while scheduled off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1145,9 +1145,10 @@ class DisplayController:
|
||||
Also services pending changes (see _service_pending_changes), and
|
||||
returns early when one of them changes what the panel should show --
|
||||
an on-demand start or stop, the display schedule turning the panel
|
||||
on or off, or a WiFi notice arriving -- so the caller can act on it
|
||||
instead of finishing a dwell that could be a minute long (sixty
|
||||
seconds while scheduled off).
|
||||
on or off, a WiFi notice arriving, or a live game taking over
|
||||
(_check_live_takeover) -- so the caller can act on it instead of
|
||||
finishing a dwell that could be a minute long (sixty seconds while
|
||||
scheduled off).
|
||||
"""
|
||||
if duration <= 0:
|
||||
return
|
||||
@@ -1173,6 +1174,7 @@ class DisplayController:
|
||||
display_watchdog.watchdog.beat()
|
||||
self._tick_plugin_updates()
|
||||
self._service_pending_changes()
|
||||
self._check_live_takeover()
|
||||
if (self.current_display_mode != mode
|
||||
or self.is_display_active != display_active
|
||||
or self.on_demand_active != on_demand
|
||||
@@ -2252,15 +2254,22 @@ class DisplayController:
|
||||
preserving order. A plugin registered under several mode keys (the
|
||||
sports plugins register one per league) contributes each live mode once.
|
||||
"""
|
||||
self._last_live_scan = time.monotonic()
|
||||
live = []
|
||||
seen = set()
|
||||
# Asked once per plugin per scan, not once per mode key: a scoreboard
|
||||
# registered under several modes computes the same answer each time.
|
||||
is_live: Dict[int, bool] = {}
|
||||
for mode_name, plugin_instance in self.plugin_modes.items():
|
||||
if not (hasattr(plugin_instance, 'has_live_priority')
|
||||
and hasattr(plugin_instance, 'has_live_content')):
|
||||
continue
|
||||
try:
|
||||
if not (plugin_instance.has_live_priority()
|
||||
and plugin_instance.has_live_content()):
|
||||
key = id(plugin_instance)
|
||||
if key not in is_live:
|
||||
is_live[key] = bool(plugin_instance.has_live_priority()
|
||||
and plugin_instance.has_live_content())
|
||||
if not is_live[key]:
|
||||
continue
|
||||
resolved = []
|
||||
if hasattr(plugin_instance, 'get_live_modes'):
|
||||
@@ -2275,6 +2284,7 @@ class DisplayController:
|
||||
live.append(m)
|
||||
except Exception as e:
|
||||
logger.warning("Error checking live priority for %s: %s", mode_name, e)
|
||||
self._last_live_modes = tuple(live)
|
||||
return live
|
||||
|
||||
def _vegas_keeps_live_in_ticker(self) -> bool:
|
||||
@@ -2311,6 +2321,58 @@ class DisplayController:
|
||||
return self.current_display_mode
|
||||
return live_modes[0]
|
||||
|
||||
#: Shortest gap between live-priority scans made mid-screen. A scan asks
|
||||
#: every live-priority plugin has_live_content(), which the scoreboards
|
||||
#: compute by filtering their game lists. Once a second matches the 1 Hz
|
||||
#: frame loop and is a quarter of the rate Vegas already polls at.
|
||||
LIVE_TAKEOVER_INTERVAL = 1.0
|
||||
|
||||
#: Class-level defaults for controllers built without __init__ (tests).
|
||||
#: When the last live-priority scan of any kind ran (_collect_live_modes).
|
||||
_last_live_scan: Optional[float] = None
|
||||
#: What that scan found.
|
||||
_last_live_modes: Tuple[str, ...] = ()
|
||||
#: A mid-screen takeover chose current_display_mode and it has not been
|
||||
#: shown yet, so the next pass must not advance the live round-robin past it.
|
||||
_live_takeover_unshown: bool = False
|
||||
|
||||
def _check_live_takeover(self) -> None:
|
||||
"""Hand the panel to a live game that started while a screen runs.
|
||||
|
||||
Called from the frame loops and the dwell sleep. Live priority used
|
||||
to be checked only between screens, so a game that went live during
|
||||
a 30 s screen waited for it to end. This switches current_display_mode
|
||||
to the live mode, which ends the screen the way any other mode change
|
||||
does. Throttled to LIVE_TAKEOVER_INTERVAL since the last scan of any
|
||||
kind. Nothing happens while an on-demand session is active, while
|
||||
the panel is scheduled off, while Vegas keeps live content in its
|
||||
ticker, or when the screen showing is already a live mode.
|
||||
|
||||
A live screen is not rescanned at all: live priority put it there,
|
||||
and live games take turns between screens, not mid-screen.
|
||||
"""
|
||||
if self.current_display_mode in self._last_live_modes:
|
||||
return
|
||||
last = self._last_live_scan
|
||||
if last is not None and time.monotonic() - last < self.LIVE_TAKEOVER_INTERVAL:
|
||||
return
|
||||
if self.on_demand_active or not self.is_display_active:
|
||||
return
|
||||
try:
|
||||
coordinator = getattr(self, 'vegas_coordinator', None)
|
||||
if (coordinator is not None and coordinator.is_enabled
|
||||
and self._vegas_keeps_live_in_ticker()):
|
||||
return
|
||||
live_modes = self._collect_live_modes()
|
||||
if not live_modes or self.current_display_mode in live_modes:
|
||||
return
|
||||
self._apply_live_priority(live_modes[0])
|
||||
self._live_takeover_unshown = True
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Called from inside the frame loops; a failure here must not
|
||||
# take the display loop down with it.
|
||||
logger.exception("Error checking for a live-priority takeover")
|
||||
|
||||
# -- Pieces of run() --------------------------------------------------
|
||||
# Extracted from run() unchanged, as the first step of restructuring it
|
||||
# into an Arbiter / ScreenRunner / Sources (docs/RUN_LOOP_REDESIGN.md).
|
||||
@@ -2984,11 +3046,15 @@ class DisplayController:
|
||||
# Skipped when the ticker is keeping live content: switching
|
||||
# the rotation underneath Vegas would move current_mode_index
|
||||
# and stash a resume point for a takeover that never happens.
|
||||
# After a mid-screen takeover (_check_live_takeover) the live
|
||||
# mode is already chosen but not shown yet: don't advance past it.
|
||||
if (not self.on_demand_active
|
||||
and not (self._is_vegas_mode_active()
|
||||
and self._vegas_keeps_live_in_ticker())):
|
||||
live_priority_mode = self._check_live_priority(advance=True)
|
||||
live_priority_mode = self._check_live_priority(
|
||||
advance=not self._live_takeover_unshown)
|
||||
self._apply_live_priority(live_priority_mode)
|
||||
self._live_takeover_unshown = False
|
||||
|
||||
# Vegas scroll mode - continuous ticker across all plugins
|
||||
# Priority: on-demand > wifi-status > live-priority > vegas > normal rotation
|
||||
@@ -3016,7 +3082,19 @@ class DisplayController:
|
||||
# It yielded for a WiFi notice: the next
|
||||
# pass shows it, not a rotation screen
|
||||
# that would outlast a short notice.
|
||||
# Checked before live content: WiFi
|
||||
# outranks live, and step 7 of a later
|
||||
# pass switches to the game.
|
||||
continue
|
||||
# Live content stopped the ticker: switch to
|
||||
# the game now. Step 7 ran before the game
|
||||
# went live, so without this a rotation screen
|
||||
# showed first and the game a screen later.
|
||||
if (not self.on_demand_active
|
||||
and not self._vegas_keeps_live_in_ticker()):
|
||||
live_mode = self._check_live_priority(advance=True)
|
||||
if live_mode:
|
||||
self._apply_live_priority(live_mode)
|
||||
except Exception:
|
||||
logger.exception("Vegas mode error")
|
||||
# Fall through to normal rotation on error
|
||||
@@ -3169,6 +3247,7 @@ class DisplayController:
|
||||
self._tick_plugin_updates()
|
||||
# Throttled: one clock compare between passes.
|
||||
self._service_pending_changes()
|
||||
self._check_live_takeover()
|
||||
|
||||
# Pace to the frame deadline rather than sleeping a flat
|
||||
# interval on top of the work. display() has already
|
||||
@@ -3250,6 +3329,7 @@ class DisplayController:
|
||||
self._send_follower_frame(manager_to_display)
|
||||
|
||||
self._service_pending_changes()
|
||||
self._check_live_takeover()
|
||||
if (self.current_display_mode != active_mode
|
||||
or not self.is_display_active
|
||||
or self._wifi_notice_pending()):
|
||||
|
||||
Reference in New Issue
Block a user