#!/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)