""" 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