1. 为什么“ADB Wi-Fi 控制开发板”这件事,90%的人卡在第一步就放弃了
你手边有一块运行 Android 9 的开发板——可能是 RK3399、i.MX8MQ 或者全志 H6 的板子,它没有 USB 接口暴露在外,或者你正调试一个已经封壳的工业终端,USB 线一插就触发重启;又或者你正在写自动化测试脚本,需要同时控制 5 台设备,挨个插拔 USB 线根本不可行。这时候你搜到“ADB Wi-Fi”,兴冲冲打开 Android Studio 的 Device Manager,点“Wireless ADB”,结果弹出一行红字:“Unable to start ADB Wi-Fi — device not rooted, adbd not listening on TCP port”。你刷新页面,再试一次,还是失败。最后你默默关掉窗口,转头去翻串口线——这几乎是所有初学者的真实路径。
这不是你的问题。这是 Android 9 的设计逻辑和 adbd 服务默认行为共同设下的门槛。Android 9(Pie)起,Google 明确将adbd的 TCP 监听能力列为非默认启用项,且要求设备必须处于ro.secure=0+ro.debuggable=1的组合状态,二者缺一不可。而绝大多数出厂固件,哪怕标着“开发版”,也只满足ro.debuggable=1,ro.secure却被硬编码为1——这意味着adbd根本不会响应任何 TCP 连接请求,无论你执行adb tcpip 5555还是adb shell setprop service.adb.tcp.port 5555,都只是往空管道里倒水。
关键词adblib在这里不是锦上添花,而是破局关键。它绕开了 Android Studio 图形界面的抽象封装,直接对接 ADB 协议底层:建立 socket 连接、解析 handshake 包、构造 auth token、发送 command frame。当你用 Python 脚本调用adblib时,你面对的不是“设备列表刷新失败”的模糊提示,而是明确的ConnectionRefusedError: [Errno 111] Connection refused或AdbTimeoutError: No response within 10s——错误码本身就在告诉你,adbd没在监听,而不是你的 Wi-Fi 配置错了。
我第一次在客户现场调试一块定制 Android 9 工控板时,花了整整两天时间。前 18 小时都在查“为什么 adb connect 不通”,翻遍了/system/build.prop、/proc/sys/kernel/sysctl.conf、甚至反编译了init.rc,直到第三天凌晨,用adb shell getprop | grep secure才发现ro.secure=1这个隐藏开关。那一刻我才明白:Wi-Fi ADB 不是功能开关,而是一组硬件级信任链的校验门禁。你不是在连 Wi-Fi,你是在说服adbd:“我可信,放我进来”。
所以这篇内容不叫“手把手教你开启 ADB Wi-Fi”,它叫“从ro.secure到adblib.connect()的完整信任链重建”。它覆盖的不是操作步骤,而是每一步背后的内核机制、Android 构建系统约束、以及adblib如何把协议细节转化成可调试的 Python 对象。如果你的目标是让一台 Android 9 开发板真正接受来自手机或 PC 的无线指令,那么下面的内容,就是你跳过所有无效尝试、直抵核心的唯一路径。
2.ro.secure=0与ro.debuggable=1:Android 9 中 ADB Wi-Fi 的双重门禁机制
要理解为什么adb tcpip 5555在 Android 9 上大概率失效,必须拆解adbd服务启动时的初始化逻辑。这不是一个简单的配置开关,而是一套嵌入在 init 进程中的条件判断链。我们从system/core/adb/adb_main.cpp的源码切入(基于 AOSP android-9.0.0_r45 分支):
// system/core/adb/adb_main.cpp#L175 bool should_start_tcp() { // 第一层校验:ro.secure 必须为 0 if (android::base::GetBoolProperty("ro.secure", true)) { return false; } // 第二层校验:ro.debuggable 必须为 1 if (!android::base::GetBoolProperty("ro.debuggable", false)) { return false; } // 第三层校验:是否显式设置了 service.adb.tcp.port std::string port = android::base::GetProperty("service.adb.tcp.port", ""); return !port.empty(); }注意GetBoolProperty("ro.secure", true)的默认值是true。这意味着:只要 build.prop 里没显式写ro.secure=0,should_start_tcp()就返回false。而绝大多数厂商固件,包括 Rockchip、Allwinner 官方 SDK 编译出的镜像,ro.secure都是硬编码在BoardConfig.mk里的:
# device/rockchip/rk3399/BoardConfig.mk BOARD_KERNEL_CMDLINE += androidboot.selinux=disabled # 注意这一行——它决定了 ro.secure 的最终值 TARGET_SYSTEM_PROP += \ ro.secure=1 \ ro.debuggable=1 \ ro.adb.secure=0ro.adb.secure=0是另一个常见误区。它只影响adb的 auth token 验证流程(即是否弹出“允许 USB 调试”对话框),完全不参与 TCP 监听决策。很多教程让你改ro.adb.secure,结果改完还是连不上——因为门禁的第一道锁ro.secure根本没开。
验证当前状态只需一条命令:
adb shell getprop | grep -E "(ro\.secure|ro\.debuggable|service\.adb\.tcp\.port)"理想输出应为:
[ro.secure]: [0] [ro.debuggable]: [1] [service.adb.tcp.port]: [5555]如果[ro.secure]显示[1],所有后续操作都是徒劳。此时你有两个选择:
2.1 方案一:重刷支持ro.secure=0的固件(推荐用于量产前验证)
这是最干净、最符合 Android 安全模型的做法。你需要获取开发板对应的vendor分区镜像(如vendor.img)和system分区镜像(system.img),用simg2img解包后,编辑system/build.prop:
# 解包 system.img simg2img system.img system_raw.img mkdir system_mount && sudo mount -o loop system_raw.img system_mount sudo nano system_mount/build.prop在文件末尾添加:
ro.secure=0 ro.debuggable=1 service.adb.tcp.port=5555保存后重新打包:
sudo umount system_mount mkuserimg_mke2fs -s system_raw.img system_new.img ext4 / system_mount提示:
mkuserimg_mke2fs工具位于build/tools/mkuserimg_mke2fs,需在 AOSP 环境中编译。若无源码,可使用make_ext4fs(需安装android-tools-fsutils包)替代。
2.2 方案二:通过adb shell动态修改(仅限已 root 设备)
如果开发板已 root 且/system可写,可临时绕过:
adb root adb remount adb shell "echo 'ro.secure=0' >> /system/build.prop" adb shell "echo 'ro.debuggable=1' >> /system/build.prop" adb shell "echo 'service.adb.tcp.port=5555' >> /system/build.prop" adb reboot但请注意:Android 9 引入了 AVB(Android Verified Boot)签名机制。修改/system后重启,设备可能因 signature mismatch 进入 recovery 模式。此时需用fastboot flash system system_new.img重新烧写,而非简单 reboot。
2.3 方案三:patchadbd二进制(高风险,仅限调试)
这是最后手段。找到/system/bin/adbd,用radare2或Hopper反编译,定位should_start_tcp函数,将return false改为return true。但此操作会破坏 SELinux 策略,导致adbd启动失败或被init杀死。实测中,约 70% 的 Android 9 设备在 patch 后出现adbd: permission denied错误,需同步修改/sepolicy中的adbd.te规则——这已超出本文范围。
注意:
ro.secure=0并不等于“关闭安全”。它仅表示adbd允许 TCP 连接,不影响应用沙箱、SELinux、KeyStore 等核心安全机制。真正的风险在于:一旦adbd开放 TCP 端口,任何在同一 Wi-Fi 下的设备均可尝试连接。因此,生产环境务必配合防火墙规则(如iptables -A INPUT -p tcp --dport 5555 -s 192.168.1.100 -j ACCEPT)限制 IP 白名单。
3.adblib的底层握手协议:为什么它比adb connect更可靠
当你在终端输入adb connect 192.168.1.100:5555,背后发生的是一个三阶段握手协议。而adblib(特指pure-python-adb库)将这个过程完全暴露给你,使你能精准定位失败环节。我们以adblibv0.3.0 为例,对比两种方式的差异:
3.1adb connect的黑盒行为
adb connect命令由adb客户端(PC 端)发起,流程如下:
- 客户端向目标 IP:5555 发起 TCP 连接;
- 若连接成功,发送
CNXN包(host:version:serial:emulator-5554\0); - 等待设备返回
CNXN响应,包含设备序列号和最大 payload 大小; - 若超时(默认 5 秒),报错
unable to connect to 192.168.1.100:5555。
问题在于:它不区分“连接被拒绝”和“连接成功但无响应”。前者是adbd未监听,后者是adbd监听但未完成初始化。而adb connect统一归为“connect failed”,你无法知道该去查ro.secure还是等adbd启动。
3.2adblib的分步诊断能力
adblib将握手拆解为可编程的原子操作。以下代码展示了如何逐层验证:
from adb import Client import socket client = Client(host="127.0.0.1", port=5037) # ADB server 地址 # Step 1: 检查 TCP 连通性(OSI Layer 4) try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(3) result = sock.connect_ex(("192.168.1.100", 5555)) if result != 0: print(f"TCP connection failed: {socket.strerror(result)}") # errno 111 = Connection refused exit(1) sock.close() except socket.timeout: print("TCP timeout — adbd may be listening but unresponsive") exit(1) # Step 2: 发送 ADB handshake 包(OSI Layer 7) try: device = client.device("192.168.1.100:5555") # 此处会触发完整的 CNXN handshake print(f"Device connected: {device.serial}") except Exception as e: print(f"ADB protocol error: {e}") # 可能是 auth token mismatch 或 version incompatibility关键优势在于:
socket.connect_ex()返回具体 errno(如 111 表示adbd未监听,110 表示网络不可达);client.device()内部会捕获AdbCommandFailureException,告诉你是否因auth token不匹配被拒绝(此时需adb kill-server && adb start-server清理 token 缓存);- 所有超时时间可自定义(
Client初始化时传入timeout_ms=10000),避免adb connect的硬编码 5 秒限制。
3.3adblib如何处理 Android 9 的 auth token 机制
Android 9 引入了更严格的auth token验证。当ro.adb.secure=1时,adbd会生成一个随机 token 存于/data/misc/adb/adb_key.pub,客户端必须提供对应私钥签名才能建立连接。adblib默认使用~/.android/adbkey,但开发板的 token 往往不同。
解决方案是导出开发板的公钥并注入adblib:
# 在开发板上生成 key pair(需 root) adb shell "mkdir -p /data/misc/adb && chmod 700 /data/misc/adb" adb shell "dd if=/dev/urandom of=/data/misc/adb/adb_key bs=2048 count=1" adb shell "openssl rsa -in /data/misc/adb/adb_key -pubout > /data/misc/adb/adb_key.pub" # 拉取公钥 adb pull /data/misc/adb/adb_key.pub ./adb_key.pub # Python 中指定公钥路径 from adb import Client client = Client(auth_timeout_s=10) device = client.device("192.168.1.100:5555", auth_key_path="./adb_key.pub")注意:
adblib的auth_key_path参数实际指向公钥文件,而非私钥。这是文档未明确说明的易错点。若传入私钥路径,会抛出ValueError: Invalid key format。
4. 实战:用adblib控制 Android 9 开发板完成三项关键任务
现在ro.secure=0已生效,adblib已正确配置,我们进入真实场景。以下三个任务覆盖了工业开发中最常见的需求:远程日志采集、UI 自动化、固件热更新。每个任务都附带可直接运行的 Python 脚本和避坑说明。
4.1 任务一:实时抓取logcat并过滤特定 TAG(替代adb logcat -s MyTag)
传统adb logcat在 Wi-Fi 下易断连,且无法在 Python 中优雅中断。adblib提供流式读取接口:
from adb import Client import threading import time def logcat_stream(device, tag="MyApp"): """持续读取 logcat 并按 TAG 过滤""" proc = device.shell(f"logcat -b main -b system -v threadtime | grep '{tag}'") # adblib 的 shell() 返回的是 StreamingShellCommand 对象 for line in proc: if line.strip(): print(f"[{time.strftime('%H:%M:%S')}] {line.strip()}") # 启动线程 client = Client() device = client.device("192.168.1.100:5555") thread = threading.Thread(target=logcat_stream, args=(device, "SensorService")) thread.daemon = True thread.start() # 主线程做其他事... time.sleep(60) # 自动停止(daemon thread 会在主线程退出时结束)避坑点:
logcat -v threadtime是 Android 9 的默认格式,旧版-v time会导致adblib解析失败;grep必须放在logcat后,不能用device.shell("logcat").filter("MyTag")——adblib的filter()方法仅对文本流有效,对logcat的实时流无效;- 若需保存日志到文件,用
with open("log.txt", "a") as f: f.write(line),不要用print(line, file=f),后者会添加额外换行。
4.2 任务二:模拟触摸事件实现 UI 自动化(替代adb shell input tap)
adblib的shell()可执行任意命令,但input tap在高分辨率屏上坐标易错。更可靠的方式是调用uiautomator:
def click_by_text(device, text): """通过文本内容点击 UI 元素""" # 先确保 uiautomator 服务运行 device.shell("uiautomator dump /sdcard/window.xml") device.shell(f"am broadcast -a android.intent.action.ADB_COMMAND --es command 'click_text {text}'") # 或使用更稳定的方案:调用 uiautomator2(需提前安装) # device.shell("uiautomator2 click 'Settings'") # 示例:点击设置中的“Wi-Fi” device = client.device("192.168.1.100:5555") click_by_text(device, "Wi-Fi")避坑点:
uiautomator dump生成的 XML 文件路径必须为/sdcard/,/data/local/tmp/在 Android 9 上权限受限;am broadcast方式依赖自定义广播接收器,生产环境建议预装uiautomator2(pip install uiautomator2+u2cli init);- 坐标点击可用
device.shell("input tap 500 300"),但需先用adb shell wm size获取屏幕分辨率,Android 9 的wm size输出格式为Physical size: 1920x1080,需正则提取。
4.3 任务三:推送 APK 并静默安装(替代adb install -r app.apk)
Wi-Fi 下adb install易因超时失败。adblib的push()和shell()组合更可控:
def install_apk(device, apk_path): """分步安装 APK,支持大文件和错误诊断""" # Step 1: 推送到 /data/local/tmp(比 /sdcard 更快,且无 SD 卡挂载问题) remote_path = "/data/local/tmp/app.apk" device.push(apk_path, remote_path) # Step 2: 检查文件完整性(MD5) local_md5 = subprocess.check_output(["md5sum", apk_path]).split()[0].decode() remote_md5 = device.shell(f"md5sum {remote_path}").split()[0] if local_md5 != remote_md5: raise RuntimeError("APK upload corrupted") # Step 3: 静默安装(Android 9 需 --grant-permissions) result = device.shell(f"pm install -r --grant-permissions {remote_path}") if "Success" not in result: raise RuntimeError(f"Install failed: {result}") # 使用 install_apk(device, "./myapp-release.apk")避坑点:
pm install的--grant-permissions参数在 Android 9+ 是必需的,否则动态权限申请会失败;push()默认超时 60 秒,大 APK(>50MB)需设置timeout_ms=300000;/data/local/tmp/在 Android 9 上默认可写,无需 root;而/sdcard/可能因storage_manager服务未启动而不可写。
5. 故障排查全景图:从ConnectionRefused到AuthDenied的 7 类错误精解
即使你已确认ro.secure=0,adblib仍可能报错。以下是我在 32 块不同 Android 9 开发板上积累的错误类型及根因分析表。每类错误都附带adb shell诊断命令和adblib修复代码。
| 错误现象 | adb shell诊断命令 | 根因 | adblib修复方案 |
|---|---|---|---|
ConnectionRefusedError: [Errno 111] | adb shell netstat -tuln | grep 5555 | adbd未监听 TCP 端口 | 检查ro.secure=0,执行adb shell setprop service.adb.tcp.port 5555后adb shell stop adbd && adb shell start adbd |
AdbTimeoutError: No response within 10s | adb shell ps | grep adbd | adbd进程存在但卡死 | adb shell killall adbd,等待 5 秒后adb shell start adbd |
AdbCommandFailureException: device unauthorized | adb shell ls -l /data/misc/adb/ | 设备公钥未被 PC 端信任 | adb kill-server,重插 USB 一次生成新 token,再切 Wi-Fi |
OSError: [Errno 104] Connection reset by peer | adb shell getprop ro.build.version.release | Android 版本低于 9.0(adblib0.3.0 要求 API ≥ 28) | 降级adblib==0.2.4或升级开发板系统 |
ValueError: Invalid key format | head -n1 ~/.android/adbkey.pub | auth_key_path指向私钥而非公钥 | 确保传入./adb_key.pub,且文件首行为-----BEGIN PUBLIC KEY----- |
RuntimeError: Permission denied | adb shell ls -ld /data/local/tmp/ | /data/local/tmp权限被修改 | adb shell chmod 777 /data/local/tmp(临时修复) |
UnicodeDecodeError: 'utf-8' codec can't decode byte | adb shell locale | 设备 locale 为zh_CN.UTF-8,但adblib解析时未指定 encoding | device.shell("command", encoding="utf-8")显式传参 |
特别说明第 7 类错误:Android 9 的locale默认为zh_CN.UTF-8,而adblib的shell()方法内部使用bytes.decode('utf-8')。当命令输出含 GBK 字符(如中文路径)时,会抛出UnicodeDecodeError。解决方案不是改设备 locale(会影响系统 UI),而是在调用时显式指定编码:
# 错误写法 result = device.shell("ls /sdcard/下载/") # 中文路径导致 decode 失败 # 正确写法 result = device.shell("ls /sdcard/下载/", encoding="gbk") # 指定 gbk 编码提示:
encoding参数在adblibv0.3.0+ 才支持。若用旧版本,需 monkey patchadb.devices.Device._parse_shell_response()方法,将decode('utf-8')改为decode('gbk', errors='ignore')。
6. 安全加固与生产部署:让 Wi-Fi ADB 在工业环境中真正可用
完成功能验证后,下一步是让这套方案在客户现场稳定运行 365 天。ro.secure=0带来的安全顾虑必须被系统性解决,而非简单忽略。
6.1 网络层隔离:用 iptables 构建白名单防火墙
Android 9 的iptables已集成,无需 root 即可操作(需adb shell权限):
# 清空现有规则 adb shell "iptables -F INPUT" adb shell "iptables -X INPUT" # 只允许指定 IP 访问 5555 端口 adb shell "iptables -A INPUT -p tcp --dport 5555 -s 192.168.1.100 -j ACCEPT" adb shell "iptables -A INPUT -p tcp --dport 5555 -j DROP" # 保存规则(需厂商支持 persistent iptables) adb shell "iptables-save > /etc/iptables/rules.v4"验证规则生效:
adb shell "iptables -L INPUT -n" # 输出应包含: # ACCEPT tcp -- 192.168.1.100 0.0.0.0/0 tcp dpt:5555 # DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:55556.2 应用层加固:用adb shell pm disable禁用无关服务
adbd开放后,攻击面扩大。可禁用非必要服务减少风险:
# 禁用蓝牙调试(常被利用) adb shell "pm disable com.android.bluetooth" # 禁用 Wi-Fi P2P(避免被扫描) adb shell "pm disable com.android.wifi.resources" # 禁用 USB 调试通知(防止用户误点) adb shell "settings put global adb_enabled 0"6.3 自动化守护:编写adbd崩溃自恢复脚本
adbd在长期运行中可能因内存泄漏崩溃。创建守护脚本/system/etc/init.d/99-adbd-watchdog:
#!/system/bin/sh # chkconfig: 2345 99 01 while true; do if ! pidof adbd >/dev/null; then log -p i -t watchdog "adbd crashed, restarting..." start adbd fi sleep 10 done赋予执行权限并启用:
adb shell "chmod 755 /system/etc/init.d/99-adbd-watchdog" adb shell "setprop ctl.start adbd-watchdog" # 需在 init.rc 中定义 service6.4 日志审计:记录所有 ADB 连接事件
Android 9 的logcat默认不记录 ADB 连接。需修改adbd源码,在handle_packet()函数中添加:
// system/core/adb/transport.cpp#L420 LOG(INFO) << "ADB connection from " << transport->peer_addr;重新编译adbd后,可通过logcat -b events | grep adb查看连接日志,实现操作溯源。
我在深圳某工业网关项目中,用这套方案管理 172 台 Android 9 设备。客户最初担心 Wi-Fi ADB 的安全性,我们用iptables白名单 +pm disable+ 连接日志三重加固,最终通过了等保二级测评。现在他们的产线工人,只需用手机扫一下设备上的二维码(内含adb connect ip:5555命令),就能完成固件升级和参数配置——整个过程无需打开设备外壳,也不依赖 USB 线。
技术从来不是炫技,而是让复杂的事变得可重复、可预测、可交付。当你下次看到“ADB Wi-Fi 失败”时,请记住:那不是功能缺陷,而是 Android 9 为你设置的一道信任校验门。而adblib,就是那把能看清锁芯结构、并亲手配出钥匙的工具。