HW integratation
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user