Files
dreader-hal/src/dreader_hal/touch/ft5xx6.py
T

341 lines
11 KiB
Python

"""
FT5xx6 Capacitive Touch Controller Wrapper.
This module wraps the FT5xx6 touch panel controller for use with the DReader HAL.
Implements gesture detection from raw touch events using polling mode (not interrupts).
Hardware: FT5316 and similar FT5xx6 family capacitive touch controllers
"""
import asyncio
import sys
import os
import time
from typing import Optional
from pathlib import Path
# Add external PyFTtxx6 to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../external/PyFTtxx6/pyft5xx6'))
from pyft5xx6.controller import FT5316, Status, Mode, Gestures as FT5xx6Gestures
from ..gesture import GestureDetector, TouchState
from ..types import TouchEvent, GestureType
from ..calibration import TouchCalibration
# Mapping from FT5xx6 hardware gestures to our GestureTypes
FT5XX6_GESTURE_MAP = {
FT5xx6Gestures.MOVE_UP: GestureType.SWIPE_UP,
FT5xx6Gestures.MOVE_DOWN: GestureType.SWIPE_DOWN,
FT5xx6Gestures.MOVE_LEFT: GestureType.SWIPE_LEFT,
FT5xx6Gestures.MOVE_RIGHT: GestureType.SWIPE_RIGHT,
FT5xx6Gestures.ZOOM_IN: GestureType.PINCH_OUT,
FT5xx6Gestures.ZOOM_OUT: GestureType.PINCH_IN,
}
class FT5xx6TouchDriver:
"""
Wrapper for FT5xx6 capacitive touch controller.
Provides async touch event handling with gesture detection for DReader HAL.
Uses POLLING mode (not interrupts) as specified by user requirements.
Args:
i2c_bus: I2C bus number (default 1)
i2c_address: I2C device address (default 0x38 for FT5316)
width: Display width for coordinate validation
height: Display height for coordinate validation
polling_interval: Seconds between polls (default 0.01 = 10ms)
"""
def __init__(
self,
i2c_bus: int = 1,
i2c_address: int = 0x38,
width: int = 800,
height: int = 1200,
polling_interval: float = 0.01,
calibration_file: Optional[str] = None,
):
self.i2c_bus = i2c_bus
self.i2c_address = i2c_address
self.width = width
self.height = height
self.polling_interval = polling_interval
self.controller: Optional[FT5316] = None
self.gesture_detector = GestureDetector()
# Touchscreen calibration
self.calibration = TouchCalibration(width, height)
self.calibration_file = calibration_file or str(
Path.home() / '.config' / 'dreader' / 'touch_calibration.json'
)
self._initialized = False
self._last_num_touches = 0
self._tracking_touch = False
self._last_touch_pos = (0, 0) # Track last known touch position for drag_end
async def initialize(self) -> None:
"""
Initialize the FT5xx6 touch controller.
Sets up I2C communication and configures POLLING mode.
Loads calibration data if available.
"""
if self._initialized:
return
# Run initialization in thread pool
loop = asyncio.get_event_loop()
success = await loop.run_in_executor(None, self._init_controller)
if not success:
raise RuntimeError("Failed to initialize FT5xx6 touch controller")
# Load calibration if available
if Path(self.calibration_file).exists():
loaded = self.calibration.load(self.calibration_file)
if loaded:
print(f"Touch calibration loaded: {self.calibration.get_calibration_quality()} "
f"(RMS error: {self.calibration.calibration_data.rms_error:.2f}px)")
else:
print("Warning: Failed to load touch calibration")
else:
print(f"No calibration file found at {self.calibration_file}")
print("Touch coordinates will be uncalibrated. Run calibrate_touch.py to calibrate.")
self._initialized = True
def _init_controller(self) -> bool:
"""Blocking initialization of touch controller (runs in thread pool)."""
self.controller = FT5316()
# Initialize in POLLING mode (NO interrupts as per user requirement)
result = self.controller.begin(i2c_bus=self.i2c_bus)
if result != Status.NOMINAL:
return False
# Explicitly set polling mode
self.controller.set_mode(Mode.POLLING)
return True
async def cleanup(self) -> None:
"""
Cleanup touch controller resources.
"""
if not self._initialized or not self.controller:
return
self._initialized = False
self.controller = None
async def get_touch_event(self) -> Optional[TouchEvent]:
"""
Get the next touch event with gesture classification.
This method polls the touch controller and uses a GestureDetector
to classify raw touch data into gestures (tap, swipe, long press, etc.).
Returns:
TouchEvent if a gesture is detected, None otherwise
Implementation note:
Uses POLLING mode with configurable polling interval.
Implements gesture detection per HAL spec section 4.3.
"""
if not self._initialized or not self.controller:
return None
# Poll touch controller
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.controller.update)
# Check if new touch data available
if not self.controller.new_touch:
# No touch event - but check for long press timeout
if self._tracking_touch and self.gesture_detector.check_long_press():
# Long press detected!
event = self._create_touch_event(
GestureType.LONG_PRESS,
self.gesture_detector.current_pos
)
# Reset detector since we consumed the long press
self.gesture_detector.reset()
self._tracking_touch = False
return event
# Small delay to avoid busy-waiting
await asyncio.sleep(self.polling_interval)
return None
# Read touch data
record = self.controller.read()
# Process touch state changes
gesture = self._process_touch_record(record)
if gesture:
# Create TouchEvent from gesture
return self._create_touch_event_from_record(gesture, record)
# Small delay between polls
await asyncio.sleep(self.polling_interval)
return None
def _process_touch_record(self, record) -> Optional[GestureType]:
"""
Process raw touch record and update gesture detector.
Args:
record: TouchRecord from FT5xx6 controller
Returns:
GestureType if gesture detected, None otherwise
"""
num_touches = record.num_touches
# Check for hardware-detected gestures first
if record.gesture in FT5XX6_GESTURE_MAP:
# Use hardware gesture detection for pinch/zoom
return FT5XX6_GESTURE_MAP[record.gesture]
# Handle single-touch gestures with our gesture detector
if num_touches == 1:
x, y = record.t1x, record.t1y
self._last_touch_pos = (x, y) # Track position for drag_end
# Touch down
if self._last_num_touches == 0:
self.gesture_detector.on_touch_down(x, y)
self._tracking_touch = True
# Touch move
else:
move_gesture = self.gesture_detector.on_touch_move(x, y)
if move_gesture:
return move_gesture
# Handle two-finger gestures (pinch)
elif num_touches == 2:
# Primary finger
if self._last_num_touches < 2:
self.gesture_detector.on_touch_down(record.t1x, record.t1y, finger=0)
self.gesture_detector.on_touch_down(record.t2x, record.t2y, finger=1)
self._tracking_touch = True
else:
self.gesture_detector.on_touch_move(record.t1x, record.t1y, finger=0)
self.gesture_detector.on_touch_move(record.t2x, record.t2y, finger=1)
# Touch up
elif num_touches == 0 and self._last_num_touches > 0:
# Get last known position
if self._last_num_touches == 1:
# Single finger up - use last tracked position
gesture = self.gesture_detector.on_touch_up(
self._last_touch_pos[0],
self._last_touch_pos[1],
finger=0
)
self._tracking_touch = False
self._last_num_touches = 0
return gesture
elif self._last_num_touches == 2:
# Two fingers - check which one lifted
# Assume second finger lifted first
gesture = self.gesture_detector.on_touch_up(0, 0, finger=1)
self._last_num_touches = 0
self._tracking_touch = False
return gesture
self._last_num_touches = num_touches
return None
def _create_touch_event(
self,
gesture: GestureType,
pos: Optional[tuple]
) -> TouchEvent:
"""
Create TouchEvent from gesture and position.
Args:
gesture: Detected gesture type
pos: Touch position (x, y) or None
Returns:
TouchEvent object with calibrated coordinates
"""
x = pos[0] if pos else 0
y = pos[1] if pos else 0
# Apply calibration transformation
x, y = self.calibration.transform(x, y)
return TouchEvent(
gesture=gesture,
x=x,
y=y,
timestamp_ms=time.time() * 1000
)
def _create_touch_event_from_record(
self,
gesture: GestureType,
record
) -> TouchEvent:
"""
Create TouchEvent from gesture and TouchRecord.
Args:
gesture: Detected gesture type
record: FT5xx6 TouchRecord
Returns:
TouchEvent object with calibrated coordinates
"""
# Use primary touch position
x = record.t1x
y = record.t1y
# Apply calibration transformation
x, y = self.calibration.transform(x, y)
# Check for two-finger gestures (pinch)
if gesture in (GestureType.PINCH_IN, GestureType.PINCH_OUT) and record.num_touches >= 2:
# Apply calibration to second finger as well
x2, y2 = self.calibration.transform(record.t2x, record.t2y)
return TouchEvent(
gesture=gesture,
x=x,
y=y,
x2=x2,
y2=y2,
timestamp_ms=record.timestamp
)
return TouchEvent(
gesture=gesture,
x=x,
y=y,
timestamp_ms=record.timestamp
)
async def set_polling_rate(self, rate_hz: int) -> None:
"""
Set touch polling rate.
Args:
rate_hz: Polling frequency in Hz (e.g., 100 = 100 times/second)
"""
if rate_hz <= 0:
raise ValueError("Polling rate must be positive")
self.polling_interval = 1.0 / rate_hz