Multi-view pagination with GPIO buttons
This commit is contained in:
222
matrix.py
222
matrix.py
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user