Files
enviro-service/lcd.py
2025-12-24 11:17:56 -06:00

210 lines
6.7 KiB
Python

"""
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