added dynamic display durations to scrolling managers

This commit is contained in:
Chuck
2025-08-09 17:15:14 -05:00
parent 8654dd44e6
commit b0941641c7
7 changed files with 700 additions and 9 deletions

View File

@@ -462,6 +462,48 @@ class DisplayController:
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)
# Handle dynamic duration for stocks
if mode_key == 'stocks' and self.stocks:
try:
dynamic_duration = self.stocks.get_dynamic_duration()
# Only log if duration has changed or we haven't logged this duration yet
if not hasattr(self, '_last_logged_duration') or self._last_logged_duration != dynamic_duration:
logger.info(f"Using dynamic duration for stocks: {dynamic_duration} seconds")
self._last_logged_duration = dynamic_duration
return dynamic_duration
except Exception as e:
logger.error(f"Error getting dynamic duration for stocks: {e}")
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)
# Handle dynamic duration for stock_news
if mode_key == 'stock_news' and self.news:
try:
dynamic_duration = self.news.get_dynamic_duration()
# Only log if duration has changed or we haven't logged this duration yet
if not hasattr(self, '_last_logged_duration') or self._last_logged_duration != dynamic_duration:
logger.info(f"Using dynamic duration for stock_news: {dynamic_duration} seconds")
self._last_logged_duration = dynamic_duration
return dynamic_duration
except Exception as e:
logger.error(f"Error getting dynamic duration for stock_news: {e}")
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)
# Handle dynamic duration for odds_ticker
if mode_key == 'odds_ticker' and self.odds_ticker:
try:
dynamic_duration = self.odds_ticker.get_dynamic_duration()
# Only log if duration has changed or we haven't logged this duration yet
if not hasattr(self, '_last_logged_duration') or self._last_logged_duration != dynamic_duration:
logger.info(f"Using dynamic duration for odds_ticker: {dynamic_duration} seconds")
self._last_logged_duration = dynamic_duration
return dynamic_duration
except Exception as e:
logger.error(f"Error getting dynamic duration for odds_ticker: {e}")
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)
# Simplify weather key handling
if mode_key.startswith('weather_'):
return self.display_durations.get(mode_key, 15)

View File

@@ -88,6 +88,14 @@ class OddsTickerManager:
self.broadcast_logo_max_width_ratio = self.odds_ticker_config.get('broadcast_logo_max_width_ratio', 0.8)
self.request_timeout = self.odds_ticker_config.get('request_timeout', 30)
# Dynamic duration settings
self.dynamic_duration_enabled = self.odds_ticker_config.get('dynamic_duration', True)
self.min_duration = self.odds_ticker_config.get('min_duration', 30)
self.max_duration = self.odds_ticker_config.get('max_duration', 300)
self.duration_buffer = self.odds_ticker_config.get('duration_buffer', 0.1)
self.dynamic_duration = 60 # Default duration in seconds
self.total_scroll_width = 0 # Track total width for dynamic duration calculation
# Initialize managers
self.cache_manager = CacheManager()
self.odds_manager = OddsManager(self.cache_manager, ConfigManager())
@@ -846,6 +854,10 @@ class OddsTickerManager:
for y in range(height):
self.ticker_image.putpixel((bar_x, y), (255, 255, 255))
current_x += gap_width
# Calculate total scroll width for dynamic duration
self.total_scroll_width = total_width
self.calculate_dynamic_duration()
def _draw_text_with_outline(self, draw: ImageDraw.Draw, text: str, position: tuple, font: ImageFont.FreeTypeFont,
fill: tuple = (255, 255, 255), outline_color: tuple = (0, 0, 0)) -> None:
@@ -857,6 +869,67 @@ class OddsTickerManager:
# Draw main text
draw.text((x, y), text, font=font, fill=fill)
def calculate_dynamic_duration(self):
"""Calculate the exact time needed to display all odds ticker content"""
# If dynamic duration is disabled, use fixed duration from config
if not self.dynamic_duration_enabled:
self.dynamic_duration = self.odds_ticker_config.get('display_duration', 60)
logger.debug(f"Dynamic duration disabled, using fixed duration: {self.dynamic_duration}s")
return
if not self.total_scroll_width:
self.dynamic_duration = self.min_duration # Use configured minimum
return
try:
# Get display width (assume full width of display)
display_width = getattr(self.display_manager, 'matrix', None)
if display_width:
display_width = display_width.width
else:
display_width = 128 # Default to 128 if not available
# Calculate total scroll distance needed
# Text needs to scroll from right edge to completely off left edge
total_scroll_distance = display_width + self.total_scroll_width
# Calculate time based on scroll speed and delay
# scroll_speed = pixels per frame, scroll_delay = seconds per frame
frames_needed = total_scroll_distance / self.scroll_speed
total_time = frames_needed * self.scroll_delay
# Add buffer time for smooth cycling (configurable %)
buffer_time = total_time * self.duration_buffer
calculated_duration = int(total_time + buffer_time)
# Apply configured min/max limits
if calculated_duration < self.min_duration:
self.dynamic_duration = self.min_duration
logger.debug(f"Duration capped to minimum: {self.min_duration}s")
elif calculated_duration > self.max_duration:
self.dynamic_duration = self.max_duration
logger.debug(f"Duration capped to maximum: {self.max_duration}s")
else:
self.dynamic_duration = calculated_duration
logger.debug(f"Odds ticker dynamic duration calculation:")
logger.debug(f" Display width: {display_width}px")
logger.debug(f" Text width: {self.total_scroll_width}px")
logger.debug(f" Total scroll distance: {total_scroll_distance}px")
logger.debug(f" Frames needed: {frames_needed:.1f}")
logger.debug(f" Base time: {total_time:.2f}s")
logger.debug(f" Buffer time: {buffer_time:.2f}s ({self.duration_buffer*100}%)")
logger.debug(f" Calculated duration: {calculated_duration}s")
logger.debug(f" Final duration: {self.dynamic_duration}s")
except Exception as e:
logger.error(f"Error calculating dynamic duration: {e}")
self.dynamic_duration = self.min_duration # Use configured minimum as fallback
def get_dynamic_duration(self) -> int:
"""Get the calculated dynamic duration for display"""
return self.dynamic_duration
def update(self):
"""Update odds ticker data."""
logger.debug("Entering update method")

