diff --git a/docs/INSTALL.md b/docs/INSTALL.md index ce385fec9..599d85c01 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -399,6 +399,16 @@ Select "CoMaps" product scheme. Compile and run the project ("Product" → "Run"). +### CarPlay +To test CarPlay, simply select "I/O" → "External Displays" → "CarPlay" in the Simulator + +### Spoofing GPS +The Simulator supports setting a specific location or spoofing a GPX track. This is especially handy when testing CarPlay + +To select an Apple predetermined track or specific custom location, choose "Features" → "Location" in the Simulator + +To simulate a custom GPX track use `python3 tools/python/ios_simulator_load_gpx.py ` which is a wrapper for `xcrun simctl location`. Default values are 60 km/h and 0.1s update intervals, but can be customized + ## Desktop app See [install_desktop](INSTALL_DESKTOP.md) to install and build Desktop app for Linux and Mac OS diff --git a/tools/python/ios_simulator_load_gpx.py b/tools/python/ios_simulator_load_gpx.py new file mode 100644 index 000000000..01ad4384d --- /dev/null +++ b/tools/python/ios_simulator_load_gpx.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +GPX to iOS Simulator simctl location command + +Converts a GPX file to simctl location start command for realistic iOS location simulation. + +Tested with CoMaps exported tracks + +Usage: + python gpx_to_simctl.py test_route.gpx +""" + +import argparse +import xml.etree.ElementTree as ET +from pathlib import Path +import sys +import subprocess + +def extract_track_points_from_gpx(gpx_file: Path): + """Extract track points from GPX file.""" + tree = ET.parse(gpx_file) + root = tree.getroot() + + points = [] + # Find all elements with lat/lon attributes + for elem in root.findall('.//*[@lat][@lon]'): + lat = float(elem.get('lat')) + lon = float(elem.get('lon')) + points.append((lat, lon)) + + return points + +def generate_simctl_command(points, speed_kmh=60, interval=0.1, distance=None, device="booted"): + """Generate simctl location start command.""" + if len(points) < 2: + raise ValueError("Need at least 2 waypoints for simctl location start") + + # Convert km/h to m/s + speed_mps = speed_kmh / 3.6 + + # Format waypoints as lat,lon pairs + waypoint_strings = [f"{lat:.6f},{lon:.6f}" for lat, lon in points] + + # Build command + cmd = ["xcrun", "simctl", "location", device, "start"] + cmd.append(f"--speed={speed_mps:.2f}") + + if distance: + cmd.append(f"--distance={distance}") + else: + cmd.append(f"--interval={interval}") + + cmd.extend(waypoint_strings) + + return cmd + +def main(): + parser = argparse.ArgumentParser( + description="Convert GPX file to simctl location start command", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python gpx_to_simctl.py test_route.gpx --speed 60 --interval 0.1 + python gpx_to_simctl.py test_route.gpx --speed 80 --distance 10 --clear-first + python gpx_to_simctl.py test_route.gpx --speed 50 --dry-run + """ + ) + + parser.add_argument('gpx_file', help='Input GPX file') + parser.add_argument('--speed', type=float, default=60, + help='Speed in km/h (default: 60)') + parser.add_argument('--interval', type=float, default=0.1, + help='Update interval in seconds (default: 0.1)') + parser.add_argument('--distance', type=float, + help='Update distance in meters (overrides --interval)') + parser.add_argument('--device', default='booted', + help='Target device (default: booted)') + parser.add_argument('--dry-run', action='store_true', + help='Show command without executing (default: execute)') + parser.add_argument('--clear-first', action='store_true', + help='Clear existing location before starting') + + args = parser.parse_args() + + # Validate input file + gpx_file = Path(args.gpx_file) + if not gpx_file.exists(): + print(f"Error: GPX file '{gpx_file}' not found", file=sys.stderr) + return 1 + + try: + # Extract waypoints + points = extract_track_points_from_gpx(gpx_file) + print(f"Extracted {len(points)} waypoints from {gpx_file}") + + if len(points) < 2: + print("Error: Need at least 2 waypoints for location simulation", file=sys.stderr) + return 1 + + # Generate command + cmd = generate_simctl_command( + points, + speed_kmh=args.speed, + interval=args.interval, + distance=args.distance, + device=args.device + ) + + # Show command + print(f"\nGenerated simctl command:") + print(" ".join(cmd)) + + # Calculate simulation info + speed_mps = args.speed / 3.6 + total_distance = 0 + for i in range(1, len(points)): + lat1, lon1 = points[i-1] + lat2, lon2 = points[i] + # Simple distance approximation + total_distance += ((lat2-lat1)**2 + (lon2-lon1)**2)**0.5 * 111000 # rough conversion to meters + + duration = total_distance / speed_mps + print(f"\nSimulation info:") + print(f" Speed: {args.speed} km/h ({speed_mps:.1f} m/s)") + print(f" Waypoints: {len(points)}") + print(f" Estimated distance: {total_distance/1000:.2f} km") + print(f" Estimated duration: {duration:.0f} seconds ({duration/60:.1f} minutes)") + if args.distance: + print(f" Update distance: {args.distance}m") + else: + print(f" Update interval: {args.interval}s") + + # Execute by default unless dry-run + if args.dry_run: + print(f"\n[DRY RUN] Command that would be executed:") + print(f" {' '.join(cmd)}") + if args.clear_first: + clear_cmd = ["xcrun", "simctl", "location", args.device, "clear"] + print(f" (would clear location first: {' '.join(clear_cmd)})") + else: + print(f"\nExecuting command...") + + # Clear location first if requested + if args.clear_first: + clear_cmd = ["xcrun", "simctl", "location", args.device, "clear"] + print("Clearing existing location...") + subprocess.run(clear_cmd, check=True) + + # Execute the start command + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print("✅ Location simulation started successfully!") + if result.stdout.strip(): + print(result.stdout.strip()) + else: + print(f"❌ Error executing command:") + print(result.stderr.strip()) + return 1 + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file