dreader-application/dreader/gpio_buttons.py
Duncan Tourolle 70c0b4a1f2
All checks were successful
Python CI / test (3.12) (push) Successful in 7m1s
Python CI / test (3.13) (push) Successful in 7m10s
HW integratation
2025-11-11 11:57:39 +01:00

292 lines
8.9 KiB
Python

"""
GPIO Button Handler for DReader.
This module provides GPIO button support for physical buttons on the e-reader device.
Buttons can be mapped to touch gestures for navigation and control.
Usage:
from dreader.gpio_buttons import GPIOButtonHandler
buttons = GPIOButtonHandler(config)
await buttons.initialize()
# Check for button events
event = await buttons.get_button_event()
if event:
print(f"Button pressed: {event.gesture}")
await buttons.cleanup()
"""
import asyncio
import logging
from typing import Optional, Dict, List
from dataclasses import dataclass
from .gesture import TouchEvent, GestureType
logger = logging.getLogger(__name__)
# Try to import RPi.GPIO
try:
import RPi.GPIO as GPIO
GPIO_AVAILABLE = True
except ImportError:
GPIO_AVAILABLE = False
logger.warning("RPi.GPIO not available. Button support disabled.")
@dataclass
class ButtonConfig:
"""Configuration for a single GPIO button."""
name: str
gpio: int
gesture: GestureType
description: str = ""
class GPIOButtonHandler:
"""
Handler for GPIO buttons that generates touch events.
This class manages physical buttons connected to GPIO pins and converts
button presses into TouchEvent objects that can be handled by the application.
Args:
buttons: List of ButtonConfig objects defining button mappings
pull_up: Use pull-up resistors (default True)
bounce_time_ms: Debounce time in milliseconds (default 200)
screen_width: Screen width for generating touch coordinates (default 1872)
screen_height: Screen height for generating touch coordinates (default 1404)
Example:
buttons_config = [
ButtonConfig("next", 23, GestureType.SWIPE_LEFT, "Next page"),
ButtonConfig("prev", 24, GestureType.SWIPE_RIGHT, "Previous page"),
]
handler = GPIOButtonHandler(buttons_config)
await handler.initialize()
# In main loop
event = await handler.get_button_event()
if event:
await app.handle_touch(event)
"""
def __init__(
self,
buttons: List[ButtonConfig],
pull_up: bool = True,
bounce_time_ms: int = 200,
screen_width: int = 1872,
screen_height: int = 1404,
):
"""Initialize GPIO button handler."""
self.buttons = buttons
self.pull_up = pull_up
self.bounce_time_ms = bounce_time_ms
self.screen_width = screen_width
self.screen_height = screen_height
self._initialized = False
self._event_queue: asyncio.Queue = asyncio.Queue()
self._gpio_map: Dict[int, ButtonConfig] = {}
if not GPIO_AVAILABLE:
logger.error("RPi.GPIO not available. Buttons will not work.")
return
logger.info(f"GPIO button handler created with {len(buttons)} buttons")
for btn in buttons:
logger.info(f" Button '{btn.name}' on GPIO {btn.gpio} -> {btn.gesture.value}")
async def initialize(self):
"""Initialize GPIO pins and set up button callbacks."""
if self._initialized:
return
if not GPIO_AVAILABLE:
logger.warning("Cannot initialize buttons: RPi.GPIO not available")
return
logger.info("Initializing GPIO buttons...")
# Set GPIO mode
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
# Configure each button
for button in self.buttons:
try:
# Configure pin
if self.pull_up:
GPIO.setup(button.gpio, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# Button pressed = LOW (0)
edge = GPIO.FALLING
else:
GPIO.setup(button.gpio, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
# Button pressed = HIGH (1)
edge = GPIO.RISING
# Add event detection with debounce
GPIO.add_event_detect(
button.gpio,
edge,
callback=lambda channel, btn=button: self._button_callback(btn),
bouncetime=self.bounce_time_ms
)
self._gpio_map[button.gpio] = button
logger.info(f"✓ Configured button '{button.name}' on GPIO {button.gpio}")
except Exception as e:
logger.error(f"Failed to configure button '{button.name}' on GPIO {button.gpio}: {e}")
self._initialized = True
logger.info("GPIO buttons initialized successfully")
def _button_callback(self, button: ButtonConfig):
"""
Callback function for button press (runs in GPIO event thread).
This is called by RPi.GPIO when a button is pressed. We put the event
in a queue for async processing.
"""
logger.debug(f"Button pressed: {button.name} (GPIO {button.gpio})")
# Create touch event
# Use center of screen for button events (x, y don't matter for swipes)
event = TouchEvent(
gesture=button.gesture,
x=self.screen_width // 2,
y=self.screen_height // 2,
)
# Put in queue (non-blocking)
try:
self._event_queue.put_nowait(event)
logger.info(f"Button event queued: {button.name} -> {button.gesture.value}")
except asyncio.QueueFull:
logger.warning("Button event queue full, dropping event")
async def get_button_event(self) -> Optional[TouchEvent]:
"""
Get the next button event from the queue.
Returns:
TouchEvent if a button was pressed, None if no events
"""
if not self._initialized:
return None
try:
# Non-blocking get
event = self._event_queue.get_nowait()
return event
except asyncio.QueueEmpty:
return None
async def cleanup(self):
"""Clean up GPIO resources."""
if not self._initialized:
return
if not GPIO_AVAILABLE:
return
logger.info("Cleaning up GPIO buttons...")
try:
# Remove event detection for all buttons
for button in self.buttons:
try:
GPIO.remove_event_detect(button.gpio)
except Exception as e:
logger.debug(f"Error removing event detect for GPIO {button.gpio}: {e}")
# Clean up GPIO
GPIO.cleanup()
logger.info("GPIO buttons cleaned up")
except Exception as e:
logger.error(f"Error during GPIO cleanup: {e}")
self._initialized = False
def load_button_config_from_dict(config: dict, screen_width: int = 1872, screen_height: int = 1404) -> Optional[GPIOButtonHandler]:
"""
Load GPIO button configuration from a dictionary.
Args:
config: Configuration dictionary with 'gpio_buttons' section
screen_width: Screen width for touch coordinates
screen_height: Screen height for touch coordinates
Returns:
GPIOButtonHandler instance if buttons enabled, None otherwise
Example config:
{
"gpio_buttons": {
"enabled": true,
"pull_up": true,
"bounce_time_ms": 200,
"buttons": [
{
"name": "next_page",
"gpio": 23,
"gesture": "swipe_left",
"description": "Next page"
}
]
}
}
"""
gpio_config = config.get("gpio_buttons", {})
if not gpio_config.get("enabled", False):
logger.info("GPIO buttons disabled in config")
return None
if not GPIO_AVAILABLE:
logger.warning("GPIO buttons enabled in config but RPi.GPIO not available")
return None
# Parse button configurations
buttons = []
for btn_cfg in gpio_config.get("buttons", []):
try:
# Parse gesture type
gesture_str = btn_cfg["gesture"]
gesture = GestureType(gesture_str)
button = ButtonConfig(
name=btn_cfg["name"],
gpio=btn_cfg["gpio"],
gesture=gesture,
description=btn_cfg.get("description", "")
)
buttons.append(button)
except Exception as e:
logger.error(f"Error parsing button config: {e}")
logger.error(f" Config: {btn_cfg}")
continue
if not buttons:
logger.warning("No valid button configurations found")
return None
# Create handler
handler = GPIOButtonHandler(
buttons=buttons,
pull_up=gpio_config.get("pull_up", True),
bounce_time_ms=gpio_config.get("bounce_time_ms", 200),
screen_width=screen_width,
screen_height=screen_height,
)
return handler