View File

@@ -39,6 +39,14 @@ class StockManager:
# Get chart toggle setting from config
self.toggle_chart = self.stocks_config.get('toggle_chart', False)
# Dynamic duration settings
self.dynamic_duration_enabled = self.stocks_config.get('dynamic_duration', True)
self.min_duration = self.stocks_config.get('min_duration', 30)
self.max_duration = self.stocks_config.get('max_duration', 300)
self.duration_buffer = self.stocks_config.get('duration_buffer', 0.1)
self.dynamic_duration = 60 # Default duration in seconds
self.total_scroll_width = 0 # Track total width for dynamic duration calculation
# Initialize frame rate tracking
self.frame_count = 0
self.last_frame_time = time.time()
@@ -659,6 +667,10 @@ class StockManager:
self.cached_text_image = full_image
self.scroll_position = 0
self.last_update = time.time()
# Calculate total scroll width for dynamic duration
self.total_scroll_width = total_width
self.calculate_dynamic_duration()
# Clear the display if requested
if force_clear:
@@ -693,6 +705,63 @@ class StockManager:
return True
return False
def calculate_dynamic_duration(self):
"""Calculate the exact time needed to display all stocks"""
# If dynamic duration is disabled, use fixed duration from config
if not self.dynamic_duration_enabled:
self.dynamic_duration = self.stocks_config.get('fixed_duration', 60)
logger.debug(f"Dynamic duration disabled, using fixed duration: {self.dynamic_duration}s")
return
if not self.total_scroll_width:
self.dynamic_duration = self.min_duration # Use configured minimum
return
try:
# Get display width (assume full width of display)
display_width = getattr(self.display_manager, 'width', 128) # Default to 128 if not available
# Calculate total scroll distance needed
# Text needs to scroll from right edge to completely off left edge
total_scroll_distance = display_width + self.total_scroll_width
# Calculate time based on scroll speed and delay
# scroll_speed = pixels per frame, scroll_delay = seconds per frame
frames_needed = total_scroll_distance / self.scroll_speed
total_time = frames_needed * self.scroll_delay
# Add buffer time for smooth cycling (configurable %)
buffer_time = total_time * self.duration_buffer
calculated_duration = int(total_time + buffer_time)
# Apply configured min/max limits
if calculated_duration < self.min_duration:
self.dynamic_duration = self.min_duration
logger.debug(f"Duration capped to minimum: {self.min_duration}s")
elif calculated_duration > self.max_duration:
self.dynamic_duration = self.max_duration
logger.debug(f"Duration capped to maximum: {self.max_duration}s")
else:
self.dynamic_duration = calculated_duration
logger.debug(f"Stock dynamic duration calculation:")
logger.debug(f" Display width: {display_width}px")
logger.debug(f" Text width: {self.total_scroll_width}px")
logger.debug(f" Total scroll distance: {total_scroll_distance}px")
logger.debug(f" Frames needed: {frames_needed:.1f}")
logger.debug(f" Base time: {total_time:.2f}s")
logger.debug(f" Buffer time: {buffer_time:.2f}s ({self.duration_buffer*100}%)")
logger.debug(f" Calculated duration: {calculated_duration}s")
logger.debug(f" Final duration: {self.dynamic_duration}s")
except Exception as e:
logger.error(f"Error calculating dynamic duration: {e}")
self.dynamic_duration = self.min_duration # Use configured minimum as fallback
def get_dynamic_duration(self) -> int:
"""Get the calculated dynamic duration for display"""
return self.dynamic_duration
def set_toggle_chart(self, enabled: bool):
"""Enable or disable chart display in the scrolling ticker."""

