2026-05-25 15:50:52 -07:00
|
|
|
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
|
2026-05-25 16:05:20 -07:00
|
|
|
from socketserver import ThreadingMixIn
|
|
|
|
|
|
|
|
|
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
|
|
|
daemon_threads = True
|
2026-05-25 15:50:52 -07:00
|
|
|
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()
|
2026-05-25 16:05:20 -07:00
|
|
|
server = ThreadedHTTPServer(("localhost", PORT), Handler)
|
2026-05-25 15:50:52 -07:00
|
|
|
server.serve_forever()
|