Initial commit: Enviro Service for Vixy's nervous system 🦊
This commit is contained in:
209
lcd.py
Normal file
209
lcd.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user