Multi-view pagination with GPIO buttons

This commit is contained in:
Joan
2025-12-31 13:06:17 +01:00
parent bfb2290634
commit 6364f63f15
3 changed files with 193 additions and 115 deletions

222
matrix.py
View File

@@ -1,18 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Matrix64 LED Display Matrix64 LED Display with Multi-View Pagination
Layout: Views:
[Weather 24x24] [HDD temps] 0: Main (Weather + Clock + Temps)
[HDD temps] 1: HDD Details
12:37:25 2: Date + Weather Forecast
Out 21.5° 65%
In 22.3° 58% Buttons (GPIO 0 & 1):
- Button 1: Next view
- Button 2: Previous view
- Long press: Toggle auto-cycle
""" """
from samplebase import SampleBase from samplebase import SampleBase
from rgbmatrix import graphics from rgbmatrix import graphics
from gpiozero import Button
import time import time
import datetime import datetime
import threading
from config import LED_ROWS, LED_COLS from config import LED_ROWS, LED_COLS
from weather_icons import draw_weather_icon from weather_icons import draw_weather_icon
@@ -23,6 +28,10 @@ from home_assistant import (
from netdata import get_hdd_temps from netdata import get_hdd_temps
# Number of views
NUM_VIEWS = 3
def format_temp(value): def format_temp(value):
"""Format temperature to one decimal place.""" """Format temperature to one decimal place."""
try: try:
@@ -61,6 +70,8 @@ class Matrix64Display(SampleBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Matrix64Display, self).__init__(*args, **kwargs) super(Matrix64Display, self).__init__(*args, **kwargs)
self.parser.add_argument("-t", "--text", help="Text", default="") self.parser.add_argument("-t", "--text", help="Text", default="")
# Data
self.temperature = None self.temperature = None
self.humidity = None self.humidity = None
self.interior_temperature = None self.interior_temperature = None
@@ -68,6 +79,41 @@ class Matrix64Display(SampleBase):
self.last_update = time.time() self.last_update = time.time()
self.weather_desc = "unknown" self.weather_desc = "unknown"
self.hdd_temps = None self.hdd_temps = None
# View state
self.current_view = 0
self.auto_cycle = False
self.last_view_change = time.time()
self.view_cycle_interval = 5 # seconds
# Button setup
self.button1 = Button(0, pull_up=True, hold_time=1.5)
self.button2 = Button(1, pull_up=True, hold_time=1.5)
# Button callbacks
self.button1.when_pressed = self.on_button1_press
self.button1.when_held = self.on_button_held
self.button2.when_pressed = self.on_button2_press
self.button2.when_held = self.on_button_held
def on_button1_press(self):
"""Next view."""
if not self.button1.is_held:
self.current_view = (self.current_view + 1) % NUM_VIEWS
self.last_view_change = time.time()
print(f"View: {self.current_view}")
def on_button2_press(self):
"""Previous view."""
if not self.button2.is_held:
self.current_view = (self.current_view - 1) % NUM_VIEWS
self.last_view_change = time.time()
print(f"View: {self.current_view}")
def on_button_held(self):
"""Toggle auto-cycle on long press."""
self.auto_cycle = not self.auto_cycle
print(f"Auto-cycle: {self.auto_cycle}")
def update_data(self): def update_data(self):
try: try:
@@ -95,6 +141,97 @@ class Matrix64Display(SampleBase):
def update_hdd_temps(self): def update_hdd_temps(self):
self.hdd_temps = get_hdd_temps() self.hdd_temps = get_hdd_temps()
def draw_view_0(self, canvas, fonts, colors):
"""Main view: Weather + Clock + Temps."""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
now = datetime.datetime.now()
time_str = f"{now.hour:02d}:{now.minute:02d}:{now.second:02d}"
# Weather Icon
draw_weather_icon(canvas, 0, 0, self.weather_desc)
# Vertical separator
graphics.DrawLine(canvas, 28, 0, 28, 25, line_color)
# HDD Temps (3 rows of 2)
if self.hdd_temps and len(self.hdd_temps) > 1:
temps = [str(int(t[0])) for t in self.hdd_temps[1:] if t]
if len(temps) >= 2:
graphics.DrawText(canvas, data_font, 34, 8, hdd_color, " ".join(temps[:2]))
if len(temps) >= 4:
graphics.DrawText(canvas, data_font, 34, 16, hdd_color, " ".join(temps[2:4]))
if len(temps) > 4:
graphics.DrawText(canvas, data_font, 34, 24, hdd_color, " ".join(temps[4:6]))
# Horizontal lines around clock
graphics.DrawLine(canvas, 0, 26, 63, 26, line_color)
time_x = (64 - len(time_str) * 7) // 2
graphics.DrawText(canvas, time_font, time_x, 39, time_color, time_str)
graphics.DrawLine(canvas, 0, 42, 63, 42, line_color)
# Outdoor temp/humidity
graphics.DrawText(canvas, data_font, 0, 52, label_color, "Out")
if self.temperature is not None:
temp_color = get_temperature_color(self.temperature)
graphics.DrawText(canvas, data_font, 20, 52, temp_color, format_temp(self.temperature))
if self.humidity is not None:
graphics.DrawText(canvas, data_font, 48, 52, humidity_color, format_humidity(self.humidity))
# Indoor temp/humidity
graphics.DrawText(canvas, data_font, 0, 62, label_color, "In")
if self.interior_temperature is not None:
temp_color = get_temperature_color(self.interior_temperature)
graphics.DrawText(canvas, data_font, 20, 62, temp_color, format_temp(self.interior_temperature))
if self.interior_humidity is not None:
graphics.DrawText(canvas, data_font, 48, 62, humidity_color, format_humidity(self.interior_humidity))
def draw_view_1(self, canvas, fonts, colors):
"""HDD Details view."""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
# Title
graphics.DrawText(canvas, data_font, 8, 8, time_color, "HDD Temps")
graphics.DrawLine(canvas, 0, 10, 63, 10, line_color)
if self.hdd_temps and len(self.hdd_temps) > 1:
y = 22
for i, t in enumerate(self.hdd_temps[1:7]):
if t:
temp_val = int(t[0])
temp_color = get_temperature_color(temp_val)
graphics.DrawText(canvas, data_font, 4, y, label_color, f"HDD{i+1}")
graphics.DrawText(canvas, data_font, 36, y, temp_color, f"{temp_val}C")
y += 10
def draw_view_2(self, canvas, fonts, colors):
"""Date + Info view."""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
now = datetime.datetime.now()
# Date
date_str = now.strftime("%d %b %Y")
day_str = now.strftime("%A")
graphics.DrawText(canvas, time_font, 2, 16, time_color, date_str)
graphics.DrawText(canvas, data_font, 8, 28, label_color, day_str)
graphics.DrawLine(canvas, 0, 32, 63, 32, line_color)
# Weather summary
draw_weather_icon(canvas, 4, 36, self.weather_desc)
if self.temperature is not None:
temp_color = get_temperature_color(self.temperature)
graphics.DrawText(canvas, time_font, 32, 52, temp_color, f"{self.temperature:.0f}C")
# Auto-cycle indicator
if self.auto_cycle:
graphics.DrawText(canvas, small_font, 56, 62, label_color, "A")
def run(self): def run(self):
canvas = self.matrix.CreateFrameCanvas() canvas = self.matrix.CreateFrameCanvas()
@@ -107,11 +244,16 @@ class Matrix64Display(SampleBase):
small_font = graphics.Font() small_font = graphics.Font()
small_font.LoadFont(f"{font_dir}/4x6.bdf") small_font.LoadFont(f"{font_dir}/4x6.bdf")
fonts = (time_font, data_font, small_font)
# Colors # Colors
time_color = graphics.Color(30, 90, 220) time_color = graphics.Color(30, 90, 220)
humidity_color = graphics.Color(80, 160, 255) humidity_color = graphics.Color(80, 160, 255)
hdd_color = graphics.Color(140, 120, 100) # Warm tan, easier to read hdd_color = graphics.Color(140, 120, 100)
label_color = graphics.Color(120, 120, 120) label_color = graphics.Color(120, 120, 120)
line_color = graphics.Color(40, 40, 40)
colors = (time_color, humidity_color, hdd_color, label_color, line_color)
# Initial fetch # Initial fetch
self.update_brightness() self.update_brightness()
@@ -119,67 +261,29 @@ class Matrix64Display(SampleBase):
self.update_hdd_temps() self.update_hdd_temps()
while True: while True:
now = datetime.datetime.now()
time_str = f"{now.hour:02d}:{now.minute:02d}:{now.second:02d}"
current_time = time.time() current_time = time.time()
# Update data every 60 seconds
if current_time - self.last_update >= 60: if current_time - self.last_update >= 60:
self.update_data() self.update_data()
self.update_brightness() self.update_brightness()
self.update_hdd_temps() self.update_hdd_temps()
self.last_update = current_time self.last_update = current_time
# Auto-cycle views
if self.auto_cycle and (current_time - self.last_view_change >= self.view_cycle_interval):
self.current_view = (self.current_view + 1) % NUM_VIEWS
self.last_view_change = current_time
canvas.Clear() canvas.Clear()
# Line color for separators # Draw current view
line_color = graphics.Color(40, 40, 40) if self.current_view == 0:
self.draw_view_0(canvas, fonts, colors)
# === Weather Icon (top-left, 24x24) === elif self.current_view == 1:
draw_weather_icon(canvas, 0, 0, self.weather_desc) self.draw_view_1(canvas, fonts, colors)
elif self.current_view == 2:
# === Vertical separator between weather and HDD === self.draw_view_2(canvas, fonts, colors)
graphics.DrawLine(canvas, 28, 0, 28, 25, line_color)
# === HDD Temps (top-right, 5x8 font, 3 rows of 2) ===
if self.hdd_temps and len(self.hdd_temps) > 1:
temps = [str(int(t[0])) for t in self.hdd_temps[1:] if t]
if len(temps) >= 2:
row1 = " ".join(temps[:2])
graphics.DrawText(canvas, data_font, 34, 8, hdd_color, row1)
if len(temps) >= 4:
row2 = " ".join(temps[2:4])
graphics.DrawText(canvas, data_font, 34, 16, hdd_color, row2)
if len(temps) > 4:
row3 = " ".join(temps[4:6])
graphics.DrawText(canvas, data_font, 34, 24, hdd_color, row3)
# === Horizontal line above clock ===
graphics.DrawLine(canvas, 0, 26, 63, 26, line_color)
# === Clock (centered) ===
time_x = (64 - len(time_str) * 7) // 2
graphics.DrawText(canvas, time_font, time_x, 39, time_color, time_str)
# === Horizontal line below clock ===
graphics.DrawLine(canvas, 0, 42, 63, 42, line_color)
# === Outdoor: Out + temp + humidity (y=52) ===
graphics.DrawText(canvas, data_font, 0, 52, label_color, "Out")
if self.temperature is not None:
temp_str = format_temp(self.temperature)
temp_color = get_temperature_color(self.temperature)
graphics.DrawText(canvas, data_font, 20, 52, temp_color, temp_str)
if self.humidity is not None:
graphics.DrawText(canvas, data_font, 48, 52, humidity_color, format_humidity(self.humidity))
# === Indoor: In + temp + humidity (y=62) ===
graphics.DrawText(canvas, data_font, 0, 62, label_color, "In")
if self.interior_temperature is not None:
temp_str = format_temp(self.interior_temperature)
temp_color = get_temperature_color(self.interior_temperature)
graphics.DrawText(canvas, data_font, 20, 62, temp_color, temp_str)
if self.interior_humidity is not None:
graphics.DrawText(canvas, data_font, 48, 62, humidity_color, format_humidity(self.interior_humidity))
time.sleep(0.5) time.sleep(0.5)
canvas = self.matrix.SwapOnVSync(canvas) canvas = self.matrix.SwapOnVSync(canvas)

View File

@@ -1,2 +1,3 @@
python-dotenv>=1.0.0 python-dotenv>=1.0.0
requests>=2.28.0 requests>=2.28.0
gpiozero>=1.6.0

View File

@@ -1,62 +1,35 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
GPIO Button Test Script GPIO Button Test Script using gpiozero
Run this on the Raspberry Pi to identify which GPIO pins the buttons are on.
Press Ctrl+C to exit.
""" """
from gpiozero import Button
import time import time
button1 = Button(0, pull_up=True)
button2 = Button(1, pull_up=True)
print("Button test - press buttons to test")
print("Press Ctrl+C to exit\n")
def on_button1_press():
print(">>> Button 1 (GPIO 0) PRESSED <<<")
def on_button1_release():
print(" Button 1 (GPIO 0) released")
def on_button2_press():
print(">>> Button 2 (GPIO 1) PRESSED <<<")
def on_button2_release():
print(" Button 2 (GPIO 1) released")
button1.when_pressed = on_button1_press
button1.when_released = on_button1_release
button2.when_pressed = on_button2_press
button2.when_released = on_button2_release
try: try:
import RPi.GPIO as GPIO while True:
except ImportError: time.sleep(0.1)
print("RPi.GPIO not found. Install with: sudo apt install python3-rpi.gpio") except KeyboardInterrupt:
exit(1) print("\nTest complete.")
# Common GPIO pins to test (BCM numbering)
# Excluding pins used by the LED matrix (typically 2-27 are used by the matrix)
# These are additional GPIO pins that might be available
TEST_PINS = [4, 5, 6, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
def main():
print("=" * 50)
print("GPIO Capacitive Button Test")
print("=" * 50)
print("\nSetting up GPIO pins for input with pull-up resistors...")
print("Press buttons to see which GPIO pin responds.")
print("Press Ctrl+C to exit.\n")
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
# Set up all test pins as inputs with pull-up
active_pins = []
for pin in TEST_PINS:
try:
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
active_pins.append(pin)
except Exception as e:
print(f" Pin {pin}: Could not configure ({e})")
print(f"Testing pins: {active_pins}\n")
# Store previous states to detect changes
prev_states = {pin: GPIO.input(pin) for pin in active_pins}
try:
while True:
for pin in active_pins:
current = GPIO.input(pin)
if current != prev_states[pin]:
if current == GPIO.LOW:
print(f">>> Button PRESSED on GPIO {pin} <<<")
else:
print(f" Button released on GPIO {pin}")
prev_states[pin] = current
time.sleep(0.05)
except KeyboardInterrupt:
print("\n\nTest complete.")
finally:
GPIO.cleanup()
if __name__ == "__main__":
main()