411 lines
11 KiB
Markdown
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.
|