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

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

  1. Gesture Types (dreader/gesture.py)

    • 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)

    • 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)

    • 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)

    • 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)

    • 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:

{
  "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:

# 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

  1. Check accelerometer is enabled in HAL:

    hal = HardwareDisplayHAL(enable_orientation=True)
    
  2. Verify calibration loaded:

    result = hal.load_accelerometer_calibration()
    print(f"Calibrated: {result}")
    
  3. 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

  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:

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

{
  "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:

License

Same as the main DReader project.