diff --git a/src/display_controller.py b/src/display_controller.py index 4ba8f6c4..fe1f55e2 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -151,10 +151,12 @@ class DisplayController: self.milb_live = MiLBLiveManager(self.config, self.display_manager, self.cache_manager) if milb_display_modes.get('milb_live', True) else None self.milb_recent = MiLBRecentManager(self.config, self.display_manager, self.cache_manager) if milb_display_modes.get('milb_recent', True) else None self.milb_upcoming = MiLBUpcomingManager(self.config, self.display_manager, self.cache_manager) if milb_display_modes.get('milb_upcoming', True) else None + logger.info(f"MiLB managers initialized - live: {self.milb_live is not None}, recent: {self.milb_recent is not None}, upcoming: {self.milb_upcoming is not None}") else: self.milb_live = None self.milb_recent = None self.milb_upcoming = None + logger.info("MiLB managers disabled") logger.info("MiLB managers initialized in %.3f seconds", time.time() - milb_time) # Initialize Soccer managers if enabled @@ -1137,6 +1139,7 @@ class DisplayController: manager_to_display = self.milb_recent elif self.current_display_mode == 'milb_upcoming' and self.milb_upcoming: manager_to_display = self.milb_upcoming + logger.info("Set manager_to_display to milb_upcoming") elif self.current_display_mode == 'soccer_recent' and self.soccer_recent: manager_to_display = self.soccer_recent elif self.current_display_mode == 'soccer_upcoming' and self.soccer_upcoming: @@ -1169,6 +1172,8 @@ class DisplayController: logger.info(f"Showing {self.current_display_mode}") self._last_logged_mode = self.current_display_mode + logger.info(f"manager_to_display is {type(manager_to_display).__name__ if manager_to_display else 'None'}") + if self.current_display_mode == 'music' and self.music_manager: # Call MusicManager's display method self.music_manager.display(force_clear=self.force_clear) @@ -1230,6 +1235,7 @@ class DisplayController: # Special handling for live managers that need update before display if self.current_display_mode.endswith('_live') and hasattr(manager_to_display, 'update'): manager_to_display.update() + logger.info(f"Calling display method for {self.current_display_mode}") manager_to_display.display(force_clear=self.force_clear) else: logger.warning(f"Manager {type(manager_to_display).__name__} for mode {self.current_display_mode} does not have a standard 'display' method.") diff --git a/src/display_manager.py b/src/display_manager.py index a212da13..3ad61318 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -641,6 +641,11 @@ class DisplayManager: except Exception: # Fallback to direct save if replace not supported self.image.save(self._snapshot_path, format='PNG') + # Try to make the snapshot world-readable so the web UI can read it regardless of user + try: + os.chmod(self._snapshot_path, 0o644) + except Exception: + pass self._last_snapshot_ts = now except Exception as e: # Snapshot failures should never break display; log at debug to avoid noise diff --git a/src/milb_manager.py b/src/milb_manager.py index 0c6406dc..d5c4425f 100644 --- a/src/milb_manager.py +++ b/src/milb_manager.py @@ -211,6 +211,7 @@ class BaseMiLBManager: def _create_game_display(self, game_data: Dict[str, Any]) -> Image.Image: """Create a display image for an MiLB game with team logos, score, and game state.""" + self.logger.info(f"[MiLB] Creating game display for: {game_data.get('away_team')} @ {game_data.get('home_team')}") width = self.display_manager.matrix.width height = self.display_manager.matrix.height image = Image.new('RGB', (width, height), color=(0, 0, 0)) @@ -417,6 +418,7 @@ class BaseMiLBManager: def _fetch_milb_api_data(self, use_cache: bool = True) -> Dict[str, Any]: """Fetch MiLB game data from the MLB Stats API.""" + self.logger.info("[MiLB] _fetch_milb_api_data called") cache_key = "milb_live_api_data" if use_cache: cached_data = self.cache_manager.get_with_auto_strategy(cache_key) @@ -616,6 +618,7 @@ class BaseMiLBManager: self.cache_manager.set(cache_key, all_games) else: self.logger.error(f"[MiLB] Cannot cache invalid data type: {type(all_games)}") + self.logger.info(f"[MiLB] Returning {len(all_games)} games from API") return all_games except Exception as e: @@ -1531,6 +1534,7 @@ class MiLBUpcomingManager(BaseMiLBManager): def update(self): """Update upcoming games data.""" + self.logger.info("[MiLB] Update method called") current_time = time.time() # Add a check to see if the manager is enabled @@ -1602,6 +1606,7 @@ class MiLBUpcomingManager(BaseMiLBManager): new_upcoming_games = [] self.logger.info(f"[MiLB] Processing {len(games)} games for upcoming games...") + self.logger.info(f"[MiLB] Games keys: {list(games.keys()) if games else 'None'}") now_utc = datetime.now(timezone.utc) for game_id, game in games.items(): @@ -1646,6 +1651,7 @@ class MiLBUpcomingManager(BaseMiLBManager): # Sort by game time (soonest first) and limit to upcoming_games_to_show new_upcoming_games.sort(key=lambda x: x.get('start_time', '')) new_upcoming_games = new_upcoming_games[:self.upcoming_games_to_show] + self.logger.info(f"[MiLB] Found {len(new_upcoming_games)} upcoming games after processing") # Compare new list with old list to see if an update is needed if self.upcoming_games != new_upcoming_games: @@ -1660,8 +1666,10 @@ class MiLBUpcomingManager(BaseMiLBManager): self.current_game_index = 0 self.current_game = self.upcoming_games[0] self.last_game_switch = current_time + self.logger.info(f"[MiLB] Set current game to: {self.current_game.get('away_team')} @ {self.current_game.get('home_team')}") else: self.current_game = None # No upcoming games + self.logger.info("[MiLB] No upcoming games, cleared current game") self.last_update = current_time @@ -1671,7 +1679,7 @@ class MiLBUpcomingManager(BaseMiLBManager): def display(self, force_clear: bool = False): """Display upcoming games.""" - self.logger.debug(f"[MiLB] Display called with {len(self.upcoming_games)} upcoming games") + self.logger.info(f"[MiLB] Display called with {len(self.upcoming_games)} upcoming games") if not self.upcoming_games: current_time = time.time() if current_time - self.last_warning_time > self.warning_cooldown: diff --git a/web_interface_v2.py b/web_interface_v2.py index 7b47d9ef..6b511ce5 100644 --- a/web_interface_v2.py +++ b/web_interface_v2.py @@ -63,12 +63,12 @@ class DisplayMonitor: # Prefer service-provided snapshot if available (works when ledmatrix service is running) if os.path.exists(snapshot_path): # Read atomically by reopening; ignore partials by skipping this frame - img_bytes = None try: with open(snapshot_path, 'rb') as f: img_bytes = f.read() except Exception: img_bytes = None + if img_bytes: img_str = base64.b64encode(img_bytes).decode() # If we can infer dimensions from display_manager, include them; else leave 0 @@ -84,16 +84,8 @@ class DisplayMonitor: # Yield and continue to next frame socketio.sleep(0.1) continue - # If we can infer dimensions from display_manager, include them; else leave 0 - width = display_manager.width if display_manager else 0 - height = display_manager.height if display_manager else 0 - current_display_data = { - 'image': img_str, - 'width': width, - 'height': height, - 'timestamp': time.time() - } - socketio.emit('display_update', current_display_data) + # If snapshot exists but couldn't be read (partial write/permissions), skip this frame + # and try again on next loop rather than emitting an invalid payload. elif display_manager and hasattr(display_manager, 'image'): # Fallback to in-process manager image img_buffer = io.BytesIO()