adb-qr 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adb_qr-0.1.0/.github/workflows/release.yml +22 -0
- adb_qr-0.1.0/.gitignore +4 -0
- adb_qr-0.1.0/LICENSE +21 -0
- adb_qr-0.1.0/PKG-INFO +123 -0
- adb_qr-0.1.0/README.md +102 -0
- adb_qr-0.1.0/adb_qr.py +244 -0
- adb_qr-0.1.0/docs/demo.png +0 -0
- adb_qr-0.1.0/docs/make_demo.py +33 -0
- adb_qr-0.1.0/pyproject.toml +33 -0
- adb_qr-0.1.0/uv.lock +35 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # PyPI trusted publishing (OIDC)
|
|
13
|
+
contents: write # gh release create
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv build
|
|
18
|
+
- run: uv publish --trusted-publishing always
|
|
19
|
+
- name: Create GitHub release
|
|
20
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
|
|
21
|
+
env:
|
|
22
|
+
GH_TOKEN: ${{ github.token }}
|
adb_qr-0.1.0/.gitignore
ADDED
adb_qr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleix (aleixrodriala)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
adb_qr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adb-qr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pair Android devices for wireless ADB by QR code -- discovery via adb's own mDNS, so it works even in WSL2, containers and VMs
|
|
5
|
+
Project-URL: Repository, https://github.com/aleixrodriala/adb-qr
|
|
6
|
+
Project-URL: Issues, https://github.com/aleixrodriala/adb-qr/issues
|
|
7
|
+
Author-email: Aleix <aleixrodriala@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: adb,android,mdns,qr-code,wireless-debugging,wsl2
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: qrcode>=7
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# adb-qr
|
|
23
|
+
|
|
24
|
+
Pair your Android phone for wireless debugging by scanning a QR code in your
|
|
25
|
+
terminal — like Android Studio's *"Pair device with QR code"*, but for the
|
|
26
|
+
CLI. No typing pairing codes, no looking up IPs and ports.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
## Why another QR-pairing tool?
|
|
31
|
+
|
|
32
|
+
Existing tools ([adb-wireless](https://github.com/teamclouday/adb-wireless),
|
|
33
|
+
[adb-qr](https://github.com/oosawy/adb-qr), and friends) run their **own**
|
|
34
|
+
mDNS listener to discover the phone after it scans the QR. That works on a
|
|
35
|
+
native host — and silently fails inside **WSL2, containers, and VMs**, where
|
|
36
|
+
LAN multicast never reaches the virtualized network. The QR renders, the
|
|
37
|
+
phone scans it, and then... nothing.
|
|
38
|
+
|
|
39
|
+
`adb-qr` doesn't listen for mDNS at all. It asks the **adb server itself**
|
|
40
|
+
what it sees (`adb mdns services`), so discovery happens wherever the server
|
|
41
|
+
runs. On WSL2, point it at the Windows `adb.exe` (it does this automatically)
|
|
42
|
+
and the Windows side — which *can* see your LAN — does the discovery. The
|
|
43
|
+
same trick means zero networking dependencies on any platform: if `adb`
|
|
44
|
+
works, `adb-qr` works.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
With [uv](https://docs.astral.sh/uv/) (recommended):
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uv tool install git+https://github.com/aleixrodriala/adb-qr
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With [pipx](https://pipx.pypa.io/):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
pipx install git+https://github.com/aleixrodriala/adb-qr
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or run it once without installing anything:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
uvx --from git+https://github.com/aleixrodriala/adb-qr adb-qr
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Requirements
|
|
67
|
+
|
|
68
|
+
- Android **platform-tools 31+** on the host that has network visibility
|
|
69
|
+
(on WSL2: install them on **Windows**; `adb-qr` finds `adb.exe` by itself)
|
|
70
|
+
- A phone on **Android 11+**, on the same Wi-Fi as that host
|
|
71
|
+
- Python 3.10+ (handled for you by `uv`/`pipx`)
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
adb-qr # show QR, wait for scan, pair, connect
|
|
77
|
+
adb-qr --pair-only # stop after pairing (adb usually auto-connects anyway)
|
|
78
|
+
adb-qr --timeout 300 # wait longer for the scan
|
|
79
|
+
adb-qr --no-invert # flip QR colors if your phone won't scan it
|
|
80
|
+
adb-qr --adb /path/to/adb # explicit adb binary (env var ADB works too)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
On the phone: **Settings → Developer options → Wireless debugging →
|
|
84
|
+
Pair device with QR code**, and point the camera at your terminal.
|
|
85
|
+
|
|
86
|
+
## How it works
|
|
87
|
+
|
|
88
|
+
The QR encodes `WIFI:T:ADB;S:<name>;P:<password>;;` — the same payload
|
|
89
|
+
Android Studio generates. When the phone scans it:
|
|
90
|
+
|
|
91
|
+
1. The phone starts advertising an `_adb-tls-pairing._tcp` mDNS service
|
|
92
|
+
whose instance name is the `<name>` from the QR.
|
|
93
|
+
2. `adb-qr` polls `adb mdns services` until that service shows up, then runs
|
|
94
|
+
`adb pair <ip>:<port> <password>`.
|
|
95
|
+
3. After pairing, the phone's `_adb-tls-connect._tcp` service is used to
|
|
96
|
+
`adb connect` (recent adb versions often auto-connect on their own —
|
|
97
|
+
`adb-qr` detects that too and gets out of the way).
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
- **QR won't scan** — try `--no-invert`; some terminal color schemes render
|
|
102
|
+
the QR with polarity a given camera dislikes.
|
|
103
|
+
- **Times out waiting for scan** — the phone must be on the same network as
|
|
104
|
+
the machine where the *adb server* runs. On WSL2 that machine is Windows,
|
|
105
|
+
not the WSL VM. Corporate/guest Wi-Fi often blocks mDNS entirely.
|
|
106
|
+
- **`adb mdns check` fails** — `adb-qr` force-enables the openscreen mDNS
|
|
107
|
+
backend (`ADB_MDNS_OPENSCREEN=1`) and restarts the server once; if it still
|
|
108
|
+
fails, update platform-tools.
|
|
109
|
+
- **Paired but not connected** — run `adb connect <ip>:<port>` with the port
|
|
110
|
+
shown on the phone's Wireless debugging screen (it differs from the
|
|
111
|
+
pairing port).
|
|
112
|
+
|
|
113
|
+
## Prior art
|
|
114
|
+
|
|
115
|
+
The pair-by-QR flow was pioneered for the terminal by
|
|
116
|
+
[teamclouday/adb-wireless](https://github.com/teamclouday/adb-wireless).
|
|
117
|
+
`adb-qr` exists because none of the existing tools survive WSL2's NAT — and
|
|
118
|
+
delegating discovery to the adb server turned out to make the whole thing
|
|
119
|
+
simpler, too.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
adb_qr-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# adb-qr
|
|
2
|
+
|
|
3
|
+
Pair your Android phone for wireless debugging by scanning a QR code in your
|
|
4
|
+
terminal — like Android Studio's *"Pair device with QR code"*, but for the
|
|
5
|
+
CLI. No typing pairing codes, no looking up IPs and ports.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Why another QR-pairing tool?
|
|
10
|
+
|
|
11
|
+
Existing tools ([adb-wireless](https://github.com/teamclouday/adb-wireless),
|
|
12
|
+
[adb-qr](https://github.com/oosawy/adb-qr), and friends) run their **own**
|
|
13
|
+
mDNS listener to discover the phone after it scans the QR. That works on a
|
|
14
|
+
native host — and silently fails inside **WSL2, containers, and VMs**, where
|
|
15
|
+
LAN multicast never reaches the virtualized network. The QR renders, the
|
|
16
|
+
phone scans it, and then... nothing.
|
|
17
|
+
|
|
18
|
+
`adb-qr` doesn't listen for mDNS at all. It asks the **adb server itself**
|
|
19
|
+
what it sees (`adb mdns services`), so discovery happens wherever the server
|
|
20
|
+
runs. On WSL2, point it at the Windows `adb.exe` (it does this automatically)
|
|
21
|
+
and the Windows side — which *can* see your LAN — does the discovery. The
|
|
22
|
+
same trick means zero networking dependencies on any platform: if `adb`
|
|
23
|
+
works, `adb-qr` works.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
With [uv](https://docs.astral.sh/uv/) (recommended):
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv tool install git+https://github.com/aleixrodriala/adb-qr
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With [pipx](https://pipx.pypa.io/):
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pipx install git+https://github.com/aleixrodriala/adb-qr
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or run it once without installing anything:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
uvx --from git+https://github.com/aleixrodriala/adb-qr adb-qr
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Requirements
|
|
46
|
+
|
|
47
|
+
- Android **platform-tools 31+** on the host that has network visibility
|
|
48
|
+
(on WSL2: install them on **Windows**; `adb-qr` finds `adb.exe` by itself)
|
|
49
|
+
- A phone on **Android 11+**, on the same Wi-Fi as that host
|
|
50
|
+
- Python 3.10+ (handled for you by `uv`/`pipx`)
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
adb-qr # show QR, wait for scan, pair, connect
|
|
56
|
+
adb-qr --pair-only # stop after pairing (adb usually auto-connects anyway)
|
|
57
|
+
adb-qr --timeout 300 # wait longer for the scan
|
|
58
|
+
adb-qr --no-invert # flip QR colors if your phone won't scan it
|
|
59
|
+
adb-qr --adb /path/to/adb # explicit adb binary (env var ADB works too)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On the phone: **Settings → Developer options → Wireless debugging →
|
|
63
|
+
Pair device with QR code**, and point the camera at your terminal.
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
The QR encodes `WIFI:T:ADB;S:<name>;P:<password>;;` — the same payload
|
|
68
|
+
Android Studio generates. When the phone scans it:
|
|
69
|
+
|
|
70
|
+
1. The phone starts advertising an `_adb-tls-pairing._tcp` mDNS service
|
|
71
|
+
whose instance name is the `<name>` from the QR.
|
|
72
|
+
2. `adb-qr` polls `adb mdns services` until that service shows up, then runs
|
|
73
|
+
`adb pair <ip>:<port> <password>`.
|
|
74
|
+
3. After pairing, the phone's `_adb-tls-connect._tcp` service is used to
|
|
75
|
+
`adb connect` (recent adb versions often auto-connect on their own —
|
|
76
|
+
`adb-qr` detects that too and gets out of the way).
|
|
77
|
+
|
|
78
|
+
## Troubleshooting
|
|
79
|
+
|
|
80
|
+
- **QR won't scan** — try `--no-invert`; some terminal color schemes render
|
|
81
|
+
the QR with polarity a given camera dislikes.
|
|
82
|
+
- **Times out waiting for scan** — the phone must be on the same network as
|
|
83
|
+
the machine where the *adb server* runs. On WSL2 that machine is Windows,
|
|
84
|
+
not the WSL VM. Corporate/guest Wi-Fi often blocks mDNS entirely.
|
|
85
|
+
- **`adb mdns check` fails** — `adb-qr` force-enables the openscreen mDNS
|
|
86
|
+
backend (`ADB_MDNS_OPENSCREEN=1`) and restarts the server once; if it still
|
|
87
|
+
fails, update platform-tools.
|
|
88
|
+
- **Paired but not connected** — run `adb connect <ip>:<port>` with the port
|
|
89
|
+
shown on the phone's Wireless debugging screen (it differs from the
|
|
90
|
+
pairing port).
|
|
91
|
+
|
|
92
|
+
## Prior art
|
|
93
|
+
|
|
94
|
+
The pair-by-QR flow was pioneered for the terminal by
|
|
95
|
+
[teamclouday/adb-wireless](https://github.com/teamclouday/adb-wireless).
|
|
96
|
+
`adb-qr` exists because none of the existing tools survive WSL2's NAT — and
|
|
97
|
+
delegating discovery to the adb server turned out to make the whole thing
|
|
98
|
+
simpler, too.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
adb_qr-0.1.0/adb_qr.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script --quiet
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["qrcode>=7"]
|
|
5
|
+
# ///
|
|
6
|
+
"""Pair an Android device for wireless ADB by scanning a QR code in the terminal.
|
|
7
|
+
|
|
8
|
+
Like Android Studio's "Pair device with QR code", but for the CLI:
|
|
9
|
+
|
|
10
|
+
1. Show a QR encoding ``WIFI:T:ADB;S:<name>;P:<password>;;``
|
|
11
|
+
2. The phone scans it (Developer options > Wireless debugging > Pair device
|
|
12
|
+
with QR code) and starts advertising an ``_adb-tls-pairing._tcp`` mDNS
|
|
13
|
+
service named ``<name>``.
|
|
14
|
+
3. We discover it and run ``adb pair ip:port password``.
|
|
15
|
+
4. We find the device's ``_adb-tls-connect._tcp`` service (or let adb's
|
|
16
|
+
auto-connect kick in) and ``adb connect``.
|
|
17
|
+
|
|
18
|
+
Unlike other QR-pairing tools, discovery is delegated to the adb server
|
|
19
|
+
itself (``adb mdns services``) instead of an in-process zeroconf listener.
|
|
20
|
+
That makes it work in places where LAN multicast never reaches the tool --
|
|
21
|
+
WSL2, containers, VMs -- as long as the adb server runs somewhere with real
|
|
22
|
+
network visibility (on WSL2: the Windows adb.exe).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import glob
|
|
31
|
+
import os
|
|
32
|
+
import secrets
|
|
33
|
+
import shutil
|
|
34
|
+
import string
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
|
|
39
|
+
import qrcode
|
|
40
|
+
|
|
41
|
+
PAIR_SVC = "_adb-tls-pairing"
|
|
42
|
+
CONNECT_SVC = "_adb-tls-connect"
|
|
43
|
+
WIN_ADB_GLOB = "/mnt/c/Users/*/AppData/Local/Android/Sdk/platform-tools/adb.exe"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_wsl() -> bool:
|
|
47
|
+
try:
|
|
48
|
+
with open("/proc/version") as f:
|
|
49
|
+
return "microsoft" in f.read().lower()
|
|
50
|
+
except OSError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_adb(override: str | None) -> str:
|
|
55
|
+
if override:
|
|
56
|
+
return override
|
|
57
|
+
if os.environ.get("ADB"):
|
|
58
|
+
return os.environ["ADB"]
|
|
59
|
+
if is_wsl():
|
|
60
|
+
# Prefer the Windows adb: its server runs on the Windows side, where
|
|
61
|
+
# LAN multicast is visible. A Linux adb inside WSL2 sits behind NAT
|
|
62
|
+
# and its mDNS discovery comes up empty.
|
|
63
|
+
exe = shutil.which("adb.exe") or next(iter(glob.glob(WIN_ADB_GLOB)), None)
|
|
64
|
+
if exe:
|
|
65
|
+
return exe
|
|
66
|
+
linux_adb = shutil.which("adb")
|
|
67
|
+
if linux_adb:
|
|
68
|
+
print(
|
|
69
|
+
"warning: no Windows adb.exe found, falling back to Linux adb -- "
|
|
70
|
+
"mDNS discovery will likely fail inside WSL2. Install "
|
|
71
|
+
"platform-tools on the Windows side for reliable pairing.",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
return linux_adb
|
|
75
|
+
else:
|
|
76
|
+
found = shutil.which("adb")
|
|
77
|
+
if found:
|
|
78
|
+
return found
|
|
79
|
+
home = os.path.expanduser("~")
|
|
80
|
+
candidates = [
|
|
81
|
+
os.path.join(home, "Library", "Android", "sdk", "platform-tools", "adb"),
|
|
82
|
+
os.path.join(home, "Android", "Sdk", "platform-tools", "adb"),
|
|
83
|
+
]
|
|
84
|
+
if os.environ.get("LOCALAPPDATA"):
|
|
85
|
+
candidates.append(
|
|
86
|
+
os.path.join(
|
|
87
|
+
os.environ["LOCALAPPDATA"],
|
|
88
|
+
"Android", "Sdk", "platform-tools", "adb.exe",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
for p in candidates:
|
|
92
|
+
if os.path.exists(p):
|
|
93
|
+
return p
|
|
94
|
+
sys.exit("adb not found -- install Android platform-tools or pass --adb /path/to/adb")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def adb_env() -> dict[str, str]:
|
|
98
|
+
# ADB_MDNS_OPENSCREEN=1 forces the built-in mdns backend on older
|
|
99
|
+
# platform-tools; harmless on recent versions where it is the default.
|
|
100
|
+
env = {**os.environ, "ADB_MDNS_OPENSCREEN": "1"}
|
|
101
|
+
if is_wsl():
|
|
102
|
+
# WSLENV makes the variable cross the WSL->Windows interop boundary,
|
|
103
|
+
# so the adb *server* (a Windows process) actually sees it.
|
|
104
|
+
env["WSLENV"] = (os.environ.get("WSLENV", "") + ":ADB_MDNS_OPENSCREEN/u").lstrip(":")
|
|
105
|
+
return env
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_adb(adb_path: str, *args: str, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
109
|
+
return subprocess.run(
|
|
110
|
+
[adb_path, *args], capture_output=True, text=True, env=adb_env(), timeout=timeout
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def mdns_services(adb_path: str) -> list[tuple[str, str, str, int]]:
|
|
115
|
+
"""Parse `adb mdns services` into (instance, service_type, ip, port) rows."""
|
|
116
|
+
out = run_adb(adb_path, "mdns", "services").stdout
|
|
117
|
+
rows = []
|
|
118
|
+
for line in out.splitlines():
|
|
119
|
+
line = line.rstrip("\r")
|
|
120
|
+
parts = line.split("\t") if "\t" in line else line.split()
|
|
121
|
+
if len(parts) < 3 or not parts[1].startswith("_adb-tls-"):
|
|
122
|
+
continue
|
|
123
|
+
instance, svc, endpoint = parts[0], parts[1], parts[2]
|
|
124
|
+
ip, _, port = endpoint.rpartition(":")
|
|
125
|
+
if ip and port.isdigit():
|
|
126
|
+
rows.append((instance, svc, ip, int(port)))
|
|
127
|
+
return rows
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def ensure_mdns(adb_path: str) -> None:
|
|
131
|
+
check = run_adb(adb_path, "mdns", "check")
|
|
132
|
+
if "daemon version" in check.stdout:
|
|
133
|
+
return
|
|
134
|
+
# The server may have been started without mdns enabled; restart it with
|
|
135
|
+
# our environment and try once more.
|
|
136
|
+
run_adb(adb_path, "kill-server")
|
|
137
|
+
check = run_adb(adb_path, "mdns", "check")
|
|
138
|
+
if "daemon version" in check.stdout:
|
|
139
|
+
return
|
|
140
|
+
msg = (check.stdout + check.stderr).strip()
|
|
141
|
+
if "unknown command" in msg or "usage:" in msg.lower():
|
|
142
|
+
sys.exit(
|
|
143
|
+
"this adb is too old for `adb mdns services` -- update Android "
|
|
144
|
+
"platform-tools (31+): https://developer.android.com/tools/releases/platform-tools"
|
|
145
|
+
)
|
|
146
|
+
sys.exit(f"adb mdns unavailable: {msg}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def print_qr(text: str, invert: bool) -> None:
|
|
150
|
+
qr = qrcode.QRCode(border=2)
|
|
151
|
+
qr.add_data(text)
|
|
152
|
+
qr.print_ascii(invert=invert)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main() -> int:
|
|
156
|
+
ap = argparse.ArgumentParser(
|
|
157
|
+
prog="adb-qr",
|
|
158
|
+
description="Pair (and connect) an Android device over wireless ADB by QR code.",
|
|
159
|
+
)
|
|
160
|
+
ap.add_argument("--adb", help="path to the adb binary (default: autodetect; env ADB also works)")
|
|
161
|
+
ap.add_argument("--timeout", type=int, default=120, metavar="SECONDS",
|
|
162
|
+
help="how long to wait for the phone to scan (default: 120)")
|
|
163
|
+
ap.add_argument("--pair-only", action="store_true",
|
|
164
|
+
help="stop after pairing; don't wait for a connection")
|
|
165
|
+
ap.add_argument("--no-invert", action="store_true",
|
|
166
|
+
help="flip QR colors if your scanner struggles on this terminal background")
|
|
167
|
+
ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
168
|
+
args = ap.parse_args()
|
|
169
|
+
|
|
170
|
+
adb_path = find_adb(args.adb)
|
|
171
|
+
ensure_mdns(adb_path)
|
|
172
|
+
|
|
173
|
+
name = f"adbqr-{secrets.token_hex(4)}"
|
|
174
|
+
alphabet = string.ascii_letters + string.digits
|
|
175
|
+
password = "".join(secrets.choice(alphabet) for _ in range(12))
|
|
176
|
+
|
|
177
|
+
print_qr(f"WIFI:T:ADB;S:{name};P:{password};;", invert=not args.no_invert)
|
|
178
|
+
print("On the phone: Developer options > Wireless debugging > Pair device with QR code")
|
|
179
|
+
print(f"Waiting for scan (up to {args.timeout}s)... ", end="", flush=True)
|
|
180
|
+
|
|
181
|
+
# Phase 1: after scanning, the phone advertises _adb-tls-pairing named
|
|
182
|
+
# after the QR's S: field. Match on that; if some device doesn't echo the
|
|
183
|
+
# name back, fall back to a lone pairing service (the password still
|
|
184
|
+
# guards against pairing with the wrong device).
|
|
185
|
+
pair_ip = pair_port = None
|
|
186
|
+
deadline = time.monotonic() + args.timeout
|
|
187
|
+
try:
|
|
188
|
+
while time.monotonic() < deadline:
|
|
189
|
+
pairing = [r for r in mdns_services(adb_path) if r[1].startswith(PAIR_SVC)]
|
|
190
|
+
match = [r for r in pairing if r[0] == name] or (
|
|
191
|
+
pairing if len(pairing) == 1 else []
|
|
192
|
+
)
|
|
193
|
+
if match:
|
|
194
|
+
_, _, pair_ip, pair_port = match[0]
|
|
195
|
+
break
|
|
196
|
+
time.sleep(1)
|
|
197
|
+
except KeyboardInterrupt:
|
|
198
|
+
print("\ninterrupted")
|
|
199
|
+
return 130
|
|
200
|
+
if not pair_ip:
|
|
201
|
+
print("\nTimed out: the phone never appeared in `adb mdns services`.")
|
|
202
|
+
print("Make sure it is on the same network as the machine running the adb")
|
|
203
|
+
print("server (on WSL2 that's Windows, not the WSL VM) and try again.")
|
|
204
|
+
return 1
|
|
205
|
+
print(f"found {pair_ip}:{pair_port}")
|
|
206
|
+
|
|
207
|
+
res = run_adb(adb_path, "pair", f"{pair_ip}:{pair_port}", password)
|
|
208
|
+
if "Successfully paired" not in res.stdout:
|
|
209
|
+
print(f"Pairing failed: {(res.stdout + res.stderr).strip()}", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
print(res.stdout.strip())
|
|
212
|
+
if args.pair_only:
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
# Phase 2: connect. Recent adb auto-connects to paired devices it sees
|
|
216
|
+
# via mDNS, so watch `adb devices` while also trying an explicit connect
|
|
217
|
+
# against the device's _adb-tls-connect service.
|
|
218
|
+
deadline = time.monotonic() + 30
|
|
219
|
+
try:
|
|
220
|
+
while time.monotonic() < deadline:
|
|
221
|
+
devices = run_adb(adb_path, "devices").stdout
|
|
222
|
+
connected = [
|
|
223
|
+
line for line in devices.splitlines()
|
|
224
|
+
if line.strip().endswith("device") and (pair_ip in line or CONNECT_SVC in line)
|
|
225
|
+
]
|
|
226
|
+
if connected:
|
|
227
|
+
print(f"Connected: {connected[0].split()[0]}")
|
|
228
|
+
return 0
|
|
229
|
+
for _, svc, ip, port in mdns_services(adb_path):
|
|
230
|
+
if svc.startswith(CONNECT_SVC) and ip == pair_ip:
|
|
231
|
+
res = run_adb(adb_path, "connect", f"{ip}:{port}")
|
|
232
|
+
print(res.stdout.strip())
|
|
233
|
+
time.sleep(1)
|
|
234
|
+
except KeyboardInterrupt:
|
|
235
|
+
print("\ninterrupted (already paired -- `adb connect` manually if needed)")
|
|
236
|
+
return 130
|
|
237
|
+
|
|
238
|
+
print("Paired, but no connection after 30s -- run `adb connect <ip>:<port>` with")
|
|
239
|
+
print("the port shown on the phone's Wireless debugging screen.")
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
sys.exit(main())
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script --quiet
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["qrcode>=7", "rich>=13"]
|
|
5
|
+
# ///
|
|
6
|
+
"""Regenerate the README demo image.
|
|
7
|
+
|
|
8
|
+
This writes demo.svg; rasterize it with a real browser so the embedded
|
|
9
|
+
webfont renders the QR's half-block glyphs correctly (cairosvg mangles them):
|
|
10
|
+
|
|
11
|
+
chrome --headless=new --screenshot=demo.png --window-size=2038,1272 \
|
|
12
|
+
--hide-scrollbars --virtual-time-budget=10000 file://.../demo.svg
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import pathlib
|
|
17
|
+
|
|
18
|
+
import qrcode
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
qr = qrcode.QRCode(border=2)
|
|
22
|
+
qr.add_data("WIFI:T:ADB;S:adbqr-3f9a17c2;P:kP4mW9xQz2Lr;;")
|
|
23
|
+
buf = io.StringIO()
|
|
24
|
+
qr.print_ascii(out=buf, invert=True)
|
|
25
|
+
|
|
26
|
+
console = Console(record=True, width=82)
|
|
27
|
+
console.print("[bold green]$[/] [bold]adb-qr[/]")
|
|
28
|
+
console.print(buf.getvalue().rstrip("\n"), markup=False, highlight=False)
|
|
29
|
+
console.print("On the phone: Developer options > Wireless debugging > Pair device with QR code")
|
|
30
|
+
console.print("Waiting for scan (up to 120s)... [cyan]found 192.168.1.42:40331[/]")
|
|
31
|
+
console.print("[green]Successfully paired to 192.168.1.42:40331 \\[guid=adb-1A2B3C4D-aBcDeF][/]")
|
|
32
|
+
console.print("[bold green]Connected: 192.168.1.42:44915[/]")
|
|
33
|
+
console.save_svg(str(pathlib.Path(__file__).parent / "demo.svg"), title="adb-qr")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "adb-qr"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pair Android devices for wireless ADB by QR code -- discovery via adb's own mDNS, so it works even in WSL2, containers and VMs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
dependencies = ["qrcode>=7"]
|
|
9
|
+
authors = [{ name = "Aleix", email = "aleixrodriala@gmail.com" }]
|
|
10
|
+
keywords = ["adb", "android", "wireless-debugging", "qr-code", "wsl2", "mdns"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Software Development :: Debuggers",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Repository = "https://github.com/aleixrodriala/adb-qr"
|
|
23
|
+
Issues = "https://github.com/aleixrodriala/adb-qr/issues"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
adb-qr = "adb_qr:main"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
include = ["adb_qr.py"]
|
adb_qr-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "adb-qr"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
source = { editable = "." }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "qrcode" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[package.metadata]
|
|
14
|
+
requires-dist = [{ name = "qrcode", specifier = ">=7" }]
|
|
15
|
+
|
|
16
|
+
[[package]]
|
|
17
|
+
name = "colorama"
|
|
18
|
+
version = "0.4.6"
|
|
19
|
+
source = { registry = "https://pypi.org/simple" }
|
|
20
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
21
|
+
wheels = [
|
|
22
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[[package]]
|
|
26
|
+
name = "qrcode"
|
|
27
|
+
version = "8.2"
|
|
28
|
+
source = { registry = "https://pypi.org/simple" }
|
|
29
|
+
dependencies = [
|
|
30
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
31
|
+
]
|
|
32
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
|
|
33
|
+
wheels = [
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
|
|
35
|
+
]
|