From 3d662baf54f504fe40e7efcdfde658e8a38376d7 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:10:50 -0500 Subject: [PATCH] Stocks (#2) * Opening Bell Introducing the Stock Ticker Feature * Update stock_manager.py Assume folder exists * Update stock_manager.py removing logos to focus on function for now * Update stock_manager.py parse yahoo scripts * Update stock_manager.py stock query update * Update stock_manager.py slow down stock display * Update display_controller.py adjust screen flow * Update stock_manager.py shipping features * Update stock_manager.py stock refresh in the background * Customize Display timings customize display timings * Update stock_manager.py stock font size change * Sizing and Spacing CHanged font sizing on chart and clock spacing * Update clock.py Date format changes * Update stock_manager.py actually read stocks from config file * Update stock_manager.py add config manager * readme update readme update and formatting for better flow * Update .gitignore rename reference folder * Update config.json changed default stocks to test update implementation * Stock News Stock news Ticker * Update config.json increase scroll speed * Scroll Performance Tuning news scrolling performance * updating scroll direction orienting scroll direction * News tuning removed test files and increased scroll speed * Create test_news_manager.py need a test script to call upon * Update test_news_manager.py test script tuning * troubleshooting test script * Update test_news_manager.py * Update config.json scroll speed increases * Update config.json scroll tuning * Update config.json speeding up * Update config.json still making text faster * Update config.json Trying to tune scrolling * Update config.json testing crazy parameters * Update test_news_manager.py remove sleep delay * scroll tuning scroll tuning * scroll logging and debugging FPS counter and debug messages * Update config.json matrix speed tuning * Update news_manager.py News separator * Update news_manager.py separator character change * Stock News manager Rename rename stock news ticker to enable other news in the future * Update display_controller.py load config update * Update stock_manager.py remove redundant import * Stock news settings Stock news has more granular control * Stock news joins the lineup Stock News added to the display controller and drawing display instead of image * Optimize scrolling text performance for news ticker * Adjust matrix settings to reduce artifacting while maintaining performance * changed float to integer * Fix news ticker performance with simplified scrolling mechanism * Fix stock news scrolling in test environment: - Optimize display manager settings for smooth scrolling - Add proper display initialization and cleanup in test script - Implement timing control to prevent display buffer overflow - Ensure consistent 1ms delay between updates for smooth scrolling * Optimize stock news scrolling for better performance: - Use pre-rendered text image for efficient scrolling - Implement cropping and pasting for smoother animation - Remove unnecessary display operations and delays * Optimize stock news display performance: - Cache text image to reduce rendering overhead - Improve frame creation and update logic - Optimize text wrapping for smoother scrolling - Remove unnecessary display clears * Optimize stock news display in controller: - Remove global sleep delay - Allow news display to run at full speed - Keep slower update rates for other displays --------- Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com> --- config/config.json | 2 +- src/display_controller.py | 9 ++--- src/display_manager.py | 19 +++++------ src/stock_news_manager.py | 68 ++++++++++++++++++++++++++------------ test_stock_news_manager.py | 16 +++++++++ 5 files changed, 76 insertions(+), 38 deletions(-) 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__":