# 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.