HW integratation
This commit is contained in:
@@ -25,6 +25,10 @@ class GestureType(Enum):
|
||||
DRAG_MOVE = "drag_move" # Continue dragging
|
||||
DRAG_END = "drag_end" # End dragging/selection
|
||||
|
||||
# Accelerometer-based gestures
|
||||
TILT_FORWARD = "tilt_forward" # Tilt device forward (page forward)
|
||||
TILT_BACKWARD = "tilt_backward" # Tilt device backward (page back)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TouchEvent:
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,593 @@
|
||||
"""
|
||||
Hardware HAL implementation using dreader-hal library.
|
||||
|
||||
This module provides the HardwareDisplayHAL class that bridges the DReader
|
||||
application HAL interface with the dreader-hal hardware abstraction layer.
|
||||
|
||||
The dreader-hal library provides complete e-ink display integration with:
|
||||
- IT8951 e-ink display driver
|
||||
- FT5xx6 capacitive touch sensor
|
||||
- BMA400 accelerometer (orientation)
|
||||
- PCF8523 RTC (timekeeping)
|
||||
- INA219 power monitor (battery)
|
||||
|
||||
Usage:
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
|
||||
# For real hardware
|
||||
hal = HardwareDisplayHAL(width=800, height=1200, vcom=-2.0)
|
||||
|
||||
# For testing without hardware
|
||||
hal = HardwareDisplayHAL(
|
||||
width=800,
|
||||
height=1200,
|
||||
virtual_display=True,
|
||||
enable_orientation=False,
|
||||
enable_rtc=False,
|
||||
enable_power_monitor=False
|
||||
)
|
||||
|
||||
config = AppConfig(display_hal=hal, library_path="~/Books")
|
||||
app = DReaderApplication(config)
|
||||
|
||||
await hal.initialize()
|
||||
await app.start()
|
||||
|
||||
# Main loop
|
||||
while app.is_running():
|
||||
event = await hal.get_touch_event()
|
||||
if event:
|
||||
await app.handle_touch(event)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
await app.shutdown()
|
||||
await hal.cleanup()
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
from .hal import DisplayHAL
|
||||
from .gesture import TouchEvent as AppTouchEvent, GestureType as AppGestureType
|
||||
|
||||
# Import dreader-hal components
|
||||
try:
|
||||
from dreader_hal import (
|
||||
EReaderDisplayHAL,
|
||||
TouchEvent as HalTouchEvent,
|
||||
GestureType as HalGestureType,
|
||||
RefreshMode,
|
||||
PowerStats,
|
||||
Orientation
|
||||
)
|
||||
DREADER_HAL_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
DREADER_HAL_AVAILABLE = False
|
||||
_import_error = e
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Gesture type mapping between dreader-hal and dreader-application
|
||||
GESTURE_TYPE_MAP = {
|
||||
HalGestureType.TAP: AppGestureType.TAP,
|
||||
HalGestureType.LONG_PRESS: AppGestureType.LONG_PRESS,
|
||||
HalGestureType.SWIPE_LEFT: AppGestureType.SWIPE_LEFT,
|
||||
HalGestureType.SWIPE_RIGHT: AppGestureType.SWIPE_RIGHT,
|
||||
HalGestureType.SWIPE_UP: AppGestureType.SWIPE_UP,
|
||||
HalGestureType.SWIPE_DOWN: AppGestureType.SWIPE_DOWN,
|
||||
HalGestureType.PINCH_IN: AppGestureType.PINCH_IN,
|
||||
HalGestureType.PINCH_OUT: AppGestureType.PINCH_OUT,
|
||||
HalGestureType.DRAG_START: AppGestureType.DRAG_START,
|
||||
HalGestureType.DRAG_MOVE: AppGestureType.DRAG_MOVE,
|
||||
HalGestureType.DRAG_END: AppGestureType.DRAG_END,
|
||||
}
|
||||
|
||||
|
||||
class HardwareDisplayHAL(DisplayHAL):
|
||||
"""
|
||||
Hardware HAL implementation using dreader-hal library.
|
||||
|
||||
This class adapts the dreader-hal EReaderDisplayHAL to work with the
|
||||
DReader application's DisplayHAL interface.
|
||||
|
||||
Args:
|
||||
width: Display width in pixels (default 1872)
|
||||
height: Display height in pixels (default 1404)
|
||||
vcom: E-ink VCOM voltage (default -2.0, check device label!)
|
||||
spi_hz: SPI clock frequency (default 24MHz)
|
||||
virtual_display: Use virtual display for testing (default False)
|
||||
auto_sleep_display: Auto-sleep display after updates (default True)
|
||||
enable_orientation: Enable orientation sensing (default True)
|
||||
enable_rtc: Enable RTC timekeeping (default True)
|
||||
enable_power_monitor: Enable battery monitoring (default True)
|
||||
shunt_ohms: Power monitor shunt resistor (default 0.1)
|
||||
battery_capacity_mah: Battery capacity in mAh (default 3000)
|
||||
|
||||
Example:
|
||||
# For real hardware (Raspberry Pi with e-ink display)
|
||||
hal = HardwareDisplayHAL(width=1872, height=1404, vcom=-2.0)
|
||||
|
||||
# For testing on development machine
|
||||
hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
virtual_display=True,
|
||||
enable_orientation=False,
|
||||
enable_rtc=False,
|
||||
enable_power_monitor=False
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int = 1872,
|
||||
height: int = 1404,
|
||||
vcom: float = -2.0,
|
||||
spi_hz: int = 24_000_000,
|
||||
virtual_display: bool = False,
|
||||
auto_sleep_display: bool = True,
|
||||
enable_orientation: bool = True,
|
||||
enable_rtc: bool = True,
|
||||
enable_power_monitor: bool = True,
|
||||
shunt_ohms: float = 0.1,
|
||||
battery_capacity_mah: float = 3000,
|
||||
):
|
||||
"""
|
||||
Initialize hardware HAL.
|
||||
|
||||
Raises:
|
||||
ImportError: If dreader-hal library is not installed
|
||||
"""
|
||||
if not DREADER_HAL_AVAILABLE:
|
||||
raise ImportError(
|
||||
f"dreader-hal library is required for HardwareDisplayHAL.\n"
|
||||
f"Install with: pip install -e external/dreader-hal\n"
|
||||
f"Original error: {_import_error}"
|
||||
)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
logger.info(f"Initializing HardwareDisplayHAL: {width}x{height}")
|
||||
logger.info(f" VCOM: {vcom}V")
|
||||
logger.info(f" Virtual display: {virtual_display}")
|
||||
logger.info(f" Orientation: {enable_orientation}")
|
||||
logger.info(f" RTC: {enable_rtc}")
|
||||
logger.info(f" Power monitor: {enable_power_monitor}")
|
||||
|
||||
# Create the underlying dreader-hal implementation
|
||||
self.hal = EReaderDisplayHAL(
|
||||
width=width,
|
||||
height=height,
|
||||
vcom=vcom,
|
||||
spi_hz=spi_hz,
|
||||
virtual_display=virtual_display,
|
||||
auto_sleep_display=auto_sleep_display,
|
||||
enable_orientation=enable_orientation,
|
||||
enable_rtc=enable_rtc,
|
||||
enable_power_monitor=enable_power_monitor,
|
||||
shunt_ohms=shunt_ohms,
|
||||
battery_capacity_mah=battery_capacity_mah,
|
||||
)
|
||||
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Initialize all hardware components.
|
||||
|
||||
This initializes:
|
||||
- E-ink display controller
|
||||
- Touch sensor
|
||||
- Accelerometer (if enabled)
|
||||
- RTC (if enabled)
|
||||
- Power monitor (if enabled)
|
||||
"""
|
||||
if self._initialized:
|
||||
logger.warning("Hardware HAL already initialized")
|
||||
return
|
||||
|
||||
logger.info("Initializing hardware components...")
|
||||
await self.hal.initialize()
|
||||
self._initialized = True
|
||||
logger.info("Hardware HAL initialized successfully")
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up all hardware resources."""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Cleaning up hardware HAL")
|
||||
await self.hal.cleanup()
|
||||
self._initialized = False
|
||||
logger.info("Hardware HAL cleaned up")
|
||||
|
||||
async def show_image(self, image: Image.Image):
|
||||
"""
|
||||
Display a PIL Image on the e-ink screen.
|
||||
|
||||
Args:
|
||||
image: PIL Image to display
|
||||
|
||||
The dreader-hal library handles:
|
||||
- Format conversion (RGB -> grayscale)
|
||||
- Dithering for e-ink
|
||||
- Refresh mode selection (auto, fast, quality, full)
|
||||
- Orientation rotation (if enabled)
|
||||
"""
|
||||
if not self._initialized:
|
||||
logger.warning("Hardware HAL not initialized, initializing now...")
|
||||
await self.initialize()
|
||||
|
||||
logger.debug(f"Displaying image: {image.size} {image.mode}")
|
||||
await self.hal.show_image(image)
|
||||
|
||||
async def get_touch_event(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Get the next touch event from hardware.
|
||||
|
||||
Returns:
|
||||
TouchEvent if available, None if no event
|
||||
|
||||
The dreader-hal library handles gesture classification:
|
||||
- TAP: Quick tap (< 30px movement, < 300ms)
|
||||
- LONG_PRESS: Hold (< 30px movement, >= 500ms)
|
||||
- SWIPE_*: Directional swipes (>= 30px movement)
|
||||
- PINCH_IN/OUT: Two-finger pinch gestures
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
# Get event from dreader-hal
|
||||
hal_event = await self.hal.get_touch_event()
|
||||
|
||||
if hal_event is None:
|
||||
return None
|
||||
|
||||
# Convert from dreader-hal TouchEvent to application TouchEvent
|
||||
app_gesture = GESTURE_TYPE_MAP.get(hal_event.gesture)
|
||||
|
||||
if app_gesture is None:
|
||||
logger.warning(f"Unknown gesture type from HAL: {hal_event.gesture}")
|
||||
return None
|
||||
|
||||
logger.debug(f"Touch event: {app_gesture.value} at ({hal_event.x}, {hal_event.y})")
|
||||
|
||||
return AppTouchEvent(
|
||||
gesture=app_gesture,
|
||||
x=hal_event.x,
|
||||
y=hal_event.y
|
||||
)
|
||||
|
||||
async def set_brightness(self, level: int):
|
||||
"""
|
||||
Set display brightness.
|
||||
|
||||
Args:
|
||||
level: Brightness level (0-10)
|
||||
|
||||
Note:
|
||||
Basic IT8951 e-ink displays don't have brightness control.
|
||||
This is a no-op unless frontlight hardware is connected.
|
||||
"""
|
||||
if not 0 <= level <= 10:
|
||||
raise ValueError("Brightness must be 0-10")
|
||||
|
||||
logger.debug(f"Setting brightness to {level}")
|
||||
await self.hal.set_brightness(level)
|
||||
|
||||
# ========== Extended Methods (Hardware-Specific Features) ==========
|
||||
|
||||
async def get_battery_level(self) -> float:
|
||||
"""
|
||||
Get battery percentage.
|
||||
|
||||
Returns:
|
||||
Battery level 0-100%, or 0.0 if power monitor unavailable
|
||||
"""
|
||||
if not self._initialized:
|
||||
return 0.0
|
||||
|
||||
return await self.hal.get_battery_level()
|
||||
|
||||
async def get_power_stats(self) -> PowerStats:
|
||||
"""
|
||||
Get detailed power statistics.
|
||||
|
||||
Returns:
|
||||
PowerStats with voltage, current, power, battery %, etc.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If power monitor not enabled
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Hardware HAL not initialized")
|
||||
|
||||
return await self.hal.get_power_stats()
|
||||
|
||||
async def is_low_battery(self, threshold: float = 20.0) -> bool:
|
||||
"""
|
||||
Check if battery is low.
|
||||
|
||||
Args:
|
||||
threshold: Battery percentage threshold (default 20%)
|
||||
|
||||
Returns:
|
||||
True if battery below threshold, False otherwise
|
||||
"""
|
||||
if not self._initialized:
|
||||
return False
|
||||
|
||||
return await self.hal.is_low_battery(threshold)
|
||||
|
||||
async def set_low_power_mode(self, enabled: bool):
|
||||
"""
|
||||
Enable/disable low power mode.
|
||||
|
||||
In low power mode:
|
||||
- Display goes to sleep
|
||||
- Touch polling rate reduced
|
||||
- Sensors put to low power
|
||||
|
||||
Args:
|
||||
enabled: True to enable low power mode
|
||||
"""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info(f"Setting low power mode: {enabled}")
|
||||
await self.hal.set_low_power_mode(enabled)
|
||||
|
||||
async def enable_orientation_monitoring(self):
|
||||
"""
|
||||
Start monitoring device orientation changes.
|
||||
|
||||
When orientation changes, display auto-rotates.
|
||||
"""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Enabling orientation monitoring")
|
||||
await self.hal.enable_orientation_monitoring()
|
||||
|
||||
async def disable_orientation_monitoring(self):
|
||||
"""Stop monitoring orientation changes."""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Disabling orientation monitoring")
|
||||
await self.hal.disable_orientation_monitoring()
|
||||
|
||||
@property
|
||||
def current_orientation(self) -> Optional[Orientation]:
|
||||
"""Get current device orientation."""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
return self.hal.current_orientation
|
||||
|
||||
@property
|
||||
def refresh_count(self) -> int:
|
||||
"""Get number of display refreshes since initialization."""
|
||||
if not self._initialized:
|
||||
return 0
|
||||
|
||||
return self.hal.refresh_count
|
||||
|
||||
async def get_datetime(self):
|
||||
"""
|
||||
Get current date/time from RTC.
|
||||
|
||||
Returns:
|
||||
struct_time with current date and time, or None if RTC unavailable
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
return await self.hal.get_datetime()
|
||||
|
||||
async def set_datetime(self, dt):
|
||||
"""
|
||||
Set the RTC date/time.
|
||||
|
||||
Args:
|
||||
dt: time.struct_time object with date and time to set
|
||||
|
||||
Raises:
|
||||
RuntimeError: If RTC not enabled
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Hardware HAL not initialized")
|
||||
|
||||
await self.hal.set_datetime(dt)
|
||||
|
||||
# ========== Accelerometer Tilt Detection ==========
|
||||
|
||||
def load_accelerometer_calibration(self, config_path: str = "accelerometer_config.json") -> bool:
|
||||
"""
|
||||
Load accelerometer calibration from file.
|
||||
|
||||
Args:
|
||||
config_path: Path to calibration JSON file
|
||||
|
||||
Returns:
|
||||
True if calibration loaded successfully, False otherwise
|
||||
"""
|
||||
config_file = Path(config_path)
|
||||
if not config_file.exists():
|
||||
logger.warning(f"Accelerometer calibration file not found: {config_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Load up vector
|
||||
up = config.get("up_vector", {})
|
||||
self.accel_up_vector = (up.get("x", 0), up.get("y", 0), up.get("z", 0))
|
||||
|
||||
# Load thresholds
|
||||
self.accel_tilt_threshold = config.get("tilt_threshold", 0.3)
|
||||
self.accel_debounce_time = config.get("debounce_time", 0.5)
|
||||
|
||||
# State tracking
|
||||
self.accel_last_tilt_time = 0
|
||||
|
||||
logger.info(f"Accelerometer calibration loaded: up_vector={self.accel_up_vector}")
|
||||
logger.info(f" Tilt threshold: {self.accel_tilt_threshold:.2f} rad (~{math.degrees(self.accel_tilt_threshold):.1f}°)")
|
||||
logger.info(f" Debounce time: {self.accel_debounce_time:.2f}s")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading accelerometer calibration: {e}")
|
||||
return False
|
||||
|
||||
async def get_tilt_gesture(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Check accelerometer for tilt gestures.
|
||||
|
||||
Returns:
|
||||
TouchEvent with TILT_FORWARD or TILT_BACKWARD gesture if detected,
|
||||
None otherwise
|
||||
|
||||
Note:
|
||||
Requires accelerometer calibration to be loaded first via
|
||||
load_accelerometer_calibration()
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
if not self.hal.orientation:
|
||||
return None
|
||||
|
||||
if not hasattr(self, 'accel_up_vector'):
|
||||
return None
|
||||
|
||||
# Get current acceleration
|
||||
try:
|
||||
ax, ay, az = await self.hal.orientation.get_acceleration()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error reading accelerometer: {e}")
|
||||
return None
|
||||
|
||||
# Check debounce
|
||||
current_time = time.time()
|
||||
if current_time - self.accel_last_tilt_time < self.accel_debounce_time:
|
||||
return None
|
||||
|
||||
# Calculate angle between current gravity and calibrated "up" vector
|
||||
# Gravity vector is the acceleration (pointing down)
|
||||
gx, gy, gz = ax, ay, az
|
||||
|
||||
# Normalize gravity
|
||||
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
|
||||
if g_mag < 0.1:
|
||||
return None
|
||||
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
|
||||
|
||||
# Normalize up vector
|
||||
ux, uy, uz = self.accel_up_vector
|
||||
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
|
||||
if u_mag < 0.1:
|
||||
return None
|
||||
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
|
||||
|
||||
# Calculate tilt: project gravity onto the "forward/backward" axis
|
||||
# Forward/backward axis is perpendicular to up vector
|
||||
# We'll use the component of gravity that's perpendicular to the up vector
|
||||
|
||||
# Dot product: component of gravity along up vector
|
||||
dot_up = gx * ux + gy * uy + gz * uz
|
||||
|
||||
# Component of gravity perpendicular to up vector
|
||||
perp_x = gx - dot_up * ux
|
||||
perp_y = gy - dot_up * uy
|
||||
perp_z = gz - dot_up * uz
|
||||
|
||||
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
|
||||
|
||||
# Angle from vertical (in radians)
|
||||
tilt_angle = math.atan2(perp_mag, abs(dot_up))
|
||||
|
||||
logger.debug(f"Tilt angle: {math.degrees(tilt_angle):.1f}° (threshold: {math.degrees(self.accel_tilt_threshold):.1f}°)")
|
||||
|
||||
# Check if tilted beyond threshold
|
||||
if tilt_angle < self.accel_tilt_threshold:
|
||||
return None
|
||||
|
||||
# Determine direction: forward or backward
|
||||
# We need to determine which direction the device is tilted
|
||||
# Use the sign of the perpendicular component along a reference axis
|
||||
|
||||
# For simplicity, we'll use the projection onto the original up vector's
|
||||
# perpendicular plane. If we tilt "forward", the gravity vector should
|
||||
# rotate in a specific direction.
|
||||
|
||||
# Calculate which direction: check if tilting away from or toward the up vector
|
||||
# If dot_up is decreasing (device tilting away from up), that's "forward"
|
||||
# If dot_up is increasing (device tilting back toward up), that's "backward"
|
||||
|
||||
# Actually, a simpler approach: check the direction of the perpendicular component
|
||||
# relative to a reference direction in the plane
|
||||
|
||||
# Let's define forward as tilting in the direction that increases the
|
||||
# y-component of acceleration (assuming standard orientation)
|
||||
# This is device-specific and may need adjustment
|
||||
|
||||
# For now, use a simple heuristic: forward = positive perpendicular y component
|
||||
if perp_y > 0:
|
||||
gesture = AppGestureType.TILT_FORWARD
|
||||
else:
|
||||
gesture = AppGestureType.TILT_BACKWARD
|
||||
|
||||
# Update debounce timer
|
||||
self.accel_last_tilt_time = current_time
|
||||
|
||||
logger.info(f"Tilt gesture detected: {gesture.value} (angle: {math.degrees(tilt_angle):.1f}°)")
|
||||
|
||||
# Return gesture at center of screen (x, y don't matter for tilt)
|
||||
return AppTouchEvent(
|
||||
gesture=gesture,
|
||||
x=self.width // 2,
|
||||
y=self.height // 2,
|
||||
timestamp_ms=current_time * 1000
|
||||
)
|
||||
|
||||
async def get_event(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Get the next event from any input source (touch or accelerometer).
|
||||
|
||||
This is a convenience method that polls both touch and accelerometer
|
||||
in a single call, prioritizing touch events over tilt events.
|
||||
|
||||
Returns:
|
||||
TouchEvent from either touch sensor or accelerometer, or None if no event
|
||||
|
||||
Usage:
|
||||
while running:
|
||||
event = await hal.get_event()
|
||||
if event:
|
||||
handle_gesture(event)
|
||||
await asyncio.sleep(0.01)
|
||||
"""
|
||||
# Check touch first (higher priority)
|
||||
touch_event = await self.get_touch_event()
|
||||
if touch_event:
|
||||
return touch_event
|
||||
|
||||
# Check accelerometer tilt
|
||||
if hasattr(self, 'accel_up_vector'):
|
||||
tilt_event = await self.get_tilt_gesture()
|
||||
if tilt_event:
|
||||
return tilt_event
|
||||
|
||||
return None
|
||||
@@ -81,6 +81,10 @@ class GestureRouter:
|
||||
return self._handle_selection_move(event.x, event.y)
|
||||
elif event.gesture == GestureType.DRAG_END:
|
||||
return self._handle_selection_end(event.x, event.y)
|
||||
elif event.gesture == GestureType.TILT_FORWARD:
|
||||
return self._handle_page_forward()
|
||||
elif event.gesture == GestureType.TILT_BACKWARD:
|
||||
return self._handle_page_back()
|
||||
|
||||
return GestureResponse(ActionType.NONE, {})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user