Skip to content

Instantly share code, notes, and snippets.

@shekohex
Last active March 30, 2026 13:49
Show Gist options
  • Select an option

  • Save shekohex/cbfafd657527b241b55236433fd7ba81 to your computer and use it in GitHub Desktop.

Select an option

Save shekohex/cbfafd657527b241b55236433fd7ba81 to your computer and use it in GitHub Desktop.
Android wireless debugging QR pairing script with auto-connect
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "qrcode>=8.2",
# "zeroconf>=0.147.0",
# ]
# ///
from __future__ import annotations
import argparse
import ipaddress
import shutil
import subprocess
import sys
import threading
from secrets import choice
from string import digits
from textwrap import dedent
import qrcode
from zeroconf import IPVersion, ServiceBrowser, ServiceInfo, ServiceListener, Zeroconf
PAIRING_SERVICE = "_adb-tls-pairing._tcp.local."
CONNECT_SERVICE = "_adb-tls-connect._tcp.local."
def random_digits(length: int) -> str:
return "".join(choice(digits) for _ in range(length))
def run_adb(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["adb", *args],
capture_output=True,
text=True,
check=False,
)
def ensure_adb() -> None:
if shutil.which("adb") is None:
raise SystemExit("adb not found in PATH")
started = run_adb("start-server")
if started.returncode != 0:
raise SystemExit((started.stderr or started.stdout or "failed to start adb").strip())
def restart_adb() -> None:
run_adb("kill-server")
started = run_adb("start-server")
if started.returncode != 0:
raise RuntimeError((started.stderr or started.stdout or "failed to restart adb").strip())
def pick_private_ipv4(info: ServiceInfo) -> str | None:
for address in info.ip_addresses_by_version(IPVersion.V4Only):
if isinstance(address, ipaddress.IPv4Address) and address.is_private:
return address.exploded
return None
class MatchListener(ServiceListener):
def __init__(self, zeroconf: Zeroconf, service_type: str, matcher):
self.zeroconf = zeroconf
self.service_type = service_type
self.matcher = matcher
self.event = threading.Event()
self.info: ServiceInfo | None = None
self.error: Exception | None = None
def add_service(self, zeroconf: Zeroconf, service_type: str, name: str) -> None:
self._handle(name)
def update_service(self, zeroconf: Zeroconf, service_type: str, name: str) -> None:
self._handle(name)
def remove_service(self, zeroconf: Zeroconf, service_type: str, name: str) -> None:
return
def _handle(self, name: str) -> None:
if self.event.is_set():
return
try:
info = self.zeroconf.get_service_info(self.service_type, name, timeout=3000)
if info and self.matcher(name, info):
self.info = info
self.event.set()
except Exception as exc:
self.error = exc
self.event.set()
def wait(self, timeout: float) -> ServiceInfo | None:
if not self.event.wait(timeout):
return None
if self.error is not None:
raise self.error
return self.info
def print_intro() -> None:
print("Android wireless debugging over QR")
print()
print("Phone setup:")
print("1. Enable Developer options: Settings -> About phone -> tap Build number 7 times.")
print("2. Open Settings -> Developer options -> Wireless debugging.")
print("3. Keep phone and computer on the same Wi-Fi network.")
print("4. Tap Pair device with QR code, then scan the QR below.")
print()
def print_qr(payload: str) -> None:
qr = qrcode.QRCode(border=1)
qr.add_data(payload)
qr.make(fit=True)
qr.print_ascii(out=sys.stdout, invert=True)
print()
print("If scanning is flaky, increase terminal zoom/font size and try again.")
print()
def wait_for_pairing_service(zeroconf: Zeroconf, service_name: str, timeout: float) -> tuple[str, int]:
expected_full_name = f"{service_name}.{PAIRING_SERVICE}"
listener = MatchListener(zeroconf, PAIRING_SERVICE, lambda name, info: name == expected_full_name)
browser = ServiceBrowser(zeroconf, PAIRING_SERVICE, listener=listener)
try:
info = listener.wait(timeout)
finally:
browser.cancel()
if info is None:
raise TimeoutError("timed out waiting for the phone to advertise its pairing service")
address = pick_private_ipv4(info)
if address is None:
raise RuntimeError("pairing service did not advertise a private IPv4 address")
return address, info.port
def wait_for_connect_service(zeroconf: Zeroconf, address: str, timeout: float) -> int | None:
listener = MatchListener(
zeroconf,
CONNECT_SERVICE,
lambda name, info: pick_private_ipv4(info) == address,
)
browser = ServiceBrowser(zeroconf, CONNECT_SERVICE, listener=listener)
try:
info = listener.wait(timeout)
finally:
browser.cancel()
return None if info is None else info.port
def adb_pair(address: str, port: int, password: str) -> None:
result = run_adb("pair", f"{address}:{port}", password)
if result.returncode != 0:
message = (result.stderr or result.stdout or "adb pair failed").strip()
raise RuntimeError(message)
def should_retry_pair(message: str) -> bool:
lowered = message.lower()
return "protocol fault" in lowered or "couldn't read status message" in lowered
def pair_with_recovery(address: str, port: int, password: str) -> None:
try:
adb_pair(address, port, password)
return
except RuntimeError as exc:
message = str(exc)
if not should_retry_pair(message):
raise
print()
print("ADB hit a stale server/protocol fault during pairing.")
print("Restarting the ADB server and retrying once...")
sys.stdout.flush()
restart_adb()
try:
adb_pair(address, port, password)
except RuntimeError as exc:
raise RuntimeError(
dedent(
f"""
pairing failed after restarting ADB: {exc}
Try this:
1. Keep the phone on the Wireless debugging screen.
2. Turn Wireless debugging off, then on again.
3. Tap Pair device with QR code again.
4. Re-run adb-wireless-qr.
If it keeps happening:
- run: adb kill-server && adb start-server
- make sure no other adb client/tool is fighting for the server
"""
).strip()
) from exc
def adb_connect(address: str, port: int) -> None:
result = run_adb("connect", f"{address}:{port}")
if result.returncode != 0:
message = (result.stderr or result.stdout or "adb connect failed").strip()
raise RuntimeError(message)
def adb_devices() -> list[str]:
result = run_adb("devices")
if result.returncode != 0:
return []
return [line for line in result.stdout.splitlines()[1:] if line.strip()]
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="adb-wireless-qr",
description="Show an Android wireless-debugging QR code, then pair and connect automatically.",
epilog=dedent(
"""
Run:
chmod +x adb-wireless-qr
./adb-wireless-qr
Or put it on PATH and run:
adb-wireless-qr
Phone flow:
Settings -> Developer options -> Wireless debugging -> Pair device with QR code
"""
).strip(),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--pair-timeout", type=float, default=180, help="seconds to wait for QR pairing (default: 180)")
parser.add_argument("--connect-timeout", type=float, default=30, help="seconds to wait for auto-connect discovery (default: 30)")
return parser
def main() -> int:
args = build_parser().parse_args()
ensure_adb()
service_name = f"adb-wireless-{random_digits(6)}"
password = random_digits(8)
payload = f"WIFI:T:ADB;S:{service_name};P:{password};;"
print_intro()
print_qr(payload)
print("Waiting for your phone to scan the QR and start pairing...")
sys.stdout.flush()
zeroconf = Zeroconf()
try:
address, pairing_port = wait_for_pairing_service(zeroconf, service_name, args.pair_timeout)
print(f"Found pairing service at {address}:{pairing_port}")
pair_with_recovery(address, pairing_port, password)
print("Pairing succeeded")
connect_port = wait_for_connect_service(zeroconf, address, args.connect_timeout)
if connect_port is None:
print("Pairing succeeded, but the debugging port was not discovered in time.")
print("Open Wireless debugging on the phone and run adb connect with the shown port if needed.")
return 1
adb_connect(address, connect_port)
print(f"Connected to {address}:{connect_port}")
devices = adb_devices()
if devices:
print()
print("adb devices:")
for line in devices:
print(line)
return 0
finally:
zeroconf.close()
if __name__ == "__main__":
try:
raise SystemExit(main())
except KeyboardInterrupt:
raise SystemExit("cancelled")
except Exception as exc:
sys.stdout.flush()
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1)

