# 用 adb 无线调试其他设备

在 Muse 的 Linux 容器里安装好 adb，即可通过无线调试（Android 11 及以上）连接另一台 Android 手机，或连接 Muse 所在的这台手机本身——全程不需要电脑。

## 适用场景

- **调试另一台手机**：安装应用、抓日志、截屏、模拟输入、上传下载文件。
- **调试本机**：在 Muse 里对正在使用的这台手机执行同样的操作。
- 需要 root 权限的操作不适用：adb 以 `shell`（uid 2000）身份运行。

## 环境基线

| 项 | 值 |
| --- | --- |
| 系统 | Debian GNU/Linux 12 (bookworm) aarch64（PRoot 容器） |
| adb | 34.0.5（Debian backports；仓库默认版本过旧，缺少 `pair` 命令） |
| 目标设备 | Android 11 及以上（本文实测 Android 15 与 Android 16） |
| 网络 | 调试另一台手机时两台设备需在同一局域网；调试本机走 `127.0.0.1`，不依赖局域网 |
| 权限 | shell（uid 2000），非 root |

## 1. 安装 adb

容器仓库自带的 adb 版本过旧，缺少无线调试所需的 `pair` 命令，需要从 backports 安装：

```bash
apt update
apt install -y -t bookworm-backports adb
adb version
# 应输出 Version 34.x
```

**注意**：adb 安装在容器系统目录，重装容器后需要重新安装，配对记录也会一并丢失。

## 2. 配对（只需一次）

先在目标手机上操作：

1. 设置 → 关于手机 → 连点「版本号」7 次，开启开发者选项；
2. 开发者选项 → 打开「无线调试」；
3. 点「使用配对码配对设备」，屏幕会显示 **IP 地址和端口** 与 **6 位配对码**。

建议把「无线调试」设置页与 Muse 分屏显示，边看边操作——配对对话框一旦被切走或关闭，端口和配对码就立即失效了。配对端口也可以用文末的脚本自动发现，只有 6 位配对码必须读屏幕。

回到 Muse 终端，把端口和配对码换成屏幕上的值：

```bash
adb pair 192.168.1.23:37123 123456
# 成功会输出 Successfully paired to ...
```

**注意**：配对端口和配对码存活时间很短，对话框一关闭就立即失效，需要重新打开。配对成功后设备会记住这台主机，之后不必再配对。

## 3. 连接

```bash
adb connect 192.168.1.23:40001
adb devices -l
```

`192.168.1.23:40001` 是无线调试主页显示的「IP 地址和端口」，注意它与配对用的端口不是同一个。

连接端口每次重新打开无线调试都会变化，可以重新查看手机屏幕，或用文末脚本自动发现。

## 4. 调试本机

调试 Muse 所在的这台手机时，地址用 `127.0.0.1`（容器与手机共享网络，不依赖局域网）：

```bash
adb pair 127.0.0.1:37123 123456
adb connect 127.0.0.1:40001
adb devices
```

**注意**：用手机的局域网 IP 也能连，但同一台设备会同时出现两条记录（一条 `127.0.0.1`、一条局域网 IP），用 `adb -s <序列号>` 指定设备，或 `adb disconnect` 去掉多余的那条。

调试本机的价值在于：Android 限制普通应用直接调用系统命令（`screencap`、`pm`、`dumpsys` 等会被拒绝），而以 `shell` 身份通过 adb 执行则不受此限制。

## 常用操作

```bash
adb shell getprop ro.product.model      # 设备型号
adb shell pm list packages              # 已安装应用
adb shell logcat -d                     # 导出当前日志
adb exec-out screencap -p > screen.png  # 截屏
adb push local.txt /sdcard/             # 上传文件
adb pull /sdcard/photo.jpg ./           # 下载文件
adb shell input keyevent KEYCODE_HOME   # 模拟按键
```

安装应用用 `adb install app.apk`，部分设备还需要在开发者选项里允许「通过 USB 安装应用」。

这些命令都以 `shell` 用户身份运行，可以读写 `/sdcard`，但无法访问 `/data/data` 等受限目录。

## 常见问题

**报错 `protocol fault (couldn't read status message)`**

