210 lines
6.7 KiB
Python
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
|