dreader-application/tests/test_accelerometer_gestures.py
2025-11-12 18:52:08 +00:00

285 lines
7.7 KiB
Python

"""
Tests for accelerometer-based gesture detection.
"""
import pytest
import asyncio
import json
import math
from pathlib import Path
import sys
# Test only the gesture types and math, not the full integration
# to avoid dependencies on pyWebLayout
class MockOrientationSensor:
"""Mock BMA400 accelerometer for testing"""
def __init__(self):
self.ax = 0.0
self.ay = 0.0
self.az = 9.8 # Standard gravity
async def get_acceleration(self):
"""Return mock acceleration data"""
return (self.ax, self.ay, self.az)
def set_acceleration(self, x, y, z):
"""Set acceleration for testing"""
self.ax = x
self.ay = y
self.az = z
class MockHAL:
"""Mock HAL for testing"""
def __init__(self):
self.orientation = MockOrientationSensor()
self.width = 800
self.height = 1200
@pytest.fixture
def mock_hal():
"""Create a mock HAL with accelerometer"""
return MockHAL()
@pytest.fixture
def calibration_file(tmp_path):
"""Create a temporary calibration file"""
config = {
"up_vector": {
"x": 0.0,
"y": 9.8,
"z": 0.0
},
"tilt_threshold": 0.3, # ~17 degrees
"debounce_time": 0.5
}
config_path = tmp_path / "test_accel_config.json"
with open(config_path, 'w') as f:
json.dump(config, f)
return str(config_path)
def test_load_calibration_success(mock_hal, calibration_file):
"""Test loading accelerometer calibration"""
# Create a minimal HAL-like object
class TestHAL:
def __init__(self):
self.width = 800
self.height = 1200
test_hal = TestHAL()
# Manually call the load function
result = load_accel_calibration(test_hal, calibration_file)
assert result is True
assert hasattr(test_hal, 'accel_up_vector')
assert test_hal.accel_up_vector == (0.0, 9.8, 0.0)
assert test_hal.accel_tilt_threshold == 0.3
assert test_hal.accel_debounce_time == 0.5
def load_accel_calibration(hal, config_path):
"""Helper function to load calibration (extracted from HardwareDisplayHAL)"""
import logging
logger = logging.getLogger(__name__)
config_file = Path(config_path)
if not config_file.exists():
logger.warning(f"Accelerometer calibration file not found: {config_path}")
return False
try:
with open(config_file, 'r') as f:
config = json.load(f)
# Load up vector
up = config.get("up_vector", {})
hal.accel_up_vector = (up.get("x", 0), up.get("y", 0), up.get("z", 0))
# Load thresholds
hal.accel_tilt_threshold = config.get("tilt_threshold", 0.3)
hal.accel_debounce_time = config.get("debounce_time", 0.5)
# State tracking
hal.accel_last_tilt_time = 0
return True
except Exception as e:
logger.error(f"Error loading accelerometer calibration: {e}")
return False
def test_tilt_detection_forward():
"""Test forward tilt detection"""
# Setup: device is upright (y = 9.8), then tilt forward (z increases)
# Calibrated up vector: (0, 9.8, 0)
# Current gravity: (0, 6, 6) - tilted ~45 degrees forward
up_vector = (0.0, 9.8, 0.0)
current_gravity = (0.0, 6.0, 6.0)
# Normalize vectors
ux, uy, uz = up_vector
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
gx, gy, gz = current_gravity
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
# Calculate tilt angle
dot_up = gx * ux + gy * uy + gz * uz
perp_x = gx - dot_up * ux
perp_y = gy - dot_up * uy
perp_z = gz - dot_up * uz
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
tilt_angle = math.atan2(perp_mag, abs(dot_up))
# Should be approximately 45 degrees (0.785 radians)
assert abs(tilt_angle - 0.785) < 0.1
# Direction: forward tilt should have positive perpendicular y component
# Actually, when tilting forward, gravity vector rotates toward +z
# The perpendicular component should reflect this
def test_tilt_detection_backward():
"""Test backward tilt detection"""
# Setup: device is upright (y = 9.8), then tilt backward (z decreases, negative)
# Calibrated up vector: (0, 9.8, 0)
# Current gravity: (0, 6, -6) - tilted ~45 degrees backward
up_vector = (0.0, 9.8, 0.0)
current_gravity = (0.0, 6.0, -6.0)
# Normalize vectors
ux, uy, uz = up_vector
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
gx, gy, gz = current_gravity
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
# Calculate tilt angle
dot_up = gx * ux + gy * uy + gz * uz
perp_x = gx - dot_up * ux
perp_y = gy - dot_up * uy
perp_z = gz - dot_up * uz
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
tilt_angle = math.atan2(perp_mag, abs(dot_up))
# Should be approximately 45 degrees (0.785 radians)
assert abs(tilt_angle - 0.785) < 0.1
def test_no_tilt_when_upright():
"""Test that no tilt is detected when device is upright"""
# Setup: device is perfectly upright
# Calibrated up vector: (0, 9.8, 0)
# Current gravity: (0, 9.8, 0) - same as calibration
up_vector = (0.0, 9.8, 0.0)
current_gravity = (0.0, 9.8, 0.0)
# Normalize vectors
ux, uy, uz = up_vector
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
gx, gy, gz = current_gravity
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
# Calculate tilt angle
dot_up = gx * ux + gy * uy + gz * uz
perp_x = gx - dot_up * ux
perp_y = gy - dot_up * uy
perp_z = gz - dot_up * uz
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
tilt_angle = math.atan2(perp_mag, abs(dot_up))
# Should be approximately 0 degrees
assert tilt_angle < 0.01
def test_small_tilt_below_threshold():
"""Test that small tilts below threshold are ignored"""
# Setup: device is slightly tilted (10 degrees)
# Calibrated up vector: (0, 9.8, 0)
# Current gravity: small tilt
angle_rad = math.radians(10)
up_vector = (0.0, 9.8, 0.0)
current_gravity = (0.0, 9.8 * math.cos(angle_rad), 9.8 * math.sin(angle_rad))
# Normalize vectors
ux, uy, uz = up_vector
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
gx, gy, gz = current_gravity
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
# Calculate tilt angle
dot_up = gx * ux + gy * uy + gz * uz
perp_x = gx - dot_up * ux
perp_y = gy - dot_up * uy
perp_z = gz - dot_up * uz
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
tilt_angle = math.atan2(perp_mag, abs(dot_up))
# Should be approximately 10 degrees (0.174 radians)
assert abs(tilt_angle - 0.174) < 0.01
# Should be below default threshold of 0.3 rad (~17 degrees)
assert tilt_angle < 0.3
def test_gesture_types_exist():
"""Test that accelerometer gesture types are defined"""
# Simple direct test - check that gesture strings are defined
gestures = [
"tap",
"long_press",
"swipe_left",
"swipe_right",
"swipe_up",
"swipe_down",
"pinch_in",
"pinch_out",
"drag_start",
"drag_move",
"drag_end",
"tilt_forward", # Our new gestures
"tilt_backward"
]
# Verify the new gesture strings are valid
assert "tilt_forward" in gestures
assert "tilt_backward" in gestures
if __name__ == "__main__":
# Run tests
pytest.main([__file__, "-v"])