11 KiB
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
-
Gesture Types (dreader/gesture.py)
TILT_FORWARD- Tilt device forward to go to next pageTILT_BACKWARD- Tilt device backward to go to previous page
-
HAL Integration (dreader/hal_hardware.py)
load_accelerometer_calibration()- Loads calibration from JSON fileget_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
-
Gesture Handlers (dreader/handlers/gestures.py)
TILT_FORWARD→ calls_handle_page_forward()TILT_BACKWARD→ calls_handle_page_back()- Uses same page navigation logic as swipe gestures
-
Calibration Tool (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
-
Demo Application (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:
- Run
python examples/calibrate_accelerometer.py - Device displays an arrow showing gravity direction
- Rotate device until arrow points up
- Tap screen to save calibration
- Calibration stored in
accelerometer_config.json
Calibration Data:
{
"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:
- Read Accelerometer: Get (x, y, z) acceleration in m/s²
- Normalize Vectors: Normalize both current gravity and calibrated up vector
- Calculate Tilt Angle:
- Project gravity onto plane perpendicular to up vector
- Calculate angle using
atan2(perpendicular_magnitude, vertical_component)
- Compare to Threshold: Default 0.3 radians (~17 degrees)
- Determine Direction:
- Positive perpendicular y-component → Forward tilt → Next page
- Negative perpendicular y-component → Backward tilt → Previous page
- 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:
# 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:
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)
python examples/calibrate_accelerometer.py
This creates accelerometer_config.json in the current directory.
2. Load Calibration in Your Application
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)
# 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)
# 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
# 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
{
"up_vector": {
"x": 0.0,
"y": 9.8,
"z": 0.0
},
"tilt_threshold": 0.2,
"debounce_time": 0.3
}
Testing
Run the test suite:
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:
python examples/calibrate_accelerometer.py
Tilt gestures not working
-
Check accelerometer is enabled in HAL:
hal = HardwareDisplayHAL(enable_orientation=True) -
Verify calibration loaded:
result = hal.load_accelerometer_calibration() print(f"Calibrated: {result}") -
Check you're polling tilt events:
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:
# 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
-
Orientation Lock: Tilt detection assumes fixed device orientation. Auto-rotation may interfere.
-
Walking/Movement: May trigger false positives when walking. Use higher threshold or disable while moving.
-
Calibration Drift: Accelerometer may drift over time. Re-calibrate periodically.
-
Direction Heuristic: Current direction detection is simplified. Complex orientations may not work correctly.
-
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:
Trueif calibration loaded successfully,Falseotherwise
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:
TouchEventfrom either touch sensor or accelerometerNoneif no event available- Touch events are prioritized over tilt events
Example:
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:
TouchEventwithTILT_FORWARDorTILT_BACKWARDgesture if tilt detectedNoneif 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
{
"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 - Interactive calibration tool
- demo_accelerometer_simple.py - Simple demo using unified
get_event()API - demo_accelerometer_page_flip.py - Full-featured demo with separate event polling
License
Same as the main DReader project.