From ed35da47c2c47c8c5deabc6c086ba2b0e7d3a501 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:41:08 -0500 Subject: [PATCH] Attacking Artifacts display changes and buffer changes to fight the artifacting --- src/clock.py | 3 + src/display_manager.py | 100 +++--------- src/weather_manager.py | 362 +++++++++++++++++++++-------------------- 3 files changed, 209 insertions(+), 256 deletions(-) diff --git a/src/clock.py b/src/clock.py index 9ba4fa9a..2ae648e4 100644 --- a/src/clock.py +++ b/src/clock.py @@ -135,6 +135,9 @@ class Clock: small_font=True ) + # Update the display after drawing everything + self.display_manager.update_display() + # Update cache self.last_time = time_str self.last_date = date_str diff --git a/src/display_manager.py b/src/display_manager.py index bed3e7b6..252b9297 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -37,17 +37,17 @@ class DisplayManager: options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm') # Optimize display settings for maximum visibility - options.brightness = 100 # Maximum brightness - options.pwm_bits = 11 # Maximum color depth + options.brightness = 100 + options.pwm_bits = 11 options.pwm_lsb_nanoseconds = 130 options.led_rgb_sequence = 'RGB' - options.pixel_mapper_config = hardware_config.get('pixel_mapper_config', '') + options.pixel_mapper_config = '' options.row_address_type = 0 options.multiplexing = 0 - options.disable_hardware_pulsing = True # Reduce flickering + options.disable_hardware_pulsing = True options.show_refresh_rate = False - options.limit_refresh_rate_hz = 120 # Higher refresh rate - options.gpio_slowdown = 1 # Minimal GPIO slowdown + options.limit_refresh_rate_hz = 120 + options.gpio_slowdown = 1 # Initialize the matrix self.matrix = RGBMatrix(options=options) @@ -72,7 +72,6 @@ class DisplayManager: def _draw_test_pattern(self): """Draw a test pattern to verify the display is working.""" - # Clear the display first self.clear() # Draw a red rectangle border @@ -84,24 +83,24 @@ class DisplayManager: # Draw some text self.draw.text((10, 10), "TEST", font=self.font, fill=(0, 0, 255)) - # Update the display using double buffering + # Update the display once after everything is drawn self.update_display() - - # Wait a moment time.sleep(2) def update_display(self): - """Update the display using double buffering for smooth transitions.""" + """Update the display using double buffering.""" # Copy the current image to the offscreen canvas self.offscreen_canvas.SetImage(self.image) - # Swap the canvases on VSync for smooth transition + # Swap the canvases self.offscreen_canvas = self.matrix.SwapOnVSync(self.offscreen_canvas) def clear(self): """Clear the display completely.""" + # Create a new image and drawing context self.image = Image.new('RGB', (self.matrix.width, self.matrix.height)) self.draw = ImageDraw.Draw(self.image) - self.update_display() # Ensure the clear is displayed + # Update the display to show the clear + self.update_display() def _load_fonts(self): """Load fonts for different text sizes.""" @@ -118,16 +117,7 @@ class DisplayManager: self.small_font = self.font def draw_text(self, text: str, x: int = None, y: int = None, color: Tuple[int, int, int] = (255, 255, 255), small_font: bool = False) -> None: - """Draw text on the display with improved visibility.""" - # Create a new blank image for this text - self.image = Image.new('RGB', (self.matrix.width, self.matrix.height)) - self.draw = ImageDraw.Draw(self.image) - - # Ensure maximum brightness for text - if isinstance(color, tuple) and len(color) == 3: - # Increase brightness of colors while maintaining relative ratios - color = tuple(min(255, int(c * 1.2)) for c in color) - + """Draw text on the display.""" font = self.small_font if small_font else self.font # Get text dimensions for centering if x not specified @@ -143,45 +133,8 @@ class DisplayManager: text_height = bbox[3] - bbox[1] y = (self.matrix.height - text_height) // 2 - # Draw text with slight glow effect for better visibility - # Draw shadow/glow - shadow_offset = 1 - shadow_color = tuple(max(0, int(c * 0.3)) for c in color) - self.draw.text((x + shadow_offset, y + shadow_offset), text, font=font, fill=shadow_color) - # Draw main text self.draw.text((x, y), text, font=font, fill=color) - - # Update the display - self.update_display() - - def draw_scrolling_text(self, text: str, scroll_position: int, force_clear: bool = False) -> None: - """Draw scrolling text on the display.""" - if force_clear: - self.clear() - else: - # Just create a new blank image without updating display - self.image = Image.new('RGB', (self.matrix.width * 2, self.matrix.height)) # Double width for scrolling - self.draw = ImageDraw.Draw(self.image) - - # Calculate text dimensions - bbox = self.draw.textbbox((0, 0), text, font=self.font) - text_width = bbox[2] - bbox[0] - text_height = bbox[3] - bbox[1] - - # Draw text at current scroll position - y = (self.matrix.height - text_height) // 2 - self.draw.text((self.matrix.width - scroll_position, y), text, font=self.font, fill=(255, 255, 255)) - - # If text has scrolled past the left edge, draw it again at the right - if scroll_position > text_width: - self.draw.text((self.matrix.width * 2 - scroll_position, y), text, font=self.font, fill=(255, 255, 255)) - - # Create a cropped version of the image that's the size of our display - visible_portion = self.image.crop((0, 0, self.matrix.width, self.matrix.height)) - - # Update the display with the visible portion - self.matrix.SetImage(visible_portion) def draw_sun(self, x: int, y: int, size: int = 16): """Draw a sun icon using yellow circles and lines.""" @@ -346,11 +299,6 @@ class DisplayManager: def draw_weather_icon(self, condition: str, x: int, y: int, size: int = 16) -> None: """Draw a weather icon based on the condition.""" - # Clear the area where the icon will be drawn - self.draw.rectangle([x, y, x + size, y + size], - fill=(0, 0, 0)) - - # Draw the appropriate weather icon if condition.lower() in ['clear', 'sunny']: self._draw_sun(x, y, size) elif condition.lower() in ['clouds', 'cloudy', 'partly cloudy']: @@ -362,29 +310,21 @@ class DisplayManager: elif condition.lower() in ['thunderstorm', 'storm']: self._draw_storm(x, y, size) else: - # Default to sun if condition is unknown self._draw_sun(x, y, size) - - self.update_display() + # Note: No update_display() here - let the caller handle the update def draw_text_with_icons(self, text: str, icons: List[tuple] = None, x: int = None, y: int = None, - color: tuple = (255, 255, 255), force_clear: bool = False): + color: tuple = (255, 255, 255)): """Draw text with weather icons at specified positions.""" - if force_clear: - self.clear() - else: - self.image = Image.new('RGB', (self.matrix.width, self.matrix.height)) - self.draw = ImageDraw.Draw(self.image) + # Draw the text + self.draw_text(text, x, y, color) - # First draw the text - self.draw_text(text, x, y, color, force_clear=False) - - # Then draw any icons + # Draw any icons if icons: for icon_type, icon_x, icon_y in icons: - WeatherIcons.draw_weather_icon(self.draw, icon_type, icon_x, icon_y) + self.draw_weather_icon(icon_type, icon_x, icon_y) - # Update the display + # Update the display once after everything is drawn self.update_display() def cleanup(self): diff --git a/src/weather_manager.py b/src/weather_manager.py index 2432bf74..b0a0730b 100644 --- a/src/weather_manager.py +++ b/src/weather_manager.py @@ -49,6 +49,10 @@ class WeatherManager: 'temp_high': (255, 100, 100), 'temp_low': (100, 100, 255) } + # Add caching for last drawn states + self.last_weather_state = None + self.last_hourly_state = None + self.last_daily_state = None def _fetch_weather(self) -> None: """Fetch current weather and forecast data from OpenWeatherMap API.""" @@ -158,194 +162,200 @@ class WeatherManager: self._fetch_weather() return self.weather_data + def _get_weather_state(self) -> Dict[str, Any]: + """Get current weather state for comparison.""" + if not self.weather_data: + return None + return { + 'temp': round(self.weather_data['main']['temp']), + 'condition': self.weather_data['weather'][0]['main'], + 'humidity': self.weather_data['main']['humidity'] + } + + def _get_hourly_state(self) -> List[Dict[str, Any]]: + """Get current hourly forecast state for comparison.""" + if not self.hourly_forecast: + return None + return [ + {'hour': f['hour'], 'temp': round(f['temp']), 'condition': f['condition']} + for f in self.hourly_forecast[:3] + ] + + def _get_daily_state(self) -> List[Dict[str, Any]]: + """Get current daily forecast state for comparison.""" + if not self.daily_forecast: + return None + return [ + { + 'date': f['date'], + 'temp_high': round(f['temp_high']), + 'temp_low': round(f['temp_low']), + 'condition': f['condition'] + } + for f in self.daily_forecast[:3] + ] + def display_weather(self, force_clear: bool = False) -> None: """Display current weather information using a static layout.""" - weather_data = self.get_weather() - if not weather_data: - return + try: + weather_data = self.get_weather() + if not weather_data: + print("No weather data available") + return - # Always clear and redraw - self.display_manager.clear() - - # Calculate layout - display_width = self.display_manager.matrix.width - display_height = self.display_manager.matrix.height - - # Get weather data - temp = round(weather_data['main']['temp']) - condition = weather_data['weather'][0]['main'] - humidity = weather_data['main']['humidity'] - - # Draw temperature (large, centered) - temp_text = f"{temp}°F" - self.display_manager.draw_text( - temp_text, - y=2, # Near top - color=self.COLORS['highlight'], - small_font=False - ) - - # Draw weather icon below temperature - icon_x = (display_width - self.ICON_SIZE['large']) // 2 - icon_y = display_height // 2 - 4 - self.display_manager.draw_weather_icon(condition, icon_x, icon_y, size=self.ICON_SIZE['large']) - - # Draw humidity at bottom - humidity_text = f"Humidity: {humidity}%" - self.display_manager.draw_text( - humidity_text, - y=display_height - 8, - color=self.COLORS['text'], - small_font=True - ) - - # Update display - self.display_manager.update_display() + # Check if state has changed + current_state = self._get_weather_state() + if not force_clear and current_state == self.last_weather_state: + return # No need to redraw if nothing changed - def display_hourly_forecast(self, scroll_position: int = 0, force_clear: bool = False) -> None: - """Display static hourly forecast showing next 3 hours.""" - if not self.hourly_forecast: - self.get_weather() + # Clear the display once at the start + self.display_manager.clear() + + # Draw temperature (large, centered) + temp_text = f"{current_state['temp']}°F" + self.display_manager.draw_text( + temp_text, + y=2, + color=self.COLORS['highlight'], + small_font=False + ) + + # Draw weather icon below temperature + icon_x = (self.display_manager.matrix.width - self.ICON_SIZE['large']) // 2 + icon_y = self.display_manager.matrix.height // 2 - 4 + self.display_manager.draw_weather_icon( + current_state['condition'], + icon_x, + icon_y, + size=self.ICON_SIZE['large'] + ) + + # Draw humidity at bottom + humidity_text = f"Humidity: {current_state['humidity']}%" + self.display_manager.draw_text( + humidity_text, + y=self.display_manager.matrix.height - 8, + color=self.COLORS['text'], + small_font=True + ) + + # Update display once after all elements are drawn + self.display_manager.update_display() + self.last_weather_state = current_state + + except Exception as e: + print(f"Error displaying weather: {e}") + + def display_hourly_forecast(self, force_clear: bool = False): + """Display the next few hours of weather forecast.""" + try: if not self.hourly_forecast: + print("No hourly forecast data available") return - - # Always clear and redraw - self.display_manager.clear() - - # Calculate layout parameters - display_width = self.display_manager.matrix.width - display_height = self.display_manager.matrix.height - section_width = display_width // 3 # Show 3 hours - - # Draw header - header_text = "NEXT 3 HOURS" - self.display_manager.draw_text( - header_text, - y=1, - color=self.COLORS['highlight'], - small_font=True - ) - - # Draw separator line - self.display_manager.draw.line( - [(0, 8), (display_width, 8)], - fill=self.COLORS['separator'] - ) - - # Show first 3 hours - for i, forecast in enumerate(self.hourly_forecast[:3]): - x_base = i * section_width - # Draw time - self.display_manager.draw_text( - forecast['hour'], - x=x_base + section_width // 2, - y=10, - color=self.COLORS['text'], - small_font=True - ) - - # Draw icon - icon_x = x_base + (section_width - self.ICON_SIZE['medium']) // 2 - icon_y = 14 - self.display_manager.draw_weather_icon( - forecast['condition'], - icon_x, - icon_y, - size=self.ICON_SIZE['medium'] - ) - - # Draw temperature - temp_text = f"{forecast['temp']}°F" - self.display_manager.draw_text( - temp_text, - x=x_base + section_width // 2, - y=display_height - 8, - color=self.COLORS['text'], - small_font=True - ) - - # Draw separator lines - if i < 2: # Only draw between sections - sep_x = x_base + section_width - 1 - self.display_manager.draw.line( - [(sep_x, 8), (sep_x, display_height)], - fill=self.COLORS['separator'] + # Check if state has changed + current_state = self._get_hourly_state() + if not force_clear and current_state == self.last_hourly_state: + return # No need to redraw if nothing changed + + # Clear once at the start + self.display_manager.clear() + + # Display next 3 hours + hours_to_show = min(3, len(self.hourly_forecast)) + section_width = self.display_manager.matrix.width // hours_to_show + + for i in range(hours_to_show): + forecast = current_state[i] + x = i * section_width + + # Draw hour + self.display_manager.draw_text( + forecast['hour'], + x=x + 2, + y=2, + color=self.COLORS['text'], + small_font=True ) + + # Draw weather icon + self.display_manager.draw_weather_icon( + forecast['condition'], + x=x + (section_width - self.ICON_SIZE['medium']) // 2, + y=12, + size=self.ICON_SIZE['medium'] + ) + + # Draw temperature + temp = f"{forecast['temp']}°" + self.display_manager.draw_text( + temp, + x=x + (section_width - len(temp) * 4) // 2, + y=24, + color=self.COLORS['highlight'], + small_font=True + ) + + # Update display once after all elements are drawn + self.display_manager.update_display() + self.last_hourly_state = current_state - # Update display - self.display_manager.update_display() + except Exception as e: + print(f"Error displaying hourly forecast: {e}") - def display_daily_forecast(self, force_clear: bool = False) -> None: - """Display static 3-day forecast.""" - if not self.daily_forecast: - self.get_weather() + def display_daily_forecast(self, force_clear: bool = False): + """Display the 3-day weather forecast.""" + try: if not self.daily_forecast: + print("No daily forecast data available") return - - # Always clear and redraw - self.display_manager.clear() - - # Calculate layout parameters - display_width = self.display_manager.matrix.width - display_height = self.display_manager.matrix.height - section_width = display_width // 3 - - # Draw header - header_text = "3-DAY FORECAST" - self.display_manager.draw_text( - header_text, - y=1, - color=self.COLORS['highlight'], - small_font=True - ) - - # Draw separator line - self.display_manager.draw.line( - [(0, 8), (display_width, 8)], - fill=self.COLORS['separator'] - ) - - for i, day in enumerate(self.daily_forecast): - x_base = i * section_width - # Draw day name - day_text = day['date'].upper() - self.display_manager.draw_text( - day_text, - x=x_base + section_width // 2, - y=10, - color=self.COLORS['text'], - small_font=True - ) - - # Draw weather icon - icon_x = x_base + (section_width - self.ICON_SIZE['medium']) // 2 - icon_y = 14 - self.display_manager.draw_weather_icon( - day['condition'], - icon_x, - icon_y, - size=self.ICON_SIZE['medium'] - ) - - # Draw temperatures with different colors for high/low - temp_text = f"{day['temp_low']}°/{day['temp_high']}°" - self.display_manager.draw_text( - temp_text, - x=x_base + section_width // 2, - y=display_height - 8, - color=self.COLORS['text'], - small_font=True - ) - - # Draw separator lines - if i < 2: # Only draw between sections - sep_x = x_base + section_width - 1 - self.display_manager.draw.line( - [(sep_x, 8), (sep_x, display_height)], - fill=self.COLORS['separator'] + # Check if state has changed + current_state = self._get_daily_state() + if not force_clear and current_state == self.last_daily_state: + return # No need to redraw if nothing changed + + # Clear once at the start + self.display_manager.clear() + + # Display 3 days + days_to_show = min(3, len(self.daily_forecast)) + section_width = self.display_manager.matrix.width // days_to_show + + for i in range(days_to_show): + forecast = current_state[i] + x = i * section_width + + # Draw day name + self.display_manager.draw_text( + forecast['date'].upper(), + x=x + 2, + y=2, + color=self.COLORS['text'], + small_font=True ) + + # Draw weather icon + self.display_manager.draw_weather_icon( + forecast['condition'], + x=x + (section_width - self.ICON_SIZE['medium']) // 2, + y=12, + size=self.ICON_SIZE['medium'] + ) + + # Draw temperature range + temp = f"{forecast['temp_low']}/{forecast['temp_high']}°" + self.display_manager.draw_text( + temp, + x=x + (section_width - len(temp) * 4) // 2, + y=24, + color=self.COLORS['highlight'], + small_font=True + ) + + # Update display once after all elements are drawn + self.display_manager.update_display() + self.last_daily_state = current_state - # Update display - self.display_manager.update_display() \ No newline at end of file + except Exception as e: + print(f"Error displaying daily forecast: {e}") \ No newline at end of file