#!/usr/bin/env python3 """ Touchscreen Calibration Utility This script guides the user through touchscreen calibration by: 1. Displaying calibration targets (circles) at known positions 2. Waiting for user to touch each target 3. Recording touch coordinates 4. Computing transformation matrix 5. Saving calibration data Usage: python3 calibrate_touch.py [--points N] [--output PATH] Options: --points N Number of calibration points (5 or 9, default 9) --output PATH Calibration file path (default ~/.config/dreader/touch_calibration.json) --virtual Use virtual display for testing """ import asyncio import argparse import sys import os from pathlib import Path from PIL import Image, ImageDraw, ImageFont # Add src to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src')) from dreader_hal.display.it8951 import IT8951DisplayDriver from dreader_hal.touch.ft5xx6 import FT5xx6TouchDriver from dreader_hal.calibration import TouchCalibration from dreader_hal.types import GestureType, RefreshMode class CalibrationUI: """ Calibration user interface. Displays calibration targets and instructions on the e-ink display. """ def __init__(self, width: int, height: int, target_radius: int = 10): self.width = width self.height = height self.target_radius = target_radius def draw_target(self, image: Image.Image, x: int, y: int, filled: bool = False) -> None: """ Draw a calibration target circle. Args: image: PIL Image to draw on x: Target X position y: Target Y position filled: Whether target has been touched """ draw = ImageDraw.Draw(image) # Draw concentric circles r = self.target_radius # Outer circle draw.ellipse([x - r, y - r, x + r, y + r], outline=0 if not filled else 128, width=2) # Middle circle draw.ellipse([x - r//2, y - r//2, x + r//2, y + r//2], outline=0 if not filled else 128, width=2) # Center dot if filled: draw.ellipse([x - 3, y - 3, x + 3, y + 3], fill=128, outline=128) else: draw.ellipse([x - 3, y - 3, x + 3, y + 3], fill=0, outline=0) def create_calibration_screen(self, targets: list, current_idx: int, completed: list) -> Image.Image: """ Create calibration screen with targets and instructions. Args: targets: List of (x, y) target positions current_idx: Index of current target completed: List of completed target indices Returns: PIL Image """ # Create white background image = Image.new('L', (self.width, self.height), color=255) draw = ImageDraw.Draw(image) # Draw title title = "Touchscreen Calibration" try: # Try to use a larger font if available font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32) font_text = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24) except: # Fall back to default font font_title = ImageFont.load_default() font_text = ImageFont.load_default() # Draw title centered bbox = draw.textbbox((0, 0), title, font=font_title) text_width = bbox[2] - bbox[0] draw.text(((self.width - text_width) // 2, 30), title, fill=0, font=font_title) # Draw instructions if current_idx < len(targets): instruction = f"Touch target {current_idx + 1} of {len(targets)}" else: instruction = "Calibration complete!" bbox = draw.textbbox((0, 0), instruction, font=font_text) text_width = bbox[2] - bbox[0] draw.text(((self.width - text_width) // 2, 80), instruction, fill=0, font=font_text) # Draw all targets for idx, (tx, ty) in enumerate(targets): if idx in completed: # Completed target - filled self.draw_target(image, tx, ty, filled=True) elif idx == current_idx: # Current target - highlighted self.draw_target(image, tx, ty, filled=False) # Draw arrow or indicator draw.text((tx + self.target_radius + 10, ty - 10), "←", fill=0, font=font_text) else: # Future target - dimmed self.draw_target(image, tx, ty, filled=False) # Draw progress bar at bottom progress_width = int((len(completed) / len(targets)) * (self.width - 100)) draw.rectangle([50, self.height - 50, 50 + progress_width, self.height - 30], fill=0, outline=0) draw.rectangle([50, self.height - 50, self.width - 50, self.height - 30], outline=0, width=2) return image def create_results_screen(self, calibration: TouchCalibration) -> Image.Image: """ Create results screen showing calibration quality. Args: calibration: TouchCalibration instance Returns: PIL Image """ image = Image.new('L', (self.width, self.height), color=255) draw = ImageDraw.Draw(image) try: font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32) font_text = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24) except: font_title = ImageFont.load_default() font_text = ImageFont.load_default() # Title title = "Calibration Complete!" bbox = draw.textbbox((0, 0), title, font=font_title) text_width = bbox[2] - bbox[0] draw.text(((self.width - text_width) // 2, 50), title, fill=0, font=font_title) # Quality quality = calibration.get_calibration_quality() rms_error = calibration.calibration_data.rms_error quality_text = f"Quality: {quality}" bbox = draw.textbbox((0, 0), quality_text, font=font_text) text_width = bbox[2] - bbox[0] draw.text(((self.width - text_width) // 2, 120), quality_text, fill=0, font=font_text) error_text = f"RMS Error: {rms_error:.2f} pixels" bbox = draw.textbbox((0, 0), error_text, font=font_text) text_width = bbox[2] - bbox[0] draw.text(((self.width - text_width) // 2, 160), error_text, fill=0, font=font_text) # Instructions info = "Calibration data has been saved." bbox = draw.textbbox((0, 0), info, font=font_text) text_width = bbox[2] - bbox[0] draw.text(((self.width - text_width) // 2, 220), info, fill=0, font=font_text) return image async def run_calibration(width: int, height: int, num_points: int, output_path: str, virtual: bool = False): """ Run the calibration process. Args: width: Display width height: Display height num_points: Number of calibration points output_path: Path to save calibration file virtual: Use virtual display """ print(f"Starting touchscreen calibration...") print(f"Display: {width}x{height}") print(f"Calibration points: {num_points}") print(f"Output: {output_path}") # Initialize display display = IT8951DisplayDriver( width=width, height=height, virtual=virtual, ) await display.initialize() print("Display initialized") # Initialize touch touch = FT5xx6TouchDriver( width=width, height=height, ) await touch.initialize() print("Touch controller initialized") # Create calibration instance calibration = TouchCalibration(width, height, num_points) ui = CalibrationUI(width, height, target_radius=20) # Generate target positions targets = calibration.generate_target_positions(margin=100, target_radius=20) print(f"Generated {len(targets)} calibration targets") completed = [] current_idx = 0 try: # Calibration loop while current_idx < len(targets): # Draw calibration screen screen = ui.create_calibration_screen(targets, current_idx, completed) await display.show_image(screen, mode=RefreshMode.QUALITY) target_x, target_y = targets[current_idx] print(f"\nTarget {current_idx + 1}/{len(targets)}: Touch circle at ({target_x}, {target_y})") # Wait for touch touch_received = False while not touch_received: event = await touch.get_touch_event() if event and event.gesture == GestureType.TAP: # Record calibration point calibration.add_calibration_point( display_x=target_x, display_y=target_y, touch_x=event.x, touch_y=event.y, ) print(f" Recorded touch at ({event.x}, {event.y})") # Mark as completed completed.append(current_idx) current_idx += 1 touch_received = True # Small delay to prevent double-touches await asyncio.sleep(0.5) # Compute calibration matrix print("\nComputing calibration matrix...") success = calibration.compute_calibration() if not success: print("ERROR: Failed to compute calibration matrix") return print(f"Calibration computed successfully!") print(f" Quality: {calibration.get_calibration_quality()}") print(f" RMS Error: {calibration.calibration_data.rms_error:.2f} pixels") print(f" Matrix: {calibration.calibration_data.matrix}") # Save calibration calibration.save(output_path) print(f"\nCalibration saved to: {output_path}") # Show results screen results_screen = ui.create_results_screen(calibration) await display.show_image(results_screen, mode=RefreshMode.QUALITY) # Wait a bit so user can see results await asyncio.sleep(3) finally: # Cleanup await display.cleanup() await touch.cleanup() print("\nCalibration complete!") def main(): """Main entry point.""" parser = argparse.ArgumentParser( description="Touchscreen calibration utility for DReader HAL" ) parser.add_argument( '--points', type=int, default=9, choices=[5, 9], help='Number of calibration points (default: 9)' ) parser.add_argument( '--output', type=str, default=str(Path.home() / '.config' / 'dreader' / 'touch_calibration.json'), help='Output calibration file path' ) parser.add_argument( '--width', type=int, default=800, help='Display width in pixels (default: 800)' ) parser.add_argument( '--height', type=int, default=1200, help='Display height in pixels (default: 1200)' ) parser.add_argument( '--virtual', action='store_true', help='Use virtual display for testing' ) args = parser.parse_args() # Run calibration asyncio.run(run_calibration( width=args.width, height=args.height, num_points=args.points, output_path=args.output, virtual=args.virtual, )) if __name__ == '__main__': main()