292 lines
8.9 KiB
Python
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
|