dreader-application/ACCELEROMETER_PAGE_FLIP.md
2025-11-12 18:52:08 +00:00

411 lines
11 KiB
Markdown

# Accelerometer-Based Page Flipping
This document describes the accelerometer-based page flipping feature that allows users to navigate pages by tilting the device.
## Overview
The accelerometer page flipping feature uses the BMA400 3-axis accelerometer to detect device tilt and automatically turn pages. This provides a hands-free way to read, which is useful when:
- Eating or drinking while reading
- Holding the device with one hand
- Device is mounted (e.g., on a stand)
- Accessibility needs
## Architecture
### Components
1. **Gesture Types** ([dreader/gesture.py](dreader/gesture.py:29-30))
- `TILT_FORWARD` - Tilt device forward to go to next page
- `TILT_BACKWARD` - Tilt device backward to go to previous page
2. **HAL Integration** ([dreader/hal_hardware.py](dreader/hal_hardware.py:414-563))
- `load_accelerometer_calibration()` - Loads calibration from JSON file
- `get_tilt_gesture()` - Polls accelerometer and detects tilt gestures
- Gravity direction calculation based on calibrated "up" vector
- Debouncing to prevent multiple page flips from single tilt
3. **Gesture Handlers** ([dreader/handlers/gestures.py](dreader/handlers/gestures.py:84-87))
- `TILT_FORWARD` → calls `_handle_page_forward()`
- `TILT_BACKWARD` → calls `_handle_page_back()`
- Uses same page navigation logic as swipe gestures
4. **Calibration Tool** ([examples/calibrate_accelerometer.py](examples/calibrate_accelerometer.py))
- Interactive calibration using display
- Shows live arrow pointing in gravity direction
- User rotates device until arrow points "up"
- Saves calibration to JSON file
5. **Demo Application** ([examples/demo_accelerometer_page_flip.py](examples/demo_accelerometer_page_flip.py))
- Complete integration example
- Combines touch and accelerometer gestures
- Shows how to poll both input sources
## How It Works
### Calibration
The calibration process establishes which direction is "up" for the device:
1. Run `python examples/calibrate_accelerometer.py`
2. Device displays an arrow showing gravity direction
3. Rotate device until arrow points up
4. Tap screen to save calibration
5. Calibration stored in `accelerometer_config.json`
**Calibration Data:**
```json
{
"up_vector": {
"x": 0.0,
"y": 9.8,
"z": 0.0
},
"tilt_threshold": 0.3,
"debounce_time": 0.5
}
```
### Tilt Detection Algorithm
The algorithm detects when the device is tilted beyond a threshold angle from the calibrated "up" position:
1. **Read Accelerometer**: Get (x, y, z) acceleration in m/s²
2. **Normalize Vectors**: Normalize both current gravity and calibrated up vector
3. **Calculate Tilt Angle**:
- Project gravity onto plane perpendicular to up vector
- Calculate angle using `atan2(perpendicular_magnitude, vertical_component)`
4. **Compare to Threshold**: Default 0.3 radians (~17 degrees)
5. **Determine Direction**:
- Positive perpendicular y-component → Forward tilt → Next page
- Negative perpendicular y-component → Backward tilt → Previous page
6. **Debounce**: Prevent repeated triggers within debounce time (default 0.5s)
**Math Details:**
Given:
- Up vector (calibrated): `U = (ux, uy, uz)`
- Current gravity: `G = (gx, gy, gz)`
Calculate:
```python
# Dot product: component of G along U
dot = gx*ux + gy*uy + gz*uz
# Perpendicular component
perp = G - dot*U
perp_magnitude = |perp|
# Tilt angle
angle = atan2(perp_magnitude, |dot|)
# Direction (simplified)
if perp_y > 0:
gesture = TILT_FORWARD
else:
gesture = TILT_BACKWARD
```
### Event Loop Integration
The main application event loop polls both touch and accelerometer:
```python
while running:
# Check touch events
touch_event = await hal.get_touch_event()
if touch_event:
handle_gesture(touch_event)
# Check accelerometer tilt (if calibrated)
if calibrated:
tilt_event = await hal.get_tilt_gesture()
if tilt_event:
handle_gesture(tilt_event)
await asyncio.sleep(0.05) # ~20Hz polling
```
## Usage
### 1. Calibration (One-time)
```bash
python examples/calibrate_accelerometer.py
```
This creates `accelerometer_config.json` in the current directory.
### 2. Load Calibration in Your Application
```python
from dreader.hal_hardware import HardwareDisplayHAL
# Create HAL with accelerometer enabled
hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True # Important!
)
await hal.initialize()
# Load calibration
if hal.load_accelerometer_calibration("accelerometer_config.json"):
print("Accelerometer calibrated!")
else:
print("No calibration found - tilt gestures disabled")
```
### 3. Poll for Gestures
**Option A: Unified Event API (Recommended)**
```python
# Main event loop - simplest approach
while True:
# Get event from any source (touch or accelerometer)
event = await hal.get_event()
if event:
response = gesture_router.handle_touch(event)
# ... process response
await asyncio.sleep(0.01)
```
**Option B: Separate Polling (More Control)**
```python
# Main event loop - explicit control
while True:
# Get touch events
touch_event = await hal.get_touch_event()
# Get tilt events (returns None if not calibrated)
tilt_event = await hal.get_tilt_gesture()
# Handle events
if touch_event:
response = gesture_router.handle_touch(touch_event)
# ... process response
if tilt_event:
response = gesture_router.handle_touch(tilt_event)
# ... process response
await asyncio.sleep(0.05)
```
### 4. Run Demo
```bash
# Simple demo using unified API
python examples/demo_accelerometer_simple.py ~/Books/mybook.epub
# Full-featured demo with separate polling
python examples/demo_accelerometer_page_flip.py ~/Books/mybook.epub
```
## Configuration
### Tilt Threshold
Adjust sensitivity by changing `tilt_threshold` in the config file:
- **0.1 rad (~6°)**: Very sensitive, small tilts trigger pages
- **0.3 rad (~17°)**: Default, moderate sensitivity
- **0.5 rad (~29°)**: Less sensitive, requires larger tilt
### Debounce Time
Adjust `debounce_time` to control how quickly you can trigger repeated page flips:
- **0.2s**: Fast, can quickly flip multiple pages
- **0.5s**: Default, prevents accidental double-flips
- **1.0s**: Slow, requires deliberate pauses between flips
### Example Custom Configuration
```json
{
"up_vector": {
"x": 0.0,
"y": 9.8,
"z": 0.0
},
"tilt_threshold": 0.2,
"debounce_time": 0.3
}
```
## Testing
Run the test suite:
```bash
python -m pytest tests/test_accelerometer_gestures.py -v
```
**Tests include:**
- Calibration loading
- Tilt angle calculation (forward, backward, upright)
- Threshold detection
- Gesture type definitions
## Troubleshooting
### "Accelerometer calibration file not found"
Run the calibration script first:
```bash
python examples/calibrate_accelerometer.py
```
### Tilt gestures not working
1. Check accelerometer is enabled in HAL:
```python
hal = HardwareDisplayHAL(enable_orientation=True)
```
2. Verify calibration loaded:
```python
result = hal.load_accelerometer_calibration()
print(f"Calibrated: {result}")
```
3. Check you're polling tilt events:
```python
tilt_event = await hal.get_tilt_gesture()
```
### Tilt too sensitive / not sensitive enough
Edit `accelerometer_config.json` and adjust `tilt_threshold`:
- Lower value = more sensitive
- Higher value = less sensitive
### Pages flip too fast / too slow
Edit `accelerometer_config.json` and adjust `debounce_time`:
- Lower value = faster repeat flips
- Higher value = slower repeat flips
### Wrong direction (forward goes backward)
The tilt direction detection is device-specific. You may need to adjust the direction logic in [dreader/hal_hardware.py](dreader/hal_hardware.py:547-550):
```python
# Current logic (line 547)
if perp_y > 0:
gesture = AppGestureType.TILT_FORWARD
else:
gesture = AppGestureType.TILT_BACKWARD
# Try inverting:
if perp_y < 0: # Changed > to <
gesture = AppGestureType.TILT_FORWARD
else:
gesture = AppGestureType.TILT_BACKWARD
```
## Limitations
1. **Orientation Lock**: Tilt detection assumes fixed device orientation. Auto-rotation may interfere.
2. **Walking/Movement**: May trigger false positives when walking. Use higher threshold or disable while moving.
3. **Calibration Drift**: Accelerometer may drift over time. Re-calibrate periodically.
4. **Direction Heuristic**: Current direction detection is simplified. Complex orientations may not work correctly.
5. **Single Axis**: Only detects tilt in one plane. Doesn't distinguish left/right tilts.
## Future Improvements
- [ ] Shake gesture to open TOC/settings
- [ ] Multi-axis tilt for 4-direction navigation
- [ ] Auto-calibration on startup
- [ ] Gyroscope integration for rotation gestures
- [ ] Adaptive threshold based on reading posture
- [ ] Tilt gesture visualization for debugging
## API Reference
### HardwareDisplayHAL
#### `load_accelerometer_calibration(config_path: str = "accelerometer_config.json") -> bool`
Load accelerometer calibration from JSON file.
**Parameters:**
- `config_path`: Path to calibration JSON file
**Returns:**
- `True` if calibration loaded successfully, `False` otherwise
#### `async get_event() -> Optional[TouchEvent]`
**[Recommended]** 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.
**Returns:**
- `TouchEvent` from either touch sensor or accelerometer
- `None` if no event available
- Touch events are prioritized over tilt events
**Example:**
```python
while running:
event = await hal.get_event()
if event:
handle_gesture(event)
await asyncio.sleep(0.01)
```
#### `async get_tilt_gesture() -> Optional[TouchEvent]`
Poll accelerometer and check for tilt gestures.
**Returns:**
- `TouchEvent` with `TILT_FORWARD` or `TILT_BACKWARD` gesture if tilt detected
- `None` if no tilt, not calibrated, or within debounce period
**Note:** Must call `load_accelerometer_calibration()` first. Consider using `get_event()` instead for simpler code.
### GestureType
#### `TILT_FORWARD = "tilt_forward"`
Gesture type for forward tilt (next page)
#### `TILT_BACKWARD = "tilt_backward"`
Gesture type for backward tilt (previous page)
### Calibration File Format
```json
{
"up_vector": {
"x": float, // X-component of gravity when upright (m/s²)
"y": float, // Y-component of gravity when upright (m/s²)
"z": float // Z-component of gravity when upright (m/s²)
},
"tilt_threshold": float, // Tilt angle threshold in radians
"debounce_time": float // Minimum time between gestures in seconds
}
```
## Examples
See the `examples/` directory for complete working examples:
- **[calibrate_accelerometer.py](examples/calibrate_accelerometer.py)** - Interactive calibration tool
- **[demo_accelerometer_simple.py](examples/demo_accelerometer_simple.py)** - Simple demo using unified `get_event()` API
- **[demo_accelerometer_page_flip.py](examples/demo_accelerometer_page_flip.py)** - Full-featured demo with separate event polling
## License
Same as the main DReader project.