PoGoMover

WebApp for spoofing your location on PoGo (Proof-Of-Concept)
This commit is contained in:
Justin Oros
2026-05-25 15:50:52 -07:00
committed by GitHub
commit f5be6d53b2
5 changed files with 2416 additions and 0 deletions
+219
View File
@@ -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.
+1475
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -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())
+261
View File
@@ -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()
+416
View File
@@ -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()