HW integratation
This commit is contained in:
Executable
+363
@@ -0,0 +1,363 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user