diff --git a/config/config.json b/config/config.json index d97b9a44..d4d18d71 100644 --- a/config/config.json +++ b/config/config.json @@ -57,7 +57,7 @@ "enabled": true, "update_interval": 300, "scroll_speed": 1, - "scroll_delay": 0.0001, + "scroll_delay": 0.001, "max_headlines_per_symbol": 1, "headlines_per_rotation": 2 } diff --git a/src/display_controller.py b/src/display_controller.py index 6b6c7e00..ddc555ee 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -113,12 +113,13 @@ class DisplayController: logger.info(f"Switching display to: {self.current_display} {self.weather_mode if self.current_display == 'weather' else ''}") self.last_switch = current_time self.force_clear = True - self.display_manager.clear() # Ensure clean transition + self.display_manager.clear() # Display current screen try: if self.current_display == 'clock' and self.config.get('clock', {}).get('enabled', False): self.clock.display_time(force_clear=self.force_clear) + time.sleep(self.update_interval) elif self.current_display == 'weather' and self.config.get('weather', {}).get('enabled', False): if self.weather_mode == 'current': self.weather.display_weather(force_clear=self.force_clear) @@ -126,9 +127,12 @@ class DisplayController: self.weather.display_hourly_forecast(force_clear=self.force_clear) else: # daily self.weather.display_daily_forecast(force_clear=self.force_clear) + time.sleep(self.update_interval) elif self.current_display == 'stocks' and self.config.get('stocks', {}).get('enabled', False): self.stocks.display_stocks(force_clear=self.force_clear) + time.sleep(self.update_interval) elif self.current_display == 'stock_news' and self.config.get('stock_news', {}).get('enabled', False): + # For news, we want to update as fast as possible without delay self.news.display_news() except Exception as e: logger.error(f"Error updating display: {e}") @@ -138,9 +142,6 @@ class DisplayController: # Reset force clear flag after use self.force_clear = False - # Sleep between updates - time.sleep(self.update_interval) - except KeyboardInterrupt: print("\nDisplay stopped by user") finally: diff --git a/src/display_manager.py b/src/display_manager.py index 85d6cbbc..b4b47ca6 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -36,18 +36,18 @@ class DisplayManager: options.parallel = hardware_config.get('parallel', 1) options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm') - # Optimize display settings for chained panels + # Balance performance and stability options.brightness = 100 - options.pwm_bits = 11 - options.pwm_lsb_nanoseconds = 200 # Increased for better stability + options.pwm_bits = 10 # Increased from 8 for better color depth + options.pwm_lsb_nanoseconds = 150 # Increased for better stability options.led_rgb_sequence = 'RGB' options.pixel_mapper_config = '' options.row_address_type = 0 options.multiplexing = 0 - options.disable_hardware_pulsing = False # Enable hardware pulsing for better sync + options.disable_hardware_pulsing = False # Re-enable hardware pulsing for stability options.show_refresh_rate = False - options.limit_refresh_rate_hz = 60 # Reduced refresh rate for stability - options.gpio_slowdown = 2 # Increased slowdown for better stability + options.limit_refresh_rate_hz = 90 # Reduced from 120Hz for better stability + options.gpio_slowdown = 2 # Increased for better stability # Initialize the matrix self.matrix = RGBMatrix(options=options) @@ -94,14 +94,11 @@ class DisplayManager: # Copy the current image to the offscreen canvas self.offscreen_canvas.SetImage(self.image) - # Wait for the next vsync before swapping - self.matrix.SwapOnVSync(self.offscreen_canvas) + # Swap buffers immediately + self.matrix.SwapOnVSync(self.offscreen_canvas, False) # Swap our canvas references self.offscreen_canvas, self.current_canvas = self.current_canvas, self.offscreen_canvas - - # Small delay to ensure stable refresh - time.sleep(0.001) except Exception as e: logger.error(f"Error updating display: {e}") diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index 68b6a73c..1e37b3d1 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -26,6 +26,9 @@ class StockNewsManager: self.news_data = {} self.current_news_group = 0 # Track which group of headlines we're showing self.scroll_position = 0 + self.cached_text_image = None # Cache for the text image + self.cached_text = None # Cache for the text string + # Get scroll settings from config with faster defaults self.scroll_speed = self.stock_news_config.get('scroll_speed', 1) @@ -205,39 +208,60 @@ class StockNewsManager: separator = " - " # Visual separator between news items news_text = separator.join(news_texts) - # Clear the display - self.display_manager.clear() + # Only create new text image if the text has changed + if news_text != self.cached_text: + self.cached_text = news_text + self.cached_text_image = self._create_text_image(news_text) + self.scroll_position = 0 # Reset scroll position for new text - # Calculate text width for scrolling - bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) - text_width = bbox[2] - bbox[0] - - # Calculate scroll position + if not self.cached_text_image: + return + + text_width = self.cached_text_image.width + text_height = self.cached_text_image.height + display_width = self.display_manager.matrix.width total_width = text_width + display_width # Update scroll position self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width - - # Draw the text at the current scroll position - self.display_manager.draw_text( - news_text, - x=display_width - self.scroll_position, - y=None, # Center vertically - color=(255, 255, 255), - small_font=True - ) - - # Update the display - self.display_manager.update_display() + + # Calculate the visible portion of the text + visible_width = min(display_width, text_width - self.scroll_position) + if visible_width > 0: + # Create a new blank image for this frame + frame_image = Image.new('RGB', (display_width, text_height), (0, 0, 0)) + + # Crop and paste in one operation + if self.scroll_position + visible_width <= text_width: + # Normal case - text is still scrolling in + visible_portion = self.cached_text_image.crop(( + self.scroll_position, 0, + self.scroll_position + visible_width, text_height + )) + frame_image.paste(visible_portion, (0, 0)) + else: + # Wrapping case - text is wrapping around + first_part_width = text_width - self.scroll_position + first_part = self.cached_text_image.crop(( + self.scroll_position, 0, + text_width, text_height + )) + second_part = self.cached_text_image.crop(( + 0, 0, + visible_width - first_part_width, text_height + )) + frame_image.paste(first_part, (0, 0)) + frame_image.paste(second_part, (first_part_width, 0)) + + # Update the display with the new frame + self.display_manager.image = frame_image + self.display_manager.update_display() # If we've completed a full scroll, move to the next group if self.scroll_position == 0: self.current_news_group = (self.current_news_group + 1) % ((total_headlines + headlines_per_rotation - 1) // headlines_per_rotation) - # Small delay to control scroll speed - time.sleep(self.scroll_delay) - # Log frame rate self._log_frame_rate() diff --git a/test_stock_news_manager.py b/test_stock_news_manager.py index 9172d974..b84d92d3 100644 --- a/test_stock_news_manager.py +++ b/test_stock_news_manager.py @@ -10,6 +10,9 @@ print(f"Current working directory: {os.getcwd()}") def main(): """Test the StockNewsManager class directly.""" + + display_manager = None + try: # Load configuration config_manager = ConfigManager() @@ -27,6 +30,13 @@ def main(): # Initialize display manager display_manager = DisplayManager(display_config) + + # Clear the display and show a test pattern + display_manager.clear() + display_manager.update_display() + time.sleep(1) # Give time to see the test pattern + + # Initialize news manager with the loaded config news_manager = StockNewsManager(config, display_manager) @@ -43,6 +53,12 @@ def main(): import traceback traceback.print_exc() finally: + if display_manager: + # Clear the display before exiting + display_manager.clear() + display_manager.update_display() + display_manager.cleanup() + print("Test completed") if __name__ == "__main__":