mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-29 03:53:00 +00:00
Compare commits
3 Commits
28a374485f
...
442638dd2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
442638dd2c | ||
|
|
8391832c90 | ||
|
|
c8737d1a6c |
@@ -647,7 +647,11 @@ class ScrollHelper:
|
|||||||
# This ensures smooth scrolling after reset without jumping ahead
|
# This ensures smooth scrolling after reset without jumping ahead
|
||||||
self.last_update_time = now
|
self.last_update_time = now
|
||||||
self.logger.debug("Scroll position reset")
|
self.logger.debug("Scroll position reset")
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Alias for reset_scroll() for convenience."""
|
||||||
|
self.reset_scroll()
|
||||||
|
|
||||||
def set_scrolling_image(self, image: Image.Image) -> None:
|
def set_scrolling_image(self, image: Image.Image) -> None:
|
||||||
"""
|
"""
|
||||||
Set a pre-rendered scrolling image and initialize all required state.
|
Set a pre-rendered scrolling image and initialize all required state.
|
||||||
|
|||||||
@@ -398,6 +398,12 @@ class DisplayController:
|
|||||||
check_interval=10 # Check every 10 frames (~80ms at 125 FPS)
|
check_interval=10 # Check every 10 frames (~80ms at 125 FPS)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set up plugin update tick to keep data fresh during Vegas mode
|
||||||
|
self.vegas_coordinator.set_update_tick(
|
||||||
|
self._tick_plugin_updates_for_vegas,
|
||||||
|
interval=1.0
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Vegas mode coordinator initialized")
|
logger.info("Vegas mode coordinator initialized")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -434,6 +440,38 @@ class DisplayController:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _tick_plugin_updates_for_vegas(self):
|
||||||
|
"""
|
||||||
|
Run scheduled plugin updates and return IDs of plugins that were updated.
|
||||||
|
|
||||||
|
Called periodically by the Vegas coordinator to keep plugin data fresh
|
||||||
|
during Vegas mode. Returns a list of plugin IDs whose data changed so
|
||||||
|
Vegas can refresh their content in the scroll.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of updated plugin IDs, or None if no updates occurred
|
||||||
|
"""
|
||||||
|
if not self.plugin_manager or not hasattr(self.plugin_manager, 'plugin_last_update'):
|
||||||
|
self._tick_plugin_updates()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Snapshot update timestamps before ticking
|
||||||
|
old_times = dict(self.plugin_manager.plugin_last_update)
|
||||||
|
|
||||||
|
# Run the scheduled updates
|
||||||
|
self._tick_plugin_updates()
|
||||||
|
|
||||||
|
# Detect which plugins were actually updated
|
||||||
|
updated = []
|
||||||
|
for plugin_id, new_time in self.plugin_manager.plugin_last_update.items():
|
||||||
|
if new_time > old_times.get(plugin_id, 0.0):
|
||||||
|
updated.append(plugin_id)
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
logger.info("Vegas update tick: %d plugin(s) updated: %s", len(updated), updated)
|
||||||
|
|
||||||
|
return updated or None
|
||||||
|
|
||||||
def _check_schedule(self):
|
def _check_schedule(self):
|
||||||
"""Check if display should be active based on schedule."""
|
"""Check if display should be active based on schedule."""
|
||||||
# Get fresh config from config_service to support hot-reload
|
# Get fresh config from config_service to support hot-reload
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ class VegasModeCoordinator:
|
|||||||
self._interrupt_check: Optional[Callable[[], bool]] = None
|
self._interrupt_check: Optional[Callable[[], bool]] = None
|
||||||
self._interrupt_check_interval: int = 10 # Check every N frames
|
self._interrupt_check_interval: int = 10 # Check every N frames
|
||||||
|
|
||||||
|
# Plugin update tick for keeping data fresh during Vegas mode
|
||||||
|
self._update_tick: Optional[Callable[[], Optional[List[str]]]] = None
|
||||||
|
self._update_tick_interval: float = 1.0 # Tick every 1 second
|
||||||
|
self._update_thread: Optional[threading.Thread] = None
|
||||||
|
self._update_results: Optional[List[str]] = None
|
||||||
|
self._update_results_lock = threading.Lock()
|
||||||
|
self._last_update_tick_time: float = 0.0
|
||||||
|
|
||||||
# Config update tracking
|
# Config update tracking
|
||||||
self._config_version = 0
|
self._config_version = 0
|
||||||
self._pending_config_update = False
|
self._pending_config_update = False
|
||||||
@@ -158,6 +166,25 @@ class VegasModeCoordinator:
|
|||||||
self._interrupt_check = checker
|
self._interrupt_check = checker
|
||||||
self._interrupt_check_interval = max(1, check_interval)
|
self._interrupt_check_interval = max(1, check_interval)
|
||||||
|
|
||||||
|
def set_update_tick(
|
||||||
|
self,
|
||||||
|
callback: Callable[[], Optional[List[str]]],
|
||||||
|
interval: float = 1.0
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set the callback for periodic plugin update ticking during Vegas mode.
|
||||||
|
|
||||||
|
This keeps plugin data fresh while the Vegas render loop is running.
|
||||||
|
The callback should run scheduled plugin updates and return a list of
|
||||||
|
plugin IDs that were actually updated, or None/empty if no updates occurred.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: Callable that returns list of updated plugin IDs or None
|
||||||
|
interval: Seconds between update tick calls (default 1.0)
|
||||||
|
"""
|
||||||
|
self._update_tick = callback
|
||||||
|
self._update_tick_interval = max(0.5, interval)
|
||||||
|
|
||||||
def start(self) -> bool:
|
def start(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Start Vegas mode operation.
|
Start Vegas mode operation.
|
||||||
@@ -210,6 +237,9 @@ class VegasModeCoordinator:
|
|||||||
self.stats['total_runtime_seconds'] += time.time() - self._start_time
|
self.stats['total_runtime_seconds'] += time.time() - self._start_time
|
||||||
self._start_time = None
|
self._start_time = None
|
||||||
|
|
||||||
|
# Wait for in-flight background update before tearing down state
|
||||||
|
self._drain_update_thread()
|
||||||
|
|
||||||
# Cleanup components
|
# Cleanup components
|
||||||
self.render_pipeline.reset()
|
self.render_pipeline.reset()
|
||||||
self.stream_manager.reset()
|
self.stream_manager.reset()
|
||||||
@@ -305,71 +335,83 @@ class VegasModeCoordinator:
|
|||||||
last_fps_log_time = start_time
|
last_fps_log_time = start_time
|
||||||
fps_frame_count = 0
|
fps_frame_count = 0
|
||||||
|
|
||||||
|
self._last_update_tick_time = start_time
|
||||||
|
|
||||||
logger.info("Starting Vegas iteration for %.1fs", duration)
|
logger.info("Starting Vegas iteration for %.1fs", duration)
|
||||||
|
|
||||||
while True:
|
try:
|
||||||
# Check for STATIC mode plugin that should pause scroll
|
while True:
|
||||||
static_plugin = self._check_static_plugin_trigger()
|
# Check for STATIC mode plugin that should pause scroll
|
||||||
if static_plugin:
|
static_plugin = self._check_static_plugin_trigger()
|
||||||
if not self._handle_static_pause(static_plugin):
|
if static_plugin:
|
||||||
# Static pause was interrupted
|
if not self._handle_static_pause(static_plugin):
|
||||||
return False
|
# Static pause was interrupted
|
||||||
# After static pause, skip this segment and continue
|
|
||||||
self.stream_manager.get_next_segment() # Consume the segment
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Run frame
|
|
||||||
if not self.run_frame():
|
|
||||||
# Check why we stopped
|
|
||||||
with self._state_lock:
|
|
||||||
if self._should_stop:
|
|
||||||
return False
|
|
||||||
if self._is_paused:
|
|
||||||
# Paused for live priority - let caller handle
|
|
||||||
return False
|
return False
|
||||||
|
# After static pause, skip this segment and continue
|
||||||
|
self.stream_manager.get_next_segment() # Consume the segment
|
||||||
|
continue
|
||||||
|
|
||||||
# Sleep for frame interval
|
# Run frame
|
||||||
time.sleep(frame_interval)
|
if not self.run_frame():
|
||||||
|
# Check why we stopped
|
||||||
|
with self._state_lock:
|
||||||
|
if self._should_stop:
|
||||||
|
return False
|
||||||
|
if self._is_paused:
|
||||||
|
# Paused for live priority - let caller handle
|
||||||
|
return False
|
||||||
|
|
||||||
# Increment frame count and check for interrupt periodically
|
# Sleep for frame interval
|
||||||
frame_count += 1
|
time.sleep(frame_interval)
|
||||||
fps_frame_count += 1
|
|
||||||
|
|
||||||
# Periodic FPS logging
|
# Increment frame count and check for interrupt periodically
|
||||||
current_time = time.time()
|
frame_count += 1
|
||||||
if current_time - last_fps_log_time >= fps_log_interval:
|
fps_frame_count += 1
|
||||||
fps = fps_frame_count / (current_time - last_fps_log_time)
|
|
||||||
logger.info(
|
|
||||||
"Vegas FPS: %.1f (target: %d, frames: %d)",
|
|
||||||
fps, self.vegas_config.target_fps, fps_frame_count
|
|
||||||
)
|
|
||||||
last_fps_log_time = current_time
|
|
||||||
fps_frame_count = 0
|
|
||||||
|
|
||||||
if (self._interrupt_check and
|
# Periodic FPS logging
|
||||||
frame_count % self._interrupt_check_interval == 0):
|
current_time = time.time()
|
||||||
try:
|
if current_time - last_fps_log_time >= fps_log_interval:
|
||||||
if self._interrupt_check():
|
fps = fps_frame_count / (current_time - last_fps_log_time)
|
||||||
logger.debug(
|
logger.info(
|
||||||
"Vegas interrupted by callback after %d frames",
|
"Vegas FPS: %.1f (target: %d, frames: %d)",
|
||||||
frame_count
|
fps, self.vegas_config.target_fps, fps_frame_count
|
||||||
)
|
)
|
||||||
return False
|
last_fps_log_time = current_time
|
||||||
except Exception:
|
fps_frame_count = 0
|
||||||
# Log but don't let interrupt check errors stop Vegas
|
|
||||||
logger.exception("Interrupt check failed")
|
|
||||||
|
|
||||||
# Check elapsed time
|
# Periodic plugin update tick to keep data fresh (non-blocking)
|
||||||
elapsed = time.time() - start_time
|
self._drive_background_updates()
|
||||||
if elapsed >= duration:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check for cycle completion
|
if (self._interrupt_check and
|
||||||
if self.render_pipeline.is_cycle_complete():
|
frame_count % self._interrupt_check_interval == 0):
|
||||||
break
|
try:
|
||||||
|
if self._interrupt_check():
|
||||||
|
logger.debug(
|
||||||
|
"Vegas interrupted by callback after %d frames",
|
||||||
|
frame_count
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
# Log but don't let interrupt check errors stop Vegas
|
||||||
|
logger.exception("Interrupt check failed")
|
||||||
|
|
||||||
logger.info("Vegas iteration completed after %.1fs", time.time() - start_time)
|
# Check elapsed time
|
||||||
return True
|
elapsed = time.time() - start_time
|
||||||
|
if elapsed >= duration:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for cycle completion
|
||||||
|
if self.render_pipeline.is_cycle_complete():
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info("Vegas iteration completed after %.1fs", time.time() - start_time)
|
||||||
|
return True
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Ensure background update thread finishes before the main loop
|
||||||
|
# resumes its own _tick_plugin_updates() calls, preventing concurrent
|
||||||
|
# run_scheduled_updates() execution.
|
||||||
|
self._drain_update_thread()
|
||||||
|
|
||||||
def _check_live_priority(self) -> bool:
|
def _check_live_priority(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -458,6 +500,71 @@ class VegasModeCoordinator:
|
|||||||
if self._pending_config is None:
|
if self._pending_config is None:
|
||||||
self._pending_config_update = False
|
self._pending_config_update = False
|
||||||
|
|
||||||
|
def _run_update_tick_background(self) -> None:
|
||||||
|
"""Run the plugin update tick in a background thread.
|
||||||
|
|
||||||
|
Stores results for the render loop to pick up on its next iteration,
|
||||||
|
so the scroll never blocks on API calls.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
updated_plugins = self._update_tick()
|
||||||
|
if updated_plugins:
|
||||||
|
with self._update_results_lock:
|
||||||
|
# Accumulate rather than replace to avoid losing notifications
|
||||||
|
# if a previous result hasn't been picked up yet
|
||||||
|
if self._update_results is None:
|
||||||
|
self._update_results = updated_plugins
|
||||||
|
else:
|
||||||
|
self._update_results.extend(updated_plugins)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Background plugin update tick failed")
|
||||||
|
|
||||||
|
def _drain_update_thread(self, timeout: float = 2.0) -> None:
|
||||||
|
"""Wait for any in-flight background update thread to finish.
|
||||||
|
|
||||||
|
Called when transitioning out of Vegas mode so the main-loop
|
||||||
|
``_tick_plugin_updates`` call doesn't race with a still-running
|
||||||
|
background thread.
|
||||||
|
"""
|
||||||
|
if self._update_thread is not None and self._update_thread.is_alive():
|
||||||
|
self._update_thread.join(timeout=timeout)
|
||||||
|
if self._update_thread.is_alive():
|
||||||
|
logger.warning(
|
||||||
|
"Background update thread did not finish within %.1fs", timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _drive_background_updates(self) -> None:
|
||||||
|
"""Collect finished background update results and launch new ticks.
|
||||||
|
|
||||||
|
Safe to call from both the main render loop and the static-pause
|
||||||
|
wait loop so that plugin data stays fresh regardless of which
|
||||||
|
code path is active.
|
||||||
|
"""
|
||||||
|
# 1. Collect results from a previously completed background update
|
||||||
|
with self._update_results_lock:
|
||||||
|
ready_results = self._update_results
|
||||||
|
self._update_results = None
|
||||||
|
if ready_results:
|
||||||
|
for pid in ready_results:
|
||||||
|
self.mark_plugin_updated(pid)
|
||||||
|
|
||||||
|
# 2. Kick off a new background update if interval elapsed and none running
|
||||||
|
current_time = time.time()
|
||||||
|
if (self._update_tick and
|
||||||
|
current_time - self._last_update_tick_time >= self._update_tick_interval):
|
||||||
|
thread_alive = (
|
||||||
|
self._update_thread is not None
|
||||||
|
and self._update_thread.is_alive()
|
||||||
|
)
|
||||||
|
if not thread_alive:
|
||||||
|
self._last_update_tick_time = current_time
|
||||||
|
self._update_thread = threading.Thread(
|
||||||
|
target=self._run_update_tick_background,
|
||||||
|
daemon=True,
|
||||||
|
name="vegas-update-tick",
|
||||||
|
)
|
||||||
|
self._update_thread.start()
|
||||||
|
|
||||||
def mark_plugin_updated(self, plugin_id: str) -> None:
|
def mark_plugin_updated(self, plugin_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
Notify that a plugin's data has been updated.
|
Notify that a plugin's data has been updated.
|
||||||
@@ -576,6 +683,9 @@ class VegasModeCoordinator:
|
|||||||
logger.info("Static pause interrupted by live priority")
|
logger.info("Static pause interrupted by live priority")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Keep plugin data fresh during static pause
|
||||||
|
self._drive_background_updates()
|
||||||
|
|
||||||
# Sleep in small increments to remain responsive
|
# Sleep in small increments to remain responsive
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|||||||
@@ -408,7 +408,10 @@ class PluginAdapter:
|
|||||||
original_image = self.display_manager.image.copy()
|
original_image = self.display_manager.image.copy()
|
||||||
logger.info("[%s] Fallback: saved original display state", plugin_id)
|
logger.info("[%s] Fallback: saved original display state", plugin_id)
|
||||||
|
|
||||||
# Ensure plugin has fresh data before capturing
|
# Lightweight in-memory data refresh before capturing.
|
||||||
|
# Full update() is intentionally skipped here — the background
|
||||||
|
# update tick in the Vegas coordinator handles periodic API
|
||||||
|
# refreshes so we don't block the content-fetch thread.
|
||||||
has_update_data = hasattr(plugin, 'update_data')
|
has_update_data = hasattr(plugin, 'update_data')
|
||||||
logger.info("[%s] Fallback: has update_data=%s", plugin_id, has_update_data)
|
logger.info("[%s] Fallback: has update_data=%s", plugin_id, has_update_data)
|
||||||
if has_update_data:
|
if has_update_data:
|
||||||
@@ -582,6 +585,28 @@ class PluginAdapter:
|
|||||||
else:
|
else:
|
||||||
self._content_cache.clear()
|
self._content_cache.clear()
|
||||||
|
|
||||||
|
def invalidate_plugin_scroll_cache(self, plugin: 'BasePlugin', plugin_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Clear a plugin's scroll_helper cache so Vegas re-fetches fresh visuals.
|
||||||
|
|
||||||
|
Uses scroll_helper.clear_cache() to reset all cached state (cached_image,
|
||||||
|
cached_array, total_scroll_width, scroll_position, etc.) — not just the
|
||||||
|
image. Without this, plugins that use scroll_helper (stocks, news,
|
||||||
|
odds-ticker, etc.) would keep serving stale scroll images even after
|
||||||
|
their data refreshes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin: Plugin instance
|
||||||
|
plugin_id: Plugin identifier
|
||||||
|
"""
|
||||||
|
scroll_helper = getattr(plugin, 'scroll_helper', None)
|
||||||
|
if scroll_helper is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if getattr(scroll_helper, 'cached_image', None) is not None:
|
||||||
|
scroll_helper.clear_cache()
|
||||||
|
logger.debug("[%s] Cleared scroll_helper cache", plugin_id)
|
||||||
|
|
||||||
def get_content_type(self, plugin: 'BasePlugin', plugin_id: str) -> str:
|
def get_content_type(self, plugin: 'BasePlugin', plugin_id: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get the type of content a plugin provides.
|
Get the type of content a plugin provides.
|
||||||
|
|||||||
@@ -217,6 +217,15 @@ class StreamManager:
|
|||||||
refreshed_segments = {}
|
refreshed_segments = {}
|
||||||
for plugin_id in updated_plugins:
|
for plugin_id in updated_plugins:
|
||||||
self.plugin_adapter.invalidate_cache(plugin_id)
|
self.plugin_adapter.invalidate_cache(plugin_id)
|
||||||
|
|
||||||
|
# Clear the plugin's scroll_helper cache so the visual is rebuilt
|
||||||
|
# from fresh data (affects stocks, news, odds-ticker, etc.)
|
||||||
|
plugin = None
|
||||||
|
if hasattr(self.plugin_manager, 'plugins'):
|
||||||
|
plugin = self.plugin_manager.plugins.get(plugin_id)
|
||||||
|
if plugin:
|
||||||
|
self.plugin_adapter.invalidate_plugin_scroll_cache(plugin, plugin_id)
|
||||||
|
|
||||||
segment = self._fetch_plugin_content(plugin_id)
|
segment = self._fetch_plugin_content(plugin_id)
|
||||||
if segment:
|
if segment:
|
||||||
refreshed_segments[plugin_id] = segment
|
refreshed_segments[plugin_id] = segment
|
||||||
|
|||||||
@@ -5053,7 +5053,7 @@ sys.exit(proc.returncode)
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['python3', wrapper_path],
|
[sys.executable, wrapper_path],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
@@ -5114,7 +5114,7 @@ sys.exit(proc.returncode)
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['python3', wrapper_path],
|
[sys.executable, wrapper_path],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
@@ -5209,7 +5209,7 @@ sys.exit(proc.returncode)
|
|||||||
else:
|
else:
|
||||||
# Simple script execution
|
# Simple script execution
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['python3', str(script_file)],
|
[sys.executable, str(script_file)],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
@@ -5320,7 +5320,7 @@ sys.exit(proc.returncode)
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['python3', wrapper_path],
|
[sys.executable, wrapper_path],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
@@ -5426,7 +5426,7 @@ def authenticate_ytm():
|
|||||||
|
|
||||||
# Run the authentication script
|
# Run the authentication script
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['python3', str(auth_script)],
|
[sys.executable, str(auth_script)],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
|
|||||||
Reference in New Issue
Block a user