View File

@@ -41,6 +41,14 @@ class StockNewsManager:
self.max_headlines_per_symbol = self.stock_news_config.get('max_headlines_per_symbol', 1)
self.headlines_per_rotation = self.stock_news_config.get('headlines_per_rotation', 2)
# Dynamic duration settings
self.dynamic_duration_enabled = self.stock_news_config.get('dynamic_duration', True)
self.min_duration = self.stock_news_config.get('min_duration', 30)
self.max_duration = self.stock_news_config.get('max_duration', 300)
self.duration_buffer = self.stock_news_config.get('duration_buffer', 0.1)
self.dynamic_duration = 60 # Default duration in seconds
self.total_scroll_width = 0 # Track total width for dynamic duration calculation
# Log the actual values being used
logger.info(f"Scroll settings - Speed: {self.scroll_speed} pixels/frame, Delay: {self.scroll_delay*1000:.2f}ms")
logger.info(f"Headline settings - Max per symbol: {self.max_headlines_per_symbol}, Per rotation: {self.headlines_per_rotation}")
@@ -362,6 +370,10 @@ class StockNewsManager:
self.scroll_position = 0
self.background_image = None # Clear the background image
# Calculate total scroll width for dynamic duration
self.total_scroll_width = self.cached_text_image.width
self.calculate_dynamic_duration()
# Move to next rotation for next time
self.current_rotation_index += 1
else:
@@ -396,6 +408,63 @@ class StockNewsManager:
self.cached_text_image = None
return True
def calculate_dynamic_duration(self):
"""Calculate the exact time needed to display all news headlines"""
# If dynamic duration is disabled, use fixed duration from config
if not self.dynamic_duration_enabled:
self.dynamic_duration = self.stock_news_config.get('fixed_duration', 60)
logger.debug(f"Dynamic duration disabled, using fixed duration: {self.dynamic_duration}s")
return
if not self.total_scroll_width:
self.dynamic_duration = self.min_duration # Use configured minimum
return
try:
# Get display width (assume full width of display)
display_width = getattr(self.display_manager, 'width', 128) # Default to 128 if not available
# Calculate total scroll distance needed
# Text needs to scroll from right edge to completely off left edge
total_scroll_distance = display_width + self.total_scroll_width
# Calculate time based on scroll speed and delay
# scroll_speed = pixels per frame, scroll_delay = seconds per frame
frames_needed = total_scroll_distance / self.scroll_speed
total_time = frames_needed * self.scroll_delay
# Add buffer time for smooth cycling (configurable %)
buffer_time = total_time * self.duration_buffer
calculated_duration = int(total_time + buffer_time)
# Apply configured min/max limits
if calculated_duration < self.min_duration:
self.dynamic_duration = self.min_duration
logger.debug(f"Duration capped to minimum: {self.min_duration}s")
elif calculated_duration > self.max_duration:
self.dynamic_duration = self.max_duration
logger.debug(f"Duration capped to maximum: {self.max_duration}s")
else:
self.dynamic_duration = calculated_duration
logger.debug(f"Stock news dynamic duration calculation:")
logger.debug(f" Display width: {display_width}px")
logger.debug(f" Text width: {self.total_scroll_width}px")
logger.debug(f" Total scroll distance: {total_scroll_distance}px")
logger.debug(f" Frames needed: {frames_needed:.1f}")
logger.debug(f" Base time: {total_time:.2f}s")
logger.debug(f" Buffer time: {buffer_time:.2f}s ({self.duration_buffer*100}%)")
logger.debug(f" Calculated duration: {calculated_duration}s")
logger.debug(f" Final duration: {self.dynamic_duration}s")
except Exception as e:
logger.error(f"Error calculating dynamic duration: {e}")
self.dynamic_duration = self.min_duration # Use configured minimum as fallback
def get_dynamic_duration(self) -> int:
"""Get the calculated dynamic duration for display"""
return self.dynamic_duration
# Calculate the visible portion
# Handle wrap-around drawing
visible_end = self.scroll_position + width