最常见的原因是配对端口已失效（对话框关闭或重新打开过），重新打开对话框、用新的端口和配对码再配一次即可。如果端口确认是最新的仍报此错，直接重试同一条命令——部分 adb 版本存在该已知问题，第二次往往就能成功。

**报错 `failed to connect to ...: Connection refused`**

连接端口过期，或设备尚未配对。重新查看无线调试主页的端口，必要时重新配对。

**每个新终端都要重新连接**

adb 服务随终端会话结束而退出，新会话里需要重新执行 `adb connect`。把 `adb start-server`、`adb connect` 和后续命令放在同一个终端里执行即可。

**`adb mdns services` 列表为空**

容器里 adb 自带的 mDNS 发现无法工作——它需要的 5353 端口已被手机系统的 mDNS 服务占用。这不影响手动连接，自动发现请用下面的脚本。

## 附：自动发现端口

无线调试开启后，设备会在局域网广播 `_adb-tls-pairing._tcp`（配对服务，仅在配对对话框打开时）和 `_adb-tls-connect._tcp`（连接服务）。把下面的脚本保存为 `discover_adb.py`，运行后将分别列出两者的「IP:端口」：

```python
#!/usr/bin/env python3
"""扫描局域网中的 adb 无线调试端点"""
import socket
import struct
import time

SERVICES = {
    '配对端口': '_adb-tls-pairing._tcp.local',
    '连接端口': '_adb-tls-connect._tcp.local',
}


def encode(name):
    out = b''
    for part in name.split('.'):
        out += bytes([len(part)]) + part.encode()
    return out + b'\x00'


def read_name(data, off):
    labels, jumped, start = [], False, off
    while True:
        length = data[off]
        if length == 0:
            off += 1
            break
        if length & 0xC0 == 0xC0:
            ptr = struct.unpack('!H', data[off:off + 2])[0] & 0x3FFF
            if not jumped:
                start = off + 2
            off, jumped = ptr, True
            continue
        labels.append(data[off + 1:off + 1 + length].decode())
        off += 1 + length
    return '.'.join(labels), (start if jumped else off)


def scan(service, seconds=6):
    # class 高位置 1 表示请求单播响应：配对服务对普通查询只回组播，
    # 不绑定 5353 就收不到；连接服务则回单播
    query = (struct.pack('!HHHHHH', 0, 0, 1, 0, 0, 0)
             + encode(service)
             + struct.pack('!HH', 12, 0x8001))
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
                    struct.pack('4s4s', socket.inet_aton('224.0.0.251'),
                                socket.inet_aton('0.0.0.0')))
    sock.bind(('', 0))
    sock.settimeout(0.3)
    endpoints = set()
    deadline = time.time() + seconds
    while time.time() < deadline:
        sock.sendto(query, ('224.0.0.251', 5353))
        try:
            data, addr = sock.recvfrom(9000)
        except socket.timeout:
            continue
        qd, an, ns, ar = struct.unpack('!HHHH', data[4:12])
        off = 12
        for _ in range(qd):
            _, off = read_name(data, off)
            off += 4
        for _ in range(an + ns + ar):
            name, off = read_name(data, off)
            rtype, _, _, rdlen = struct.unpack('!HHIH', data[off:off + 10])
            off += 10
            rdata = data[off:off + rdlen]
            off += rdlen
            if rtype == 33 and service.split('.')[0] in name:
                port = struct.unpack('!HHH', rdata[:6])[2]
                endpoints.add(f'{addr[0]}:{port}')
    sock.close()
    return endpoints


for label, service in SERVICES.items():
    for endpoint in sorted(scan(service)):
        print(f'{label}  {endpoint}')
```

用法：

```bash
python3 discover_adb.py
# 配对端口  192.168.1.23:37123
# 连接端口  192.168.1.23:40001
adb pair 192.168.1.23:37123 123456
adb connect 192.168.1.23:40001
```

**注意**：配对服务只在「使用配对码配对设备」对话框打开时广播，因此要在对话框开启时运行脚本；6 位配对码始终需要从手机屏幕读取。列表里可能出现多个结果（含设备重启前的过期端口），逐个 `adb connect` 试即可。
