commit f5be6d53b2f61d8929f921ddd4ced63ae16a5815 Author: Justin Oros Date: Mon May 25 15:50:52 2026 -0700 PoGoMover WebApp for spoofing your location on PoGo (Proof-Of-Concept) diff --git a/README.md b/README.md new file mode 100644 index 0000000..93c8865 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# PoGo Mover ๐Ÿ—บ๏ธ + +> **โš ๏ธ IMPORTANT DISCLAIMER โ€” READ BEFORE USING** +> +> GPS spoofing in Pokรฉmon GO violates [Niantic's Terms of Service](https://nianticlabs.com/terms). Using this tool **will likely result in a soft ban, strike, or permanent account ban**. This project is shared as a **proof of concept only** for educational and accessibility research purposes. The author accepts no responsibility for any consequences to your Pokรฉmon GO account. Use at your own risk, and consider using a secondary/throwaway account if you choose to experiment with this. +> +> Additionally, as of mid-2024, Pokรฉmon GO's anti-cheat system (Error 12 โ€” "Failed to detect location") actively blocks the DVT-based GPS simulation method used by this tool. **Pokรฉmon GO spawns and PokรฉStops will not appear** until this is resolved at the OS/app level. See [Known Limitations](#known-limitations) for details. + +--- + +## What Is This? + +PoGo Mover is a macOS web app that lets you control your Pokรฉmon GO character's GPS location from your Mac while your iPhone is connected via USB. It was built as an accessibility tool for someone who became wheelchair-bound and could no longer physically walk to play the game. + +It works by using Apple's **DVT (Developer Tools) location simulation** API via `pymobiledevice3` โ€” the same mechanism Xcode uses to simulate locations during app development. No jailbreak required. + +![PoGo Mover Screenshot](https://i.imgur.com/placeholder.png) + +--- + +## Features + +- ๐Ÿ—บ๏ธ **Interactive map** with Street and Satellite view (remembers your preference) +- โŒจ๏ธ **WASD / arrow key movement** for real-time walking control +- ๐Ÿ–ฑ๏ธ **Click-to-move with waypoint queue** โ€” click multiple points to build a route +- ๐Ÿšถ **Speed presets** โ€” Walk, Jog, Bike, Drive (all remembered between sessions) +- โœจ **Teleport mode** with cooldown timer and distance-based wait recommendations +- ๐Ÿ“ **Locate button** โ€” snap to your real-world GPS location +- ๐Ÿ”„ **Persistent GPS lock** โ€” continuously re-sends position to prevent real GPS from overriding +- ๐Ÿ’พ **Remembers last position**, zoom level, map layer, and speed between sessions +- ๐ŸŒ **Runs locally** โ€” no internet required after initial map tile load, no data sent anywhere + +--- + +## How It Works + +``` +Mac (PoGo Mover web app) + โ†“ HTTP POST /move +Mac (server.py + location_daemon.py) + โ†“ pymobiledevice3 DVT API over USB tunnel +iPhone (iOS location service) + โ†“ +Pokรฉmon GO reads spoofed GPS +``` + +1. `start.py` automatically sets up the tunnel, checks Developer Mode, mounts disk images, and opens the browser +2. `server.py` serves the web app and receives coordinate updates +3. `location_daemon.py` holds a **persistent DVT connection** to the iPhone and streams location updates without gaps โ€” preventing the real GPS from breaking through +4. The browser app sends position updates via HTTP to the local server + +--- + +## Requirements + +- **Mac** running macOS 12 or later +- **iPhone** with iOS 15+ (tested on iOS 26) +- **USB cable** (Lightning or USB-C) +- **Python 3.8+** +- **Xcode** (free, from Mac App Store) โ€” needed once to enable Developer Mode on the iPhone +- **pymobiledevice3** (installed automatically by `start.py`) + +--- + +## Setup + +### 1. Enable Developer Mode on iPhone (one time only) + +1. Install **Xcode** from the Mac App Store (free, ~10GB) +2. Open Xcode โ†’ create or open any project +3. Go to **Window โ†’ Devices and Simulators** (this activates the iPhone connection) +4. On iPhone: **Settings โ†’ Privacy & Security โ†’ scroll to bottom โ†’ Developer Mode โ†’ ON** +5. iPhone restarts โ€” swipe up and enter your passcode to confirm +6. You can uninstall Xcode after this if you need disk space back + +### 2. Install PoGo Mover + +```bash +git clone https://github.com/yourusername/pogo-mover.git +cd pogo-mover +``` + +Or just download and unzip the release. + +### 3. Run + +```bash +python3 start.py +``` + +That's it. `start.py` handles everything: +- Installs `pymobiledevice3` if missing +- Detects your iPhone +- Checks Developer Mode status (with guidance if it's off) +- Starts the GPS tunnel (will ask for your sudo password once) +- Mounts the developer disk image +- Opens the web app in your browser at `http://localhost:7777` + +--- + +## Controls + +| Action | How | +|--------|-----| +| Move character | **WASD** or **Arrow keys** โ€” hold to walk continuously | +| Add waypoint | **Click on the map** โ€” adds to queue, player walks the route in order | +| Cancel route | Press any WASD key | +| Teleport | Enable **Teleport Mode** then click the map | +| Change speed | Click **Walk / Jog / Bike / Drive** presets or drag the slider | +| Snap to real location | Click the **compass arrow** button (bottom-left of map) | +| Switch map layer | Click **Street / Satellite** toggle (bottom-left of map) | + +### Speed Reference + +| Mode | Speed | Notes | +|------|-------|-------| +| ๐Ÿšถ Walk | 1.4 m/s | Safest โ€” best for egg hatching | +| ๐Ÿƒ Jog | 4 m/s | Still within normal range | +| ๐Ÿšฒ Bike | 11 m/s | Use carefully | +| ๐Ÿš— Drive | 28 m/s | May trigger speed detection | + +### Teleport Cooldown Guide + +After teleporting, keep Pokรฉmon GO **closed** for the recommended time before reopening: + +| Distance | Wait | +|----------|------| +| < 10 km | 30 minutes | +| 10โ€“50 km | 60 minutes | +| 50โ€“500 km | 90 minutes | +| 500+ km | 120 minutes | + +--- + +## Known Limitations + +### โš ๏ธ Pokรฉmon GO Error 12 โ€” "Failed to detect location" + +As of mid-2024, Niantic added detection specifically targeting DVT-based GPS simulation. When using PoGo Mover with Pokรฉmon GO: + +- The player character **does move** on the map โœ… +- Pokรฉmon **will not spawn** โŒ +- PokรฉStops **will not appear** โŒ +- Error 12 is displayed in-game โŒ + +This is a Niantic-side block, not a bug in PoGo Mover. Other apps and games that use the iPhone's GPS (Maps, Waze, Find My, location-based AR apps) **work fine** with PoGo Mover. + +**Tools that claim to bypass Error 12** (iAnyGo, PoGoSkill, PoGo Wizard) do so by installing a **modified version of Pokรฉmon GO** โ€” which carries a higher ban risk and is not something this project pursues. + +### Other Limitations + +- macOS only (the DVT tunnel requires macOS system tools) +- Requires USB connection โ€” Wi-Fi is not currently supported +- The `remote tunneld` process requires sudo (Mac password) each session +- If the tunnel drops, GPS reverts to real location until restarted + +--- + +## File Structure + +``` +pogo-mover/ +โ”œโ”€โ”€ start.py # All-in-one launcher โ€” run this +โ”œโ”€โ”€ server.py # Local HTTP server + GPS command relay +โ”œโ”€โ”€ location_daemon.py # Persistent DVT connection to iPhone +โ”œโ”€โ”€ index.html # Web app (map, controls, UI) +โ””โ”€โ”€ README.md # This file +``` + +--- + +## Technical Details + +### GPS Persistence + +A key challenge was preventing the iPhone's real GPS from overriding the spoofed location. The solution is `location_daemon.py` โ€” a persistent async Python process that holds the DVT `LocationSimulation` service connection open continuously, streaming the current target position. Because the connection never closes, iOS never gets a window to reassert the real GPS signal. + +### Tunnel Architecture (iOS 17+) + +iOS 17 moved all developer services behind a RemoteXPC tunnel. `start.py` launches `sudo pymobiledevice3 remote tunneld` which: +1. Creates a VPN-like tunnel interface +2. Exposes a local HTTP API at `127.0.0.1:49151` +3. `server.py` queries this API to get the live RSD (Remote Service Discovery) address and port +4. `location_daemon.py` connects to the DVT service through this RSD endpoint + +### GPS Jitter + +Real iPhone GPS has ~2m of natural variance. PoGo Mover adds ยฑ2m of random noise to every location update to simulate this, making the signal look more natural to apps that check GPS accuracy metadata. + +--- + +## Why This Was Built + +This project started as an accessibility tool. The original use case: someone who loved Pokรฉmon GO became wheelchair-bound and could no longer physically walk to play. The goal was to let them control their in-game movement from a Mac, using a keyboard or by clicking on a map. + +The project evolved into a full-featured GPS controller with waypoint routing, teleport mode, speed presets, satellite view, and persistent location locking. Even though Pokรฉmon GO's current anti-cheat prevents full gameplay, the underlying technique works reliably for any other app that uses iOS location services. + +--- + +## Contributing + +PRs welcome. Areas where help would be appreciated: + +- **Wi-Fi tunnel support** โ€” eliminating the USB requirement +- **Route import** โ€” loading GPX files as waypoint routes +- **Error 12 research** โ€” any findings on bypassing Niantic's DVT detection without modified apps +- **Windows/Linux support** โ€” `pymobiledevice3` has partial support on other platforms + +--- + +## License + +MIT โ€” do whatever you want, but please keep the disclaimer. + +--- + +## Acknowledgements + +Built on top of [pymobiledevice3](https://github.com/doronz88/pymobiledevice3) by @doronz88 โ€” an extraordinary piece of work that implements essentially the entire Apple device protocol stack in pure Python. diff --git a/index.html b/index.html new file mode 100644 index 0000000..7492162 --- /dev/null +++ b/index.html @@ -0,0 +1,1475 @@ + + + + + +PoGo Mover + + + + + + + + +
+ +
+
+ Connecting... +
+
+ +
+
+ + +
+ + + + + +
+
COOLDOWN TIMER
Safe to open Pokรฉmon GO when timer ends
+
+ + + + +
+ โ€” + remaining +
+
+
+ Keep Pokรฉmon GO closed until this timer reaches zero to avoid a soft ban. +
+ +
+ + + + diff --git a/location_daemon.py b/location_daemon.py new file mode 100644 index 0000000..d9c03f9 --- /dev/null +++ b/location_daemon.py @@ -0,0 +1,45 @@ +import asyncio +import json +import sys +import os + +async def main(): + rsd_host = sys.argv[1] + rsd_port = int(sys.argv[2]) + + from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService + from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider + from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation + + rsd = RemoteServiceDiscoveryService((rsd_host, rsd_port)) + await rsd.connect() + + async with DvtProvider(rsd) as dvt, LocationSimulation(dvt) as loc: + sys.stdout.write("ready\n") + sys.stdout.flush() + + loop = asyncio.get_event_loop() + reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(reader) + await loop.connect_read_pipe(lambda: protocol, sys.stdin) + + while True: + line = await reader.readline() + if not line: + break + line = line.decode().strip() + if not line: + continue + try: + parts = line.split(",") + lat = float(parts[0]) + lng = float(parts[1]) + await loc.set(lat, lng) + sys.stdout.write("ok\n") + sys.stdout.flush() + except Exception as e: + sys.stdout.write(f"err:{e}\n") + sys.stdout.flush() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/server.py b/server.py new file mode 100644 index 0000000..9194cad --- /dev/null +++ b/server.py @@ -0,0 +1,261 @@ +import json +import math +import mimetypes +import os +import subprocess +import sys +import threading +import time +import urllib.request +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse + +PORT = 7777 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +COORDS_FILE = os.path.join(BASE_DIR, ".last_coords.json") +DAEMON_SCRIPT = os.path.join(BASE_DIR, "location_daemon.py") +DEFAULT_COORDS = {"lat": 33.4484, "lng": -112.0740} + +def load_coords(): + try: + with open(COORDS_FILE, "r") as f: + data = json.load(f) + if "lat" in data and "lng" in data: + print(f"Resuming from last position: {data['lat']:.5f}, {data['lng']:.5f}") + return data + except Exception: + pass + return dict(DEFAULT_COORDS) + +def save_coords(lat, lng): + try: + with open(COORDS_FILE, "w") as f: + json.dump({"lat": lat, "lng": lng}, f) + except Exception: + pass + +current_coords = load_coords() +last_result = {"method": None, "ok": False, "error": "not tried yet"} + +_daemon_proc = None +_daemon_lock = threading.Lock() +_cached_rsd_host = None +_cached_rsd_port = None +_rsd_cache_time = 0 +RSD_CACHE_TTL = 30 + +def get_rsd(): + global _cached_rsd_host, _cached_rsd_port, _rsd_cache_time + now = time.time() + if _cached_rsd_host and (now - _rsd_cache_time) < RSD_CACHE_TTL: + return _cached_rsd_host, _cached_rsd_port + try: + with urllib.request.urlopen("http://127.0.0.1:49151/", timeout=3) as r: + tunnels = json.loads(r.read()) + if isinstance(tunnels, dict) and tunnels: + entry = list(tunnels.values())[0] + if isinstance(entry, list) and entry: + t = entry[0] + host = t.get("tunnel-address") + port = t.get("tunnel-port") + if host and port: + _cached_rsd_host = str(host) + _cached_rsd_port = int(port) + _rsd_cache_time = now + return _cached_rsd_host, _cached_rsd_port + except Exception: + pass + return None, None + +def start_daemon(): + global _daemon_proc, _cached_rsd_host, _cached_rsd_port + host, port = get_rsd() + if not host: + print(" [!] No RSD tunnel found โ€” daemon not started", flush=True) + return False + try: + proc = subprocess.Popen( + [sys.executable, DAEMON_SCRIPT, host, str(port)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + ready_line = proc.stdout.readline().strip() + if ready_line == "ready": + _daemon_proc = proc + print(f" [โœ“] Location daemon connected via RSD {host}:{port}", flush=True) + return True + else: + print(f" [!] Daemon startup failed: {ready_line}", flush=True) + proc.kill() + return False + except Exception as e: + print(f" [!] Could not start daemon: {e}", flush=True) + return False + +def send_via_daemon(lat, lng): + global _daemon_proc, last_result + with _daemon_lock: + if _daemon_proc is None or _daemon_proc.poll() is not None: + print(" [~] Daemon not running โ€” restarting...", flush=True) + start_daemon() + if _daemon_proc is None: + return False + try: + _daemon_proc.stdin.write(f"{lat},{lng}\n") + _daemon_proc.stdin.flush() + response = _daemon_proc.stdout.readline().strip() + if response == "ok": + last_result = {"method": "DVT daemon (persistent)", "ok": True, "error": None} + return True + else: + print(f" [!] Daemon response: {response}", flush=True) + return False + except Exception as e: + print(f" [!] Daemon write failed: {e}", flush=True) + _daemon_proc = None + return False + +def send_location_cli(lat, lng): + global last_result, _cached_rsd_host, _cached_rsd_port + host, port = get_rsd() + if host and port: + try: + r = subprocess.run( + ["pymobiledevice3", "developer", "dvt", "simulate-location", + "set", "--rsd", host, str(port), "--", str(lat), str(lng)], + capture_output=True, text=True, timeout=5 + ) + if r.returncode == 0: + last_result = {"method": "dvt --rsd (cli)", "ok": True, "error": None} + return True + except (subprocess.TimeoutExpired, Exception): + _cached_rsd_host = None + last_result = {"method": None, "ok": False, "error": "all methods failed"} + return False + +import random + +def add_gps_jitter(lat, lng): + meter_lat = 1.0 / 111320.0 + meter_lng = 1.0 / (111320.0 * max(abs(math.cos(math.radians(lat))), 0.001)) + jitter_m = 2.0 + return ( + lat + random.uniform(-jitter_m, jitter_m) * meter_lat, + lng + random.uniform(-jitter_m, jitter_m) * meter_lng + ) + +def set_location(lat, lng): + jlat, jlng = add_gps_jitter(lat, lng) + if not send_via_daemon(jlat, jlng): + send_location_cli(jlat, jlng) + +_target_lat = current_coords.get("lat") +_target_lng = current_coords.get("lng") +_target_updated = time.time() +_target_lock = threading.Lock() +_last_sent_lat = None +_last_sent_lng = None + +def location_loop(): + global _last_sent_lat, _last_sent_lng + while True: + with _target_lock: + lat = _target_lat + lng = _target_lng + + if lat is None or lng is None: + time.sleep(0.1) + continue + + if lat != _last_sent_lat or lng != _last_sent_lng: + set_location(lat, lng) + _last_sent_lat = lat + _last_sent_lng = lng + else: + time.sleep(0.1) + +def set_target(lat, lng): + global _target_lat, _target_lng, _target_updated + with _target_lock: + _target_lat = lat + _target_lng = lng + _target_updated = time.time() + +class Handler(BaseHTTPRequestHandler): + def handle_error(self, request, client_address): + import sys as _sys + exc = _sys.exc_info()[1] + if isinstance(exc, (BrokenPipeError, ConnectionResetError)): + return + super().handle_error(request, client_address) + + def log_message(self, format, *args): + pass + + def send_json(self, data, status=200): + body = json.dumps(data, indent=2).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_OPTIONS(self): + self.send_response(200) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + self.end_headers() + + def do_GET(self): + path = urlparse(self.path).path + if path == "/coords": + return self.send_json(current_coords) + if path == "/status": + return self.send_json({"ok": True, "spoof": last_result}) + if path in ("/", "/index.html"): + path = "/index.html" + file_path = os.path.join(BASE_DIR, path.lstrip("/")) + if os.path.isfile(file_path): + mime, _ = mimetypes.guess_type(file_path) + with open(file_path, "rb") as f: + body = f.read() + self.send_response(200) + self.send_header("Content-Type", mime or "application/octet-stream") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + def do_POST(self): + global current_coords + if urlparse(self.path).path == "/move": + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + try: + data = json.loads(body) + lat = float(data["lat"]) + lng = float(data["lng"]) + current_coords = {"lat": lat, "lng": lng} + save_coords(lat, lng) + set_target(lat, lng) + self.send_json({"ok": True, "lat": lat, "lng": lng}) + except Exception as e: + self.send_json({"error": str(e)}, 400) + else: + self.send_response(404) + self.end_headers() + +if __name__ == "__main__": + print(f"PoGo Mover running at http://localhost:{PORT}") + print(" Starting location daemon...", flush=True) + start_daemon() + threading.Thread(target=location_loop, daemon=True).start() + server = HTTPServer(("localhost", PORT), Handler) + server.serve_forever() diff --git a/start.py b/start.py new file mode 100644 index 0000000..e7d5db9 --- /dev/null +++ b/start.py @@ -0,0 +1,416 @@ +import os +import sys +import time +import platform +import subprocess +import webbrowser +import threading + +def banner(msg, char="="): + print(f"\n{char * 50}") + print(f" {msg}") + print(f"{char * 50}") + +def step(msg): + print(f"\nโ–ถ {msg}") + +def ok(msg): + print(f" โœ“ {msg}") + +def warn(msg): + print(f" โš  {msg}") + +def fail(msg): + print(f" โœ— {msg}") + +def check_macos(): + if platform.system() != "Darwin": + fail("PoGo Mover requires macOS.") + sys.exit(1) + ok("macOS detected") + +def check_python(): + if sys.version_info < (3, 8): + fail("Python 3.8 or newer is required.") + sys.exit(1) + ok(f"Python {sys.version.split()[0]}") + +def install_pymobiledevice3(): + step("Checking for pymobiledevice3...") + try: + import pymobiledevice3 + ok("pymobiledevice3 already installed") + return + except ImportError: + pass + warn("pymobiledevice3 not found โ€” installing now (this may take a minute)...") + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "pymobiledevice3", "--quiet"], + capture_output=False + ) + if result.returncode != 0: + fail("Installation failed. Try running: pip3 install pymobiledevice3") + sys.exit(1) + ok("pymobiledevice3 installed") + +def detect_iphone_system_profiler(): + try: + result = subprocess.run( + ["system_profiler", "SPUSBDataType", "-json"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0 and ("iPhone" in result.stdout or "iPad" in result.stdout): + return True + except Exception: + pass + return False + +def detect_iphone_ioreg(): + try: + result = subprocess.run( + ["ioreg", "-p", "IOUSB", "-l", "-w", "0"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0 and "iPhone" in result.stdout: + return True + except Exception: + pass + return False + +def detect_iphone_pymobiledevice(): + try: + result = subprocess.run( + ["pymobiledevice3", "list-devices"], + capture_output=True, text=True, timeout=10 + ) + output = result.stdout.strip() + if output and len(output) > 8: + return True + except Exception: + pass + + try: + result = subprocess.run( + ["pymobiledevice3", "usbmux", "list"], + capture_output=True, text=True, timeout=10 + ) + output = result.stdout.strip() + if output and len(output) > 8: + return True + except Exception: + pass + + return False + +def check_iphone(): + step("Checking for connected iPhone...") + + if detect_iphone_system_profiler() or detect_iphone_ioreg(): + ok("iPhone detected via USB") + return True + + if detect_iphone_pymobiledevice(): + ok("iPhone detected via pymobiledevice3") + return True + + warn("iPhone not detected via USB.") + print() + print(" Things to try:") + print(" 1. Make sure the USB cable is fully plugged in") + print(" 2. Unlock your iPhone and tap 'Trust' if prompted") + print(" 3. Try a different USB cable or port") + print(" 4. Open Finder โ€” does your iPhone appear in the sidebar?") + print() + choice = input(" Press Enter to retry, S to skip this check, or Ctrl+C to quit: ").strip().lower() + if choice == "s": + warn("Skipping iPhone check โ€” continuing anyway.") + return False + return check_iphone() + +def get_ios_version_from_ioreg(): + try: + result = subprocess.run( + ["ioreg", "-p", "IOUSB", "-l", "-w", "0"], + capture_output=True, text=True, timeout=10 + ) + for line in result.stdout.splitlines(): + if "ProductVersion" in line: + parts = line.split('"') + for i, p in enumerate(parts): + if p == "ProductVersion" and i + 2 < len(parts): + version_str = parts[i + 2] + return int(version_str.split(".")[0]) + except Exception: + pass + return 0 + +def get_ios_version_from_pymobiledevice(): + try: + import json + for cmd in [ + ["pymobiledevice3", "list-devices", "--json"], + ["pymobiledevice3", "usbmux", "list", "--json"], + ]: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=8) + try: + devices = json.loads(result.stdout) + if isinstance(devices, list) and devices: + v = devices[0].get("ProductVersion") or devices[0].get("product_version", "0") + return int(str(v).split(".")[0]) + if isinstance(devices, dict): + for v in ["ProductVersion", "product_version"]: + if v in devices: + return int(str(devices[v]).split(".")[0]) + except Exception: + pass + except Exception: + pass + return 0 + +def get_ios_version(): + v = get_ios_version_from_pymobiledevice() + if v > 0: + return v + return get_ios_version_from_ioreg() + +def maybe_start_tunnel(): + step("Starting GPS tunnel...") + + ios = get_ios_version() + if ios == 0: + ios = 17 + + if ios < 17: + ok(f"iOS {ios} detected โ€” no tunnel needed.") + return None + + tunnel_cmd = ["sudo", "pymobiledevice3", "remote", "tunneld"] + + warn("The tunnel requires your Mac password (sudo).") + print() + + proc = subprocess.Popen( + tunnel_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) + + deadline = time.time() + 30 + tunnel_ready = False + + while time.time() < deadline: + line = proc.stdout.readline() + if not line: + if proc.poll() is not None: + break + time.sleep(0.1) + continue + line = line.strip() + if "Application startup complete" in line or "Uvicorn running" in line: + tunnel_ready = True + break + if "password" in line.lower() or "Password" in line or "ERROR" in line.upper(): + print(f" {line}") + + if proc.poll() is not None: + fail("Tunnel process exited unexpectedly.") + warn("Try running manually in a separate tab:") + warn(" sudo pymobiledevice3 remote tunneld") + input(" Press Enter once the tunnel is running, or Ctrl+C to quit...") + return None + + if tunnel_ready: + ok("Tunnel ready โ€” tunneld HTTP service is running") + else: + ok("Tunnel started (waiting for device connection...)") + + threading.Thread(target=lambda: _drain_tunnel(proc), daemon=True).start() + return proc + +def _drain_tunnel(proc): + try: + for line in proc.stdout: + pass + except Exception: + pass + +def check_xcode(): + try: + result = subprocess.run( + ["xcode-select", "-p"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0 and result.stdout.strip(): + path = result.stdout.strip() + if "Xcode.app" in path: + ok(f"Xcode installed at {path}") + return "xcode" + else: + warn("Only Command Line Tools found โ€” Xcode app is needed for Developer Mode") + return "clt_only" + except Exception: + pass + warn("Xcode not found") + return "none" + +def check_developer_mode(): + try: + result = subprocess.run( + ["pymobiledevice3", "amfi", "developer-mode-status"], + capture_output=True, text=True, timeout=8 + ) + status = result.stdout.strip().lower() + if "true" in status: + ok("Developer Mode is ON โœ“") + return True + elif "false" in status: + return False + else: + return None + except Exception: + return None + +def check_and_guide_developer_mode(): + step("Checking Developer Mode status on iPhone...") + + dev_mode = check_developer_mode() + + if dev_mode is True: + return + + if dev_mode is False: + fail("Developer Mode is OFF โ€” GPS spoofing will not work until this is enabled.") + else: + warn("Could not determine Developer Mode status.") + + print() + xcode_status = check_xcode() + print() + + if xcode_status == "xcode": + print(" โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”") + print(" โ”‚ HOW TO ENABLE DEVELOPER MODE (Xcode found) โ”‚") + print(" โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค") + print(" โ”‚ 1. Open Xcode โ†’ create or open any project โ”‚") + print(" โ”‚ 2. Menu: Window โ†’ Devices and Simulators โ”‚") + print(" โ”‚ (this activates the iPhone connection) โ”‚") + print(" โ”‚ 3. On iPhone: Settings โ†’ Privacy & Security โ”‚") + print(" โ”‚ โ†’ scroll to bottom โ†’ Developer Mode โ†’ ON โ”‚") + print(" โ”‚ 4. iPhone will restart automatically โ”‚") + print(" โ”‚ 5. After restart: Swipe Up and enter passcode โ”‚") + print(" โ”‚ to confirm Developer Mode โ”‚") + print(" โ”‚ 6. Come back here and rerun start.py โ”‚") + print(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜") + elif xcode_status == "clt_only": + print(" โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”") + print(" โ”‚ HOW TO ENABLE DEVELOPER MODE โ”‚") + print(" โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค") + print(" โ”‚ Command Line Tools found but Xcode app needed. โ”‚") + print(" โ”‚ โ”‚") + print(" โ”‚ Option A โ€” Install Xcode (recommended): โ”‚") + print(" โ”‚ 1. Open Mac App Store โ†’ search 'Xcode' โ”‚") + print(" โ”‚ 2. Install (free, ~10GB) โ”‚") + print(" โ”‚ 3. Open Xcode โ†’ Window โ†’ Devices โ”‚") + print(" โ”‚ 4. Click iPhone โ†’ Enable Developer Mode โ”‚") + print(" โ”‚ โ”‚") + print(" โ”‚ Option B โ€” Try without Xcode: โ”‚") + print(" โ”‚ On iPhone: Settings โ†’ Privacy & Security โ”‚") + print(" โ”‚ โ†’ scroll to bottom โ†’ Developer Mode โ†’ ON โ”‚") + print(" โ”‚ (only appears after connecting to Xcode once) โ”‚") + print(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜") + else: + print(" โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”") + print(" โ”‚ HOW TO ENABLE DEVELOPER MODE (no Xcode found) โ”‚") + print(" โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค") + print(" โ”‚ Option A โ€” Install Xcode (recommended): โ”‚") + print(" โ”‚ 1. Open Mac App Store โ†’ search 'Xcode' โ”‚") + print(" โ”‚ 2. Install (free, ~10GB) โ”‚") + print(" โ”‚ 3. Open Xcode โ†’ Window โ†’ Devices โ”‚") + print(" โ”‚ 4. Click iPhone โ†’ Enable Developer Mode โ”‚") + print(" โ”‚ 5. Confirm on iPhone โ†’ restart โ”‚") + print(" โ”‚ โ”‚") + print(" โ”‚ Option B โ€” Apple Configurator 2 (tiny, free): โ”‚") + print(" โ”‚ 1. Mac App Store โ†’ 'Apple Configurator 2' โ”‚") + print(" โ”‚ 2. Open it with iPhone connected โ”‚") + print(" โ”‚ 3. Actions โ†’ Advanced โ†’ Enable Developer Mode โ”‚") + print(" โ”‚ โ”‚") + print(" โ”‚ Option C โ€” Apple Store: โ”‚") + print(" โ”‚ Ask staff to enable Developer Mode โ€” free โ”‚") + print(" โ”‚ and takes 2 minutes with their tools โ”‚") + print(" โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜") + + print() + + if dev_mode is False: + choice = input(" Press Enter to continue anyway (GPS won't work until fixed), or Ctrl+C to quit: ").strip() + else: + ok("Continuing โ€” Developer Mode status unknown, GPS may still work.") + + step("Mounting developer disk image (if needed)...") + try: + result = subprocess.run( + ["pymobiledevice3", "mounter", "auto-mount"], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + ok("Developer disk image ready") + else: + warn("Mount step returned non-zero โ€” likely already mounted, continuing.") + except Exception as e: + warn(f"Mount step skipped: {e}") + +def mount_developer_image(): + step("Mounting developer disk image (if needed)...") + try: + result = subprocess.run( + ["pymobiledevice3", "mounter", "auto-mount"], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + ok("Developer disk image ready") + else: + ok("Developer disk image already mounted") + except Exception as e: + warn(f"Mount step skipped: {e}") + +def open_browser(): + url = "http://localhost:7777" + step("Opening PoGo Mover in your browser...") + time.sleep(1.5) + webbrowser.open(url) + ok(f"Opened: {url}") + +def start_server(): + server_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "server.py") + step("Starting GPS server on http://localhost:7777...") + ok("Server running โ€” press Ctrl+C to stop.") + print() + try: + subprocess.run([sys.executable, server_path]) + except KeyboardInterrupt: + pass + +def main(): + banner("PoGo Mover โ€” Pokรฉmon GO GPS Controller", "=") + print(" Setting everything up automatically...\n") + + check_macos() + check_python() + install_pymobiledevice3() + check_iphone() + check_and_guide_developer_mode() + tunnel_proc = maybe_start_tunnel() + mount_developer_image() + + banner("All set! Launching PoGo Mover", "-") + + threading.Thread(target=open_browser, daemon=True).start() + start_server() + + if tunnel_proc and tunnel_proc.poll() is None: + tunnel_proc.terminate() + tunnel_proc.wait() + +if __name__ == "__main__": + main()