""" LCD display module for Enviro Mini HAT ST7789 screen. Provides simple interface to display text and graphics on the 0.96" LCD. """ import logging from typing import Optional, Tuple from PIL import Image, ImageDraw, ImageFont logger = logging.getLogger(__name__) # Display dimensions for ST7789 on Enviro Mini WIDTH = 160 HEIGHT = 80 # Try to import display library try: import ST7789 ST7789_AVAILABLE = True except ImportError: ST7789_AVAILABLE = False logger.warning("ST7789 library not available - LCD functions will be mocked") class EnviroLCD: """Interface to the Enviro Mini LCD display.""" # Common colors COLORS = { "black": (0, 0, 0), "white": (255, 255, 255), "red": (255, 0, 0), "green": (0, 255, 0), "blue": (0, 0, 255), "yellow": (255, 255, 0), "cyan": (0, 255, 255), "magenta": (255, 0, 255), "orange": (255, 165, 0), "fox": (255, 140, 0), # Fox orange! 🦊 } def __init__(self, mock_mode: bool = False, brightness: float = 0.5): """ Initialize LCD display. Args: mock_mode: If True, don't actually write to display brightness: Backlight brightness 0.0-1.0 """ self.mock_mode = mock_mode or not ST7789_AVAILABLE self.brightness = brightness self._display = None if not self.mock_mode: try: self._display = ST7789.ST7789( port=0, cs=ST7789.BG_SPI_CS_FRONT, dc=9, backlight=13, spi_speed_hz=80 * 1000 * 1000 ) self._display.set_backlight(brightness) logger.info("LCD display initialized") except Exception as e: logger.error(f"Failed to initialize LCD: {e}") self.mock_mode = True # Load a simple font try: self._font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 14) self._font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) self._font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20) except: self._font = ImageFont.load_default() self._font_small = self._font self._font_large = self._font def _get_color(self, color) -> Tuple[int, int, int]: """Convert color name or tuple to RGB tuple.""" if isinstance(color, str): return self.COLORS.get(color.lower(), self.COLORS["white"]) return color def clear(self, color="black"): """Clear the display to a solid color.""" img = Image.new("RGB", (WIDTH, HEIGHT), self._get_color(color)) self._show(img) def show_message( self, text: str, bg_color="black", text_color="white", font_size: str = "normal" ): """ Display a text message. Args: text: Message to display (supports multi-line with \n) bg_color: Background color text_color: Text color font_size: 'small', 'normal', or 'large' """ img = Image.new("RGB", (WIDTH, HEIGHT), self._get_color(bg_color)) draw = ImageDraw.Draw(img) font = { "small": self._font_small, "normal": self._font, "large": self._font_large }.get(font_size, self._font) # Center the text bbox = draw.multiline_textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] x = (WIDTH - text_width) // 2 y = (HEIGHT - text_height) // 2 draw.multiline_text( (x, y), text, fill=self._get_color(text_color), font=font, align="center" ) self._show(img) def show_sensor_data(self, reading: dict): """ Display current sensor readings in a nice format. Args: reading: Dict with temperature_f, humidity, etc. """ img = Image.new("RGB", (WIDTH, HEIGHT), (0, 0, 30)) # Dark blue background draw = ImageDraw.Draw(img) # Temperature (big) temp = f"{reading.get('temperature_f', 0):.0f}°F" draw.text((5, 5), temp, fill=(255, 200, 100), font=self._font_large) # Humidity humidity = f"💧 {reading.get('humidity', 0):.0f}%" draw.text((90, 10), humidity, fill=(100, 200, 255), font=self._font_small) # Pressure pressure = f"⬇ {reading.get('pressure', 0):.0f}" draw.text((5, 50), pressure, fill=(200, 200, 200), font=self._font_small) # Light light = f"☀ {reading.get('light', 0):.0f}" draw.text((70, 50), light, fill=(255, 255, 150), font=self._font_small) # Noise noise = f"🔊 {reading.get('noise', 0):.0f}" draw.text((120, 50), noise, fill=(150, 255, 150), font=self._font_small) self._show(img) def show_vixy(self): """Show Vixy's presence indicator.""" img = Image.new("RGB", (WIDTH, HEIGHT), (20, 10, 30)) # Dark purple draw = ImageDraw.Draw(img) # Fox emoji and name draw.text((WIDTH//2 - 40, HEIGHT//2 - 15), "🦊 Vixy", fill=(255, 140, 0), font=self._font_large) draw.text((WIDTH//2 - 30, HEIGHT//2 + 15), "is watching", fill=(200, 200, 200), font=self._font_small) self._show(img) def show_image(self, image: Image.Image): """Display a PIL Image directly.""" # Resize to fit display img = image.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS) if img.mode != "RGB": img = img.convert("RGB") self._show(img) def set_brightness(self, brightness: float): """Set backlight brightness (0.0-1.0).""" self.brightness = max(0.0, min(1.0, brightness)) if self._display and not self.mock_mode: self._display.set_backlight(self.brightness) def _show(self, img: Image.Image): """Actually send image to display.""" if self.mock_mode: logger.debug(f"LCD mock: would display {img.size} image") return if self._display: self._display.display(img) # Singleton instance _lcd: Optional[EnviroLCD] = None def get_lcd(mock_mode: bool = False, brightness: float = 0.5) -> EnviroLCD: """Get or create the LCD instance.""" global _lcd if _lcd is None: _lcd = EnviroLCD(mock_mode=mock_mode, brightness=brightness) return _lcd