364 lines
12 KiB
Python
Executable File
364 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Accelerometer Calibration Script
|
|
|
|
This script helps calibrate the accelerometer for gravity-based page flipping.
|
|
It displays visual instructions on the e-ink display to guide the user through
|
|
aligning the device with the "up" direction.
|
|
|
|
The calibration process:
|
|
1. Shows an arrow pointing up
|
|
2. User rotates device until arrow aligns with desired "up" direction
|
|
3. User confirms by tapping screen
|
|
4. Script saves calibration offset to config file
|
|
|
|
Usage:
|
|
python examples/calibrate_accelerometer.py
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from dreader.hal_hardware import HardwareDisplayHAL
|
|
from dreader.gesture import GestureType
|
|
|
|
|
|
class AccelerometerCalibrator:
|
|
"""Interactive accelerometer calibration tool"""
|
|
|
|
def __init__(self, hal: HardwareDisplayHAL, config_path: str = "accelerometer_config.json"):
|
|
self.hal = hal
|
|
self.config_path = Path(config_path)
|
|
self.width = hal.width
|
|
self.height = hal.height
|
|
self.calibrated = False
|
|
|
|
# Calibration data
|
|
self.up_vector = None # (x, y, z) when device is in "up" position
|
|
|
|
async def run(self):
|
|
"""Run the calibration process"""
|
|
print("Starting accelerometer calibration...")
|
|
print(f"Display: {self.width}x{self.height}")
|
|
|
|
await self.hal.initialize()
|
|
|
|
try:
|
|
# Show welcome screen
|
|
await self.show_welcome()
|
|
await self.wait_for_tap()
|
|
|
|
# Calibration loop
|
|
await self.calibration_loop()
|
|
|
|
# Show completion screen
|
|
await self.show_completion()
|
|
await asyncio.sleep(3)
|
|
|
|
finally:
|
|
await self.hal.cleanup()
|
|
|
|
async def show_welcome(self):
|
|
"""Display welcome/instruction screen"""
|
|
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Try to load a font, fall back to default
|
|
try:
|
|
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
|
|
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
|
|
except:
|
|
title_font = ImageFont.load_default()
|
|
body_font = ImageFont.load_default()
|
|
|
|
# Title
|
|
title = "Accelerometer Calibration"
|
|
title_bbox = draw.textbbox((0, 0), title, font=title_font)
|
|
title_width = title_bbox[2] - title_bbox[0]
|
|
draw.text(((self.width - title_width) // 2, 100), title, fill=(0, 0, 0), font=title_font)
|
|
|
|
# Instructions
|
|
instructions = [
|
|
"This will calibrate the accelerometer",
|
|
"for gravity-based page flipping.",
|
|
"",
|
|
"You will:",
|
|
"1. See an arrow on screen",
|
|
"2. Rotate device until arrow points UP",
|
|
"3. Tap screen to confirm",
|
|
"",
|
|
"Tap anywhere to begin..."
|
|
]
|
|
|
|
y = 250
|
|
for line in instructions:
|
|
line_bbox = draw.textbbox((0, 0), line, font=body_font)
|
|
line_width = line_bbox[2] - line_bbox[0]
|
|
draw.text(((self.width - line_width) // 2, y), line, fill=(0, 0, 0), font=body_font)
|
|
y += 50
|
|
|
|
await self.hal.show_image(img)
|
|
|
|
async def calibration_loop(self):
|
|
"""Main calibration loop - show live arrow and accelerometer reading"""
|
|
print("\nCalibration mode:")
|
|
print("Rotate device until arrow points UP, then tap screen.")
|
|
|
|
last_display_time = 0
|
|
display_interval = 0.2 # Update display every 200ms
|
|
|
|
while not self.calibrated:
|
|
# Get current acceleration
|
|
x, y, z = await self.hal.hal.orientation.get_acceleration()
|
|
|
|
# Update display if enough time has passed
|
|
current_time = asyncio.get_event_loop().time()
|
|
if current_time - last_display_time >= display_interval:
|
|
await self.show_calibration_screen(x, y, z)
|
|
last_display_time = current_time
|
|
|
|
# Check for touch event
|
|
event = await self.hal.get_touch_event()
|
|
if event and event.gesture == GestureType.TAP:
|
|
# Save current orientation as "up"
|
|
self.up_vector = (x, y, z)
|
|
self.calibrated = True
|
|
print(f"\nCalibration saved: up_vector = ({x:.2f}, {y:.2f}, {z:.2f})")
|
|
break
|
|
|
|
await asyncio.sleep(0.05) # Poll at ~20Hz
|
|
|
|
async def show_calibration_screen(self, ax: float, ay: float, az: float):
|
|
"""
|
|
Show arrow pointing in direction of gravity
|
|
|
|
Args:
|
|
ax, ay, az: Acceleration components in m/s²
|
|
"""
|
|
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Calculate gravity direction (normalized)
|
|
magnitude = math.sqrt(ax**2 + ay**2 + az**2)
|
|
if magnitude < 0.1: # Avoid division by zero
|
|
magnitude = 1.0
|
|
|
|
gx = ax / magnitude
|
|
gy = ay / magnitude
|
|
gz = az / magnitude
|
|
|
|
# Project gravity onto screen plane (assuming z is out of screen)
|
|
# We want to show which way is "down" on the device
|
|
# Arrow should point opposite to gravity (toward "up")
|
|
arrow_dx = -gx
|
|
arrow_dy = -gy
|
|
|
|
# Normalize for display
|
|
arrow_length = min(self.width, self.height) * 0.3
|
|
arrow_magnitude = math.sqrt(arrow_dx**2 + arrow_dy**2)
|
|
if arrow_magnitude < 0.1:
|
|
arrow_magnitude = 1.0
|
|
|
|
arrow_dx = (arrow_dx / arrow_magnitude) * arrow_length
|
|
arrow_dy = (arrow_dy / arrow_magnitude) * arrow_length
|
|
|
|
# Center point
|
|
cx = self.width // 2
|
|
cy = self.height // 2
|
|
|
|
# Arrow endpoint
|
|
end_x = cx + int(arrow_dx)
|
|
end_y = cy + int(arrow_dy)
|
|
|
|
# Draw large arrow
|
|
self.draw_arrow(draw, cx, cy, end_x, end_y, width=10)
|
|
|
|
# Draw circle at center
|
|
circle_radius = 30
|
|
draw.ellipse(
|
|
[(cx - circle_radius, cy - circle_radius),
|
|
(cx + circle_radius, cy + circle_radius)],
|
|
outline=(0, 0, 0),
|
|
width=5
|
|
)
|
|
|
|
# Draw text with acceleration values
|
|
try:
|
|
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28)
|
|
except:
|
|
font = ImageFont.load_default()
|
|
|
|
text = f"X: {ax:6.2f} m/s²"
|
|
draw.text((50, 50), text, fill=(0, 0, 0), font=font)
|
|
|
|
text = f"Y: {ay:6.2f} m/s²"
|
|
draw.text((50, 100), text, fill=(0, 0, 0), font=font)
|
|
|
|
text = f"Z: {az:6.2f} m/s²"
|
|
draw.text((50, 150), text, fill=(0, 0, 0), font=font)
|
|
|
|
text = "Rotate device until arrow points UP"
|
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
|
text_width = text_bbox[2] - text_bbox[0]
|
|
draw.text(((self.width - text_width) // 2, self.height - 150),
|
|
text, fill=(0, 0, 0), font=font)
|
|
|
|
text = "Then TAP screen to save"
|
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
|
text_width = text_bbox[2] - text_bbox[0]
|
|
draw.text(((self.width - text_width) // 2, self.height - 100),
|
|
text, fill=(0, 0, 0), font=font)
|
|
|
|
await self.hal.show_image(img)
|
|
|
|
def draw_arrow(self, draw: ImageDraw.Draw, x1: int, y1: int, x2: int, y2: int, width: int = 5):
|
|
"""Draw an arrow from (x1, y1) to (x2, y2)"""
|
|
# Main line
|
|
draw.line([(x1, y1), (x2, y2)], fill=(0, 0, 0), width=width)
|
|
|
|
# Arrow head
|
|
dx = x2 - x1
|
|
dy = y2 - y1
|
|
length = math.sqrt(dx**2 + dy**2)
|
|
|
|
if length < 0.1:
|
|
return
|
|
|
|
# Normalize
|
|
dx /= length
|
|
dy /= length
|
|
|
|
# Arrow head size
|
|
head_length = 40
|
|
head_width = 30
|
|
|
|
# Perpendicular vector
|
|
px = -dy
|
|
py = dx
|
|
|
|
# Arrow head points
|
|
p1_x = x2 - dx * head_length + px * head_width
|
|
p1_y = y2 - dy * head_length + py * head_width
|
|
|
|
p2_x = x2 - dx * head_length - px * head_width
|
|
p2_y = y2 - dy * head_length - py * head_width
|
|
|
|
# Draw arrow head
|
|
draw.polygon([(x2, y2), (p1_x, p1_y), (p2_x, p2_y)], fill=(0, 0, 0))
|
|
|
|
async def show_completion(self):
|
|
"""Show calibration complete screen"""
|
|
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
try:
|
|
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
|
|
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
|
|
except:
|
|
title_font = ImageFont.load_default()
|
|
body_font = ImageFont.load_default()
|
|
|
|
# Title
|
|
title = "Calibration Complete!"
|
|
title_bbox = draw.textbbox((0, 0), title, font=title_font)
|
|
title_width = title_bbox[2] - title_bbox[0]
|
|
draw.text(((self.width - title_width) // 2, 200), title, fill=(0, 0, 0), font=title_font)
|
|
|
|
# Details
|
|
if self.up_vector:
|
|
x, y, z = self.up_vector
|
|
details = [
|
|
f"Up vector saved:",
|
|
f"X: {x:.3f} m/s²",
|
|
f"Y: {y:.3f} m/s²",
|
|
f"Z: {z:.3f} m/s²",
|
|
"",
|
|
f"Saved to: {self.config_path}"
|
|
]
|
|
|
|
y_pos = 350
|
|
for line in details:
|
|
line_bbox = draw.textbbox((0, 0), line, font=body_font)
|
|
line_width = line_bbox[2] - line_bbox[0]
|
|
draw.text(((self.width - line_width) // 2, y_pos), line, fill=(0, 0, 0), font=body_font)
|
|
y_pos += 50
|
|
|
|
await self.hal.show_image(img)
|
|
|
|
# Save calibration to file
|
|
self.save_calibration()
|
|
|
|
def save_calibration(self):
|
|
"""Save calibration data to JSON file"""
|
|
if not self.up_vector:
|
|
print("Warning: No calibration data to save")
|
|
return
|
|
|
|
x, y, z = self.up_vector
|
|
|
|
config = {
|
|
"up_vector": {
|
|
"x": x,
|
|
"y": y,
|
|
"z": z
|
|
},
|
|
"tilt_threshold": 0.3, # Radians (~17 degrees)
|
|
"debounce_time": 0.5, # Seconds between tilt gestures
|
|
}
|
|
|
|
with open(self.config_path, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
print(f"Calibration saved to {self.config_path}")
|
|
|
|
async def wait_for_tap(self):
|
|
"""Wait for user to tap screen"""
|
|
while True:
|
|
event = await self.hal.get_touch_event()
|
|
if event and event.gesture == GestureType.TAP:
|
|
break
|
|
await asyncio.sleep(0.05)
|
|
|
|
|
|
async def main():
|
|
"""Main entry point"""
|
|
# Create HAL with accelerometer enabled
|
|
print("Initializing hardware...")
|
|
hal = HardwareDisplayHAL(
|
|
width=1872,
|
|
height=1404,
|
|
enable_orientation=True,
|
|
enable_rtc=False,
|
|
enable_power_monitor=False,
|
|
virtual_display=False # Set to True for testing without hardware
|
|
)
|
|
|
|
# Create calibrator
|
|
calibrator = AccelerometerCalibrator(hal)
|
|
|
|
# Run calibration
|
|
await calibrator.run()
|
|
|
|
print("\nCalibration complete!")
|
|
print("You can now use accelerometer-based page flipping.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
asyncio.run(main())
|
|
except KeyboardInterrupt:
|
|
print("\nCalibration cancelled by user")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(f"\nError during calibration: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|