adb-wireless-qr

Single-file Python script for Android Wireless Debugging QR pairing with automatic adb connect after pairing.

Requirements

  • adb in PATH
  • uv in PATH
  • Android 11+
  • Phone and computer on the same Wi-Fi network

Download

gh gist clone cbfafd657527b241b55236433fd7ba81

Or download the raw script directly and make it executable:

curl -L -o adb-wireless-qr https://gist.githubusercontent.com/shekohex/cbfafd657527b241b55236433fd7ba81/raw/9d9b5771ff9a6d4119132f38bb3a5dd2b762442e/adb-wireless-qr
chmod +x adb-wireless-qr

Run

./adb-wireless-qr

If the script is already on your PATH:

adb-wireless-qr

Phone Setup

  1. Enable Developer options: Settings -> About phone -> tap Build number 7 times
  2. Open Settings -> Developer options -> Wireless debugging
  3. Tap Pair device with QR code
  4. Scan the QR shown by the script

The script will:

  1. Show the QR code in your terminal
  2. Wait for the phone pairing service
  3. Run adb pair automatically
  4. Discover the debug port
  5. Run adb connect automatically

Notes

  • First run may take a moment while uv installs Python dependencies.
  • If pairing hits a stale ADB server issue, the script restarts the ADB server and retries once.
  • If QR scanning is flaky, increase terminal font size/zoom and try again.

Help

adb-wireless-qr --help
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment