04
2026
11

为TP-Link TPmini大眼睛路由器添加蓝牙适配器……并让AI帮忙写篇Blog

EMBEDDED · RETRO GAMING · KERNEL DEBUG

把蓝牙塞进一台 2013 年的
电视盒子

TPmini 大眼睛上外接 CSR8510 蓝牙适配器的完整记录 —— 从补齐 BlueZ 4.93,到定位 Linux 3.0.36 / DWC OTG 的 USB 传输层缺陷,再到 usbfs 旁路、控制面与数据面的硬边界,以及三次方案转向后最终交付的那块「适配器控制面板」。

周期 2026-10-27 → 11-04 平台 RK3066 / Android 4.2.2 / Linux 3.0.36+ 适配器 CSR8510 A10 · 0a12:0001 关键词 BlueZ · D-Bus · usbfs · HCI · dwc_otg
ROOT CAUSE
btusb 发送路径 + 老内核 USB 主机控制器
HARD LIMIT
主动 Inquiry 会锁死整机 USB 栈
PIVOT ×3
补栈 → 旁路 → 只做控制面
DELIVERED
可运行的 Web 适配器控制面板

00TL;DR

一句话:这台机器能认出蓝牙适配器、能发单条 HCI 命令、能让手机搜到自己,但任何持续的 USB 蓝牙数据流量都会把整机锁死;雪上加霜的是,我们自己写的开机自启脚本让原生 BlueZ 和旁路程序同时抢同一个 dongle,制造了「开机一两分钟必死」的假象。

这台 TP-LINK TPmini 大眼睛是一台 TV 盒子 + 路由器二合一设备,官方 ROM 把蓝牙功能整体裁掉了。我想给它外接一个 USB 蓝牙 dongle,最终目的是给这台机器上的怀旧游戏模拟器接一个蓝牙手柄。八天里我们依次走了三条技术路线,也依次撞上三堵墙。

阶段技术路线做了什么结果
① 10-27~10-31补齐 BlueZ 4.93 用户态从 CM10.1 ROM 提取 bluetoothd + 插件;自写 D-Bus launcher、nosetuid、spawn、开机自启;手写 C 版 D-Bus + Web 控制面板栈跑通 适配器和面板都出来了,但扫描 0 台
② 10-31~11-04usbfs 旁路 btusb绕过内核驱动,用 USBDEVFS_CONTROL 直接发 HCI 命令、从 0x81 读事件;把 dongle 接口从 btusb 摘走独占控制面可用 单条命令全成功;主动 Inquiry 一开就锁死整机
③ 11-04被动可发现 + 控制面板Write_Scan_Enable=3、事件泵自动 Accept/PIN/SSP、连接日志;目标收缩为适配器控制面板可用 手机能搜到本机,但链路建立超时,无法真正配对
最反直觉的一条结论

硬件、USB 核心、DWC OTG 控制器都是好的 —— 用 usbfs 发命令全部成功。坏的只有 btusb 的发送路径,以及这个老控制器对持续中断 IN 流量的承受能力。所以「换个 dongle 就好了」大概率无效:任何 dongle 都走同一份 btusb 代码、同一个 dwc 控制器。

关于本文的证据

文中所有终端输出、HCI 事件日志、命令回显、错误码都来自真机实录;截图分两类:实机截图(一期 BlueZ 面板,设备上 Android 浏览器实拍)与本地渲染图(最终版控制面板,用实测 /api/state 数据离线注入渲染,因为该面板最后随自启脚本一并停用、没来得及留实机截图)。每张图都标注了来源,没有伪造的「验证截图」。

01主角档案:硬件与软件情况

机器本体

项目实测值 / 现状
机型TP-LINK TPmini 大眼睛(TV 盒子 + 路由器二合一)。前面板 USB 2.0 ×2 + TF,背面 LAN / WAN / HDMI
SoC / 内存Rockchip RK3066,双核 Cortex-A9;MemTotal ≈ 847 MB
系统Android 4.2.2(API 17),rk30board,已 root(KingRoot)
内核Linux 3.0.36+;蓝牙子系统(bluetooth / bnep / btusb / hidp / rfcomm)全部内建,无需 .ko
USB 主控两个 DWC OTG 实例:usb20_host(bus 2)与 usb20_otg(bus 1)。两者表现不同,后文会用到
framework 蓝牙Bluedroid 体系,设置里没有蓝牙开关;属性 persist.sys.bluetooth.enable=0
系统里残留的蓝牙/system/bin 仍有 hciconfig / hcitool / hcidump / dbus-daemon;/system/lib 仍有 libbluetooth.so / libbluetoothd.so / libglib.so / libdbus.so;/system/etc/bluetooth/ 有 main.conf —— 说明原厂是 BlueZ 4.93
真正被删掉的bluetoothd 守护进程、/system/lib/bluez-plugin/*.so(全空)、init.rc 里的 dbus / bluetoothd 服务定义、framework 层蓝牙 UI
板载蓝牙?原厂设计有 Broadcom UART 蓝牙(brcm_patchram_plus + bcm432x/*.hcd + /dev/ttyS0 权限 bluetooth:system),但量产换料为纯 WiFi 的 RTL8189ES:hcd 文件全删、UART 在 9600/115200/921600/3000000 四档被动监听 + 主动 HCI Reset / BCM 唤醒全部零响应 → 判定未实装

外接的蓝牙适配器

USB ID
0a12:0001
CSR8510 A10,CSR 4.0 方案,3.0 内核 btusb 原生支持、无需 patchram 固件
BD_ADDR
00:1A:7D:DA:71:13
HCI 4.0 / LMP 4.0,厂商 0x000A(CSR),HCI rev 8891,LMP sub 0x22BB
接口 / 端点
if0 + if1
1-1:1.0 含 ep_81(中断 IN,HCI 事件)、ep_02(bulk OUT);:1.1 含 ep_82(SCO)
系统节点
/dev/bus/usb/001/002
OTG 口为 1-1(bus 1);host 口为 2-1(bus 2)

软件层次:为什么一开始就得自己造轮子

这台机器最别扭的地方在于 framework 与系统栈是两套互不相通的蓝牙体系:Android 的 Java BluetoothAdapter 走 Bluedroid,而机器上唯一能跑起来的是 BlueZ 4.93;同时系统没有 libdbus 的 Java 绑定、没有 bluetoothctl、没有可用的 APK 可以直接调用。所以想操作蓝牙,只有两条路:写 C 程序直接说 D-Bus,或者干脆绕过整个用户态协议栈。

为TP-Link TPmini大眼睛路由器添加蓝牙适配器……并让AI帮忙写篇Blog
图 1 · 三条路线分别作用在哪一层。注意 btusb 与 dwc_otg 是两个独立的故障点

工具链与环境约束

PC 侧用 Android NDK r30 的 armv7a-linux-androideabi21-clang.cmd 交叉编译,产物推到 /data/local/tmp/。给 API 17 编译有三个必须避开的坑:

坑原因正确做法
不能 -static新 bionic 的 arc4random 调用 MADV_WIPEONFORK,3.0.36 内核不支持 → 进程直接 abort动态链接
不能 select / fd_setAPI 21 头文件把 FD_SET 绑到 __FD_SET_chk,4.2 的 libc 没这个符号(链接能过、运行期才炸)用 poll()
不能 signal()老 libc 只导出 bsd_signal用 sigaction()

设备侧的工具集也被裁得很干净:没有 pkill killall pidof pgrep setsid nohup tail head tr wc strings file chattr awk,只有 ps / grep / kill / ls / cat / sh / sleep / getprop / mount / su 和一个原生 spawn。清进程只能遍历 /proc/<pid>/comm —— 这个限制后面坑了我们一次。

网络通道本身也不靠谱

adb 走 192.168.1.15:5555(出厂 service.adb.tcp.port=5555 已开),但 PC 侧 adb server 几乎每条命令之间都会掉,必须先 adb connect 热身;相比之下自写的 HTTP 服务 :8787 更稳。另外,Windows 的 ping 统计行在这一场景下完全不可信(详见 §11):目标二层不可达时本机自己回的 ICMP 错误会被计入「已接收」,显示「0% 丢失」、退出码还是 0。可靠判据是 ARP 邻居表 + TCP 端口 + curl /api/state。

02最初的目标

表层目标是「让官方砍掉蓝牙的 TPmini 能通过外接 USB 适配器扫描、配对、连接蓝牙设备」;真正的目的是给这台已经跑起 RetroArch 的怀旧游戏盒子接一个蓝牙游戏手柄。

所以验收标准其实很朴素:打开设置面板 → 点「开始扫描」→ 列表里出现手柄 → 点「配对」→ 连接成功 → 按键能进游戏。整个链条按层次拆开是这样:

目标链路 · 从 UI 到游戏输入FLOW
[ Web UI 点击扫描 ]
   → BlueZ / D-Bus StartDiscovery
   → HCI_Inquiry(持续广播 + 中断 IN 事件流)
   → 收到 Inquiry Result,列出周边设备
   → Create_Connection + 认证 / SSP 配对
   → ACL 链路 → L2CAP → SDP → HIDP
   → /dev/input/eventX → Android InputReader → 游戏

事后看,这条链路在三个不同的层次上各断了一次:用户态守护进程不存在(可补)、内核 btusb 发送路径有缺陷(可绕过)、USB 主机控制器扛不住持续流量(绕不过)。我们对前两层都找到了干净的解法,第三层是硬件/内核级的硬限制,也是整件事最终收口的原因。

另一个前置判断值得记下来:一开始先花时间确认了「板载蓝牙到底有没有」。原厂 init.rk30board.rc 里确实有 brcm_patchram_plus 服务和 /dev/ttyS0 的 bluetooth 权限,看起来像有 UART 蓝牙;但实测四种波特率零响应、hcd 固件全删、SDIO 上只有一块纯 WiFi 的 RTL8189ES,才判定本批次硬件未实装板载蓝牙芯片,USB dongle 是唯一通路。这个结论避免了在错误的方向上继续投入。

03路线一:把 BlueZ 4.93 用户态补齐

3.1 找齐缺的二进制

设备自带 libbluetoothd.so 是 4.93,所以必须找同版本的 bluetoothd 与插件,不能随便拿一个 BlueZ 5 的来用;同时 vanilla BlueZ 4.101 需要 epoll_create1 / signalfd / timerfd_create / ppoll,这台机器的 bionic 全都没有,交叉编译链接期就会失败。可行的来源是 CyanogenMod 10.1(Android 4.2.2)里已经为 bionic 打过补丁的 BlueZ 构建。

ROM 包蓝牙体系结论
N8000_CM10.1Bluedroid(bt_stack.conf / libbluedroid.so)无用 没有 bluetoothd / bluez-plugin / hciconfig
LG_P990_CM10.1BlueZ 4.93命中 提供 bluetoothd 4.93 + input / audio / network / health 插件 + 全套库与配置,ABI 完全匹配

把 bluetoothd 推到 /system/bin、插件推到 /system/lib/bluez-plugin/、配置推到 /system/etc/bluetooth/ 之后,进程能启动并正确解析 main.conf,但卡在 D-Bus 连接上。

3.2 三道 D-Bus / root 关卡

关卡 1
Android 版 dbus-daemon 不认 unix:path
dbus-daemon 1.4.0 编译时带 HAVE_ANDROID,系统总线只接受 init 通过 ANDROID_SOCKET_dbus 注入的监听 fd,任意自定义路径都报 Not an android socket。而 init.rc 里根本没有 dbus 服务。另外 /etc/machine-id 缺失,需要 remount /system rw 写入恰好 32 位 hex。
关卡 2
bluetoothd 会自动降权
libbluetoothd.so 导入了 setuid,启动后降到 bluetooth 用户;而 /dev/socket/dbus 是 750 root:root,降权后既写不了 socket 也开不了 BTPROTO_HCI / L2CAP。
关卡 3
后台进程把 adb shell 挂住
KingRoot 的 su 会泄漏 pty fd 给子进程,daemon 攥着 /dev/pts/N (deleted) 不放 → adb shell 永远等不到 EOF,表现为命令超时但脚本其实已跑完。

对应地写了三个小工具,都是 NDK 编译的 ARM 原生程序:

三个自写启动器 / 脱离器C · 概念
/* bt_launcher.c —— 冒充 init,给 dbus-daemon 造一个 android socket */
fd = socket(AF_UNIX, SOCK_STREAM, 0);
bind(fd, "/dev/socket/dbus"); listen(fd, ...);
fd = dup(fd);  /* 提到 ≥10,防止被 stdio 覆盖 */
setenv("ANDROID_SOCKET_dbus", fd_string, 1);
execve("dbus-daemon", ["--system", "--nofork", ...]);
        /* --nofork 必需:否则 daemonize 会关掉 fd≥3,把监听 fd 丢掉 */

/* bt_bluez.c —— 还要额外造 /dev/socket/bluetooth(SDP server 需要) */
setenv("ANDROID_SOCKET_bluetooth", fd2_string, 1);
setenv("LD_PRELOAD", "/data/local/tmp/nosetuid.so", 1);
        /* nosetuid.so 把 setuid/setgid/setresuid/setgroups 全变空操作,保持 root */

/* spawn.c —— 通用「脱离器」,让 adb 立即返回 */
fork(); setsid() /* su 已把子进程设为进程组长,直接 setsid 会 EPERM,需要再 fork 一次 */
pipe()  /* 父子同步:父进程必须等子进程确认「已 setsid」才 _exit,否则 su 会按进程组清理 */
for (i = 3; i < 1024; i++) close(i);  /* 关掉所有继承来的 pty fd */

跑通之后的证据非常干净:

bt.log / 探针输出 · 实机实录LOG
$ cat /data/local/tmp/bt.log
Starting SDP server
Got Unix socket fd '12' from environment
Adding device id record for 000a:0000
  plugins loaded: hciops input audio network health
Entering main loop
  init_known_adapters()

$ dbus_client request_name org.bluez
ret=2 (IN_QUEUE)   # org.bluez 已被 bluetoothd 占据,说明守护进程真的活着

$ btsock_probe
AF_BLUETOOTH / L2CAP  OK
AF_BLUETOOTH / HCI    OK
AF_BLUETOOTH / SCO    OK

3.3 btctl:一个 70KB 的自给自足 Web 面板

既然 Java 层调不到 BlueZ,也没有可用蓝牙命令行工具,干脆写一个 纯 C 守护进程:自己实现 D-Bus 报文层(约 500 行)、自己封装 BlueZ 4.93 的 Adapter / Device / Agent 调用、内置单线程 HTTP 服务监听 0.0.0.0:8787、把单页 HTML 直接编进二进制(ui.html → gen_ui.py → ui_data.h)。整个程序是一个静态 ARM 二进制,无运行时依赖,用设备自带浏览器打开 http://127.0.0.1:8787/ 就能点。

手写 D-Bus 报文的过程也留下几条反直觉的经验:SIGNATURE 字段的 variant 签名是 g 不是 s;字符串数组每元素前要 4 字节对齐;ListNames 前会插一条 NameAcquired 信号,必须按 reply_serial 循环匹配而不能读一条就当回复。另外 BlueZ 4.93 的方法是 ListDevices 而不是 GetDevices。

3.4 开机自启、桌面图标与持久化

为了让「插上 dongle 就能用」,我们把自启挂在 /init.rc:532 的 service flash_recovery /system/etc/install-recovery.sh 上(class main / oneshot / root,设备没有 SELinux 强制)。这个文件被 KingRoot 加了 IMMUTABLE + APPEND_ONLY 标志,普通 root 也写不动,于是又自写了一个 chattr_tool 用 FS_IOC_SETFLAGS 解标志 —— 32 位 ARM 上这个 ioctl 是 0x80046601,写成 64 位版的 0x80086601 会得到 ENOTTY。文件末尾追加了 TPMINI_BT_HOOK 块,调用 /data/local/tmp/bt_boot.sh。

顺带把桌面快捷方式和浏览器书签也做了。Android 4.2 的 Launcher2 有两个坑:Intent URI 不能用 data= 键、必须写 intent:<URL>#Intent;action=...;component=...;end;图标用 iconResource 永远是绿色机器人,必须把 PNG 二进制写进 favorites.icon BLOB 并把 iconType 设为 0。adb reboot 后进程自启、图标书签存活、/api/state 返回 present=1 powered=1,这一阶段看起来是成功的。

路线一的实际成果

dbus-daemon + bluetoothd 4.93 全链路跑通,SDP server 正常,HCI / L2CAP / SCO socket 可用,CSR8510 被内核正确枚举成 hci0,Web 面板能显示适配器名称与 MAC,重启可持久化。唯一的异常是:「附近设备」永远是 0 台。

04第一次转向:扫描永远 0 台,根因不在 BlueZ

点「开始扫描」后 D-Bus 返回 ok、面板显示「正在扫描附近设备…」、状态位 discovering=1,但设备列表始终为空。这是整个项目里最有价值的一次「假成功」。

4.1 证据链:命令发出去了,事件一条也没回来

计数器 / dmesg / urbnum · 实机实录LOG
# ① 命令与事件计数不平衡,且差值恒定
$ hcidbg info
cmd_tx = 52    evt_rx = 41        ← 冷启动后
cmd_tx = 46    evt_rx = 41        ← 我发 5 条命令后
cmd_tx = 41    evt_rx = 41        ← HCIDEVUP 刚完成时(41 条全成功)
evt_rx 永远停在 41。发多少条命令,evt_rx 一动不动。

# ② 内核日志直接指出失败点
$ dmesg | grep Bluetooth
Bluetooth: hci0 urb dd45ba00 submission failed
Bluetooth: hci0 command tx timeout
Bluetooth: hci0 urb dd45b980 submission failed
Bluetooth: hci0 command tx timeout
(每秒一对,形成死循环;冷启动后 63 秒=蓝牙栈拉起时刻开始,稳定复现)

# ③ URB 根本没提交到主机控制器
复位前 urbnum = 141
发一条命令后 urbnum = 141      ← 完全没变
urbnum++ 在 usb_hcd_submit_urb() 内部、设备状态与端点检查之后才发生;
它不涨 ⇒ usb core 的前置检查阶段就 early return 了。

而对照面非常干净:HCIDEVUP 触发的整串初始化命令(内核约 28 条 + BlueZ device_devup_setup 约 13 条 = 41 条)零失败,配置里的 Name = TPmini 也确实写进了控制器。USB 设备从开机 2 秒枚举后从未断开,rfkill 全 1 未屏蔽,inquiry_cache / uuids / blacklist 全空。

4.2 逐个排除,直到只剩一个解释

假设验证方式结论
内存不足 ENOMEMMemFree 约 250 MB排除
USB autosuspend 挂起「先枚举、后设 power/control=on + delay=-1」重测,t=188ms 仍失败(此前测废过一次:先设 control 再重新枚举会把值重置回 auto)排除
LE / BLE 命令相关enable_le 本来就是 N排除
btusb 参数 reset=N无效,已恢复 Y排除
btusb 参数 ignore_csr=Y设 Y 后 bind 直接失败(驱动不认设备),已恢复 N排除
bind 顺序不同(bluetoothd 是 socket→bind→HCIDEVUP→write)新增 win <dev> <maxms> <step> 1 复现该顺序,t=187ms 同样失败排除
端口 / host 控制器实例把 dongle 从 2-1(usb20_host)换到 1-1(usb20_otg):重新枚举 279→353(+74)、HCIDEVUP 353→417(+64)、发 1 条命令 417→417(+0),症状完全一致排除
被我反复 unbind/bind 折腾坏了adb reboot 冷启动后同样复现排除

为了把「初始化窗口」这个假说测死,写了 hcidbg(绕过 BlueZ 直接和控制器对话,子命令 info / up / down / cycle / name / scan / cod / eir / inq / fast / win)。win 只做一次 HCIDEVUP,然后按时间步长连发 Read_Local_Version,打印每次的 t 与 status。结果是窗口长度为 0:只有内核在 HCIDEVUP 内部发的命令能过,任何用户态 HCI 命令一律被拒。

阶段性根因

Linux 3.0.36 的 btusb 发送路径 + DWC OTG 主控,对 CSR8510 存在兼容缺陷:初始化一结束,任何新的 HCI 命令在 USB 层就提交不出去。 BlueZ 的 StartDiscovery 只是 D-Bus 层返回 ok,底层 HCI_Inquiry 被内核静默丢弃;面板的「正在扫描」纯属假象。

顺带修了一个真 bug:/etc/bluetooth/main.conf 里写的是 Name = %m,而 %m 是 BlueZ 5.x 才支持的模板变量,4.93 不认识、展开成空串。改成字面量 Name = TPmini 后日志出现 hciops_set_name() hci0, name TPmini。也顺带用对照实验确认了 libbluetoothd.so 里虽然能看到 Discoverable / Pairable 字符串,但 4.93 的 parse_config 根本不读这两个键 —— 没法靠配置文件让设备开机即可被发现。

一个差点把排查方向带偏的认知修正

一开始根据日志认为「BlueZ 的 set_name / set_class 成功了、只有后续命令失败」。后来才发现 hciops_set_name() 只是尝试前的日志,不代表收到 Command Complete。真实规律严格得多:只有内核在 HCIDEVUP 内部发的命令能过,窗口长度 = 0。日志里出现函数名 ≠ 命令成功,这个教训后来又救了我们一次。

推论也改变了硬件策略:换 dongle 的胜率很低,因为任何 dongle 都走同一份 btusb 代码。于是路线二转向:干脆不用这个驱动。

05路线二:用 usbfs 绕过 btusb

既然内核驱动发不出命令,那就从用户态直接对 USB 设备说话:打开 /dev/bus/usb/<bus>/<dev>,用 USBDEVFS_CONTROL 发 HCI 命令,用 USBDEVFS_BULK 从中断端点 0x81 读 HCI 事件。

5.1 HCI over USB 的协议要点

方向格式 / 调用易错点
命令 ↓控制传输:bmRequestType=0x20、bRequest=0、wValue=wIndex=0,数据 [op_lo, op_hi, plen, params...]opcode 小端在前
事件 ↑中断端点 0x81,[evt_code, plen, params...]没有 H4 的 0x04 前导字节;USBDEVFS_BULK 对中断端点同样有效
Command Complete[0x0e, plen, num_pkts, op_lo, op_hi, status, ...]别把 num_pkts 当 opcode
Command Status[0x0f, plen, status, num_pkts, op_lo, op_hi]status 在第 3 字节,我们最初解析错位(见 §7)
接口占用ioctl(fd, USBDEVFS_CLAIMINTERFACE, &iface)参数是 int* 不是 int,传值会 EFAULT
usbscan.c · 底层 HCI 传输(真实源码摘录)C
static int uf_hci_cmd(unsigned int op, const unsigned char *param, int plen)
{
    struct usbdevfs_ctrltransfer ct;
    unsigned char buf[300];
    buf[0] = op & 0xff;
    buf[1] = (op >> 8) & 0xff;
    buf[2] = (unsigned char)plen;
    if (plen > 0 && param) memcpy(buf + 3, param, plen);
    memset(&ct, 0, sizeof(ct));
    ct.bRequestType = 0x20;
    ct.bRequest     = 0x00;
    ct.wLength      = 3 + plen;
    ct.timeout      = 2000;
    ct.data         = buf;
    return ioctl(g_ufd, USBDEVFS_CONTROL, &ct);
}

static int uf_read_evt(int timeout_ms, unsigned char *buf, int cap)
{
    struct usbdevfs_bulktransfer bt;
    memset(&bt, 0, sizeof(bt));
    bt.ep      = 0x81;          /* 中断 IN,bulk ioctl 同样适用 */
    bt.len     = cap;
    bt.timeout = timeout_ms;
    bt.data    = buf;
    return ioctl(g_ufd, USBDEVFS_BULK, &bt);
}

用独立的诊断工具 usbhci(子命令 ver / loop / scan / name / inq / raw)验证,结果非常振奋:

usbhci 旁路探针 · 实机实录LOG
$ usbhci ver
Read_Local_Version → HCI 4.0 / 厂商 0x000A(CSR) / LMP 6

$ usbhci loop 20
连发 20 条命令,18 条成功   # 通道稳定,不是偶发

$ usbhci raw 0c13 54 50 6d 69 6e 69 ...
Write_Local_Name("TPmini") → status=0

$ usbhci raw 0c1a 03
Write_Scan_Enable=3 → status=0   # 设备进入可发现 + 可连接
这一步推翻了「硬件坏了」的全部猜想

硬件正常、USB core 正常、DWC OTG 正常,唯一坏的是 btusb 的发送路径。而且 Write_Scan_Enable=3 能成功,意味着「让手机搜到这台机器」这件事已经做到了 —— 只是当时还没意识到它有多重要。

5.2 怎么把 dongle 从 btusb 手里「摘」干净

usbfs 要 CLAIMINTERFACE 成功,前提是没有别的驱动占着这个接口。这段路踩了一串内核级的坑:

尝试结果教训
rmmod btusb驱动是内核内建的,卸不掉—
USBDEVFS_DISCONNECT / CONNECT写了 disctest.c 穷举验证,3.0.36 上全部返回 ENOTTY老内核没实现这两个 ioctl
整设备级 unbind(写 .../usb/unbind)会把整条总线摘离,dwc 驱动直接锁死:连插着的 USB 鼠标都不动、整机不可达严禁 这是用户最先观察到的「机器被搞死」现象
接口级 unbind + 每次扫描完 rebind重新 probe 时 USB 栈挂死严禁 不 rebind
host 口(bus 2)接口级 unbind留下 stuck claim,CLAIMINTERFACE 返回 EBUSY端口相关
OTG 口(bus 1)接口级 unbind成功 只摘含 ep_81 的 HCI 接口 1-1:1.0,intf->claimed 被正确释放,claim 成功最终方案
为TP-Link TPmini大眼睛路由器添加蓝牙适配器……并让AI帮忙写篇Blog
图 2 · 旁路后的数据路径。关键约束是「接口级 unbind 只做一次、永不 rebind」
usbscan.c · usbscan_init() 核心(真实源码摘录)C
/* 找 CSR dongle:sysfs 的 idVendor 是十六进制字符串 "0a12",
   atoi() 会解析成 0,strtol base 0 遇到 "0a12" 按八进制也只取前导 0,
   必须显式 base 16 —— 这个 bug 让第一版 usbscan 死活找不到设备。 */
if (n > 0) v = (int)strtol(buf, NULL, 16);
...
snprintf(g_unode, sizeof(g_unode), "/dev/bus/usb/%03d/%03d", bus, dev);

/* 只摘含 ep_81 的 HCI 接口,SCO 接口保持不动 */
if (access(ep81, F_OK) == 0) {
    snprintf(ub, sizeof(ub), "/sys/bus/usb/drivers/btusb/unbind");
    sysfs_write(ub, se->d_name);
}
usleep(300000);
g_ufd = open(g_unode, O_RDWR);
int iface = 0;
ioctl(g_ufd, USBDEVFS_CLAIMINTERFACE, &iface);  /* 注意是 int* */

工程侧还修了两个不起眼但致命的问题:设备上没有 nohup / setsid,( & ) 起的子 shell 会在 su 会话结束时被杀,改用原生 spawn 常驻;PC 侧在 MSYS_NO_PATHCONV=1 下,adb push 的本地路径必须写 D:/... 而不是 /d/...,否则 adb.exe 收到字面路径直接 cannot stat。

06第二次转向:主动 Inquiry 会把整机锁死

控制面全部打通后,自然要把 HCI_Inquiry 集成进面板「开始扫描」。结果是这八天里最硬的一堵墙,而且复现了两次、与实现细节无关。

6.1 先确认「无害命令」真的无害

在冒险触发扫描前,先用 usbhci 单独发一轮安全探针(杀掉占用 claim 的 btctl → 干净 claim → 只发单条命令):

probe.sh · 安全 HCI 探针 · 实机实录LOG
$ usbhci raw 1009 00                       # Read_BD_ADDR
status=0   BD_ADDR = 00:1A:7D:DA:71:13
$ usbhci ver                                # Read_Local_Version
成功
$ usbhci raw 0c13 ...                      # Write_Local_Name
成功
$ (仅 Read_Local_Name 因管道残留 stale 事件被误判超时,非 dongle 问题)
全程 PING / HTTP / adb 稳定,设备没有楔死

这同时纠正了另一个误判:之前 /api/state 里 addr 偶尔为空,并不是 dongle 不响应,而是 btctl 在杀掉 bluetoothd 后启动太快、dongle 尚未 settle 的时序问题;干净重启后地址稳定返回。

6.2 一触发 Inquiry 就整机失联

第一版 usbscan_do() 是 HCI_Reset → drain → HCI_Inquiry(0x0401) → 解析 Inquiry Result(0x02)。怀疑是 Reset 让 CSR 重载固件、在破裂的 USB 路径上产生卡死 URB,于是又编了一版去掉 HCI_Reset 的保守实现(78884 B)。两版结果完全一样:

POST /api/discover 之后 · 实机实录LOG
$ curl -X POST http://192.168.1.15:8787/api/discover -d '{"on":1}'
(35 秒内无任何响应,curl rc=28 超时)

$ ping 192.168.1.15
丢包 66% ~ 100%

$ curl http://192.168.1.15:8787/api/state
超时
$ adb devices
List of devices attached   (空)

→ 唯一的恢复方式是硬断电重启(软 reboot 命令已经发不出去了)
为TP-Link TPmini大眼睛路由器添加蓝牙适配器……并让AI帮忙写篇Blog
图 3 · 真正决定可行性的不是「命令能不能发」,而是「USB 上有没有持续流量」
决定性结论

去掉 HCI_Reset 仍楔死,说明问题不在实现细节,而在持续 Inquiry 无线电流量产生的中断 IN 事件流把 3.0.36 的 DWC 主机控制器锁死,连带拖垮整个 USB 子系统与用户态网络服务。主动扫描在这台硬件上不可行,是硬限制。usbfs 旁路的价值被锁死在「认得 dongle、发得动单条命令」。

到这里必须做方向决策。可选路径有三条:回到内核原生 btusb 路线、让 TPmini 做被动可发现目标、或者换硬件。我们选了第二条:既然 dongle 固件可以自己应答 Inquiry / Page,那就不需要主机持续收包,让别的设备来搜我们。

07路线三:被动可发现 + 自动接受连接

7.1 让 dongle 固件替我们干活

Write_Scan_Enable(3) 是一条单命令:设好后由 CSR 固件自己做 Inquiry Scan 和 Page Scan,主机不需要持续读 0x81。因此它落在「控制面」这一侧,实测完全安全:

/api/state · hciEvents 实录JSON / LOG
$ curl http://192.168.1.15:8787/api/state
{
  "adapter": { "present": 1, "powered": 1,
               "addr": "00:1A:7D:DA:71:13", "discoverable": 1 },
  "hciEvents": "#1 Write_Scan_Enable=3\n#2 cmd complete op=0x0c1a st=0x00 成功"
}
$ PING 0% 丢包、HTTP 200 全程稳定 —— 未楔死

为了让「被动」也能有点交互能力,主循环从 500ms 改成 100ms,每轮做一次非阻塞事件泵,自动应答入站连接与配对事件。下面是修正后的真实源码片段:

usbscan.c · handle_event() 摘录(真实源码)C
case 0x04:  /* Connection Request: [04,len,bdaddr(6),cod(3),linktype(1)] */
    memcpy(q, &ev[2], 6);
    q[6] = 0x01;              /* Role=Slave:让发起方(手机/手柄)当 master */
    uf_hci_cmd(0x0409, q, 7); /* Accept_Connection_Request,参数必须 7 字节 */
    g_conn_attempts++;
    evlog("CONN REQ %s (link=0x%02x) -> accept(role=slave)", bd, ltype);
    break;

case 0x16:  /* PIN Code Request:BD_ADDR(6)+PIN_Len(1)+PIN(16) = 23 字节 */
    uf_hci_cmd(0x040D, q, 23);
    evlog("PIN REQ %s -> reply 0000", bd);
    break;

case 0x31:  /* IO Capability Request(SSP 第一步) -> NoInputNoOutput */
    uf_hci_cmd(0x042B, q, 9);
    break;

case 0x0f:  /* Command Status: [0f,len,status,num,op_lo,op_hi] */
    evlog("cmd status op=0x%02x%02x st=0x%02x %s",
           ev[5], ev[4], ev[2], hci_err_text(ev[2]));
    break;

7.2 手机实测:搜到了,但连不上

把手机蓝牙打开,确实搜到一台 CSR8510 A10,尝试配对。TPmini 侧的 hciEvents 把整个过程完整记了下来:

hciEvents · 手机 34:15:00:5D:2B:A4 的连接尝试LOG
#3 CONN REQ 34:15:00:5D:2B:A4 (link=0x01) -> accept(role=slave)
#4 cmd status op=0x0409 st=0x00 成功
#5 CONN COMPLETE st=0x10 Connection Accept Timeout(对端未响应/链路建立超时)
(同样的 accept → st=0x10 连续 5 次)

结论清晰:入站连接请求确实到达、btctl 每次都自动 Accept 了,但 LMP / ACL 链路建立超时。根因是 ROM 阉割了 BlueZ 服务,而 usbfs 只有 HCI 控制面,没有 L2CAP / SDP / HIDP 主机栈去接住握手。好消息是这次没有楔死 USB 栈 —— 失败的链路不产生持续流量,反过来印证了图 3 的分界线。

7.3 顺手挖出的一批「一直在骗我们」的 bug

分析日志时发现的这些问题,本身就很值得单独记一笔:

问题原值 / 现象修正
Command Status 解析错位把 num_pkts 当 opcode 低字节、把 opcode 高字节 0x04 当 status,日志出现误导性的 op=0x0901 st=0x04按 [0f,len,status,num,op_lo,op_hi] 重解析,并把常见 HCI 错误码转成中文
PIN_Code_Request_Reply0x0C0D,参数只发 11 字节0x040D,参数 23 字节(BD_ADDR6 + PIN_len1 + PIN16)
Link_Key_Request_Negative_Reply0x0C14(其实是 Read_Local_Name)0x040C
User_Confirmation_Request_Reply0x0C250x042C
User_Passkey_Request_Reply0x0C2C0x042E
缺失的 SSP 环节IO Capability Request 无人应答补 0x042B(NoInputNoOutput);Accept 补 7 字节含 Role

四个配对应答的 OGF 段全被写成了 0x0Cxx(Host Controller & Baseband),实际应属于 Link Control 0x04xx —— 也就是说,之前连配对应答都发成了无关命令。对照 Linux include/net/bluetooth/hci.h 逐个改正后,HCI 层的握手才谈得上规范。但主机协议栈的缺口依然在,这是「代码正确也无法跨越」的那一层。

必须说清的硬边界

就算连接成功且没楔死,连上 ≠ 能玩。手柄的按键走 HID 中断通道(约每 10 ms 一个输入报告),既属于会锁死 dwc 的「持续流量」,也需要我们完全没有的 L2CAP / SDP / HIDP 主机栈去解析。要让手柄真正进游戏,只能让内核原生 btusb + BlueZ 正常接管(但 §4 已证明这条路本身有缺陷),或者在 usbfs 上重写整套主机协议栈 —— 工程量与不确定性都不可接受。

08第三次转向:把目标收缩成「适配器控制面板」

在确认「ROM 的蓝牙被整体阉割」「主动扫描不可能」「连接建不起来」之后,目标被主动收缩:不再追求发现别的设备,而是让 CSR8510 在 Web 面板里提供尽可能多的可控功能 —— 展示 MAC、开关蓝牙、设为可被发现(接受可能连不上)、改名,以及把连接尝试变成可见日志。

这个收缩不是放弃,而是把「能做到且不会损坏设备」的部分做扎实,同时把会楔死硬件的入口彻底封死,避免以后误触。

8.1 功能与 API

能力实现方式状态
展示 MAC / 名称Read_BD_ADDR 0x1009 / Read_Local_Name 0x0C14,init 时读一次并缓存已验证
展示固件信息Read_Local_Version 0x1001 → HCI 4.0 / LMP 4.0 / CSR (0x000A) / rev 8891 / sub 0x22BB已验证
改名POST /api/name → Write_Local_Name 0x0C13,手机搜索列表里的名字随之变化已实现
蓝牙开关POST /api/power → 软件电源:OFF 时关广播 + 停事件泵,ON 时恢复;只发单条 HCI 命令,不释放 claim、不 rebind,安全可逆已实现
可被发现开关POST /api/discoverable → Write_Scan_Enable 3 / 0;面板明确标注「能被搜到、也可能连不上」已验证
连接日志HCI 事件环形缓冲(32 条、带序号)+ 累计连接请求数 + 最近发起连接的 MAC;面板用颜色区分命令 / 请求 / 成功 / 失败已验证
主动扫描/api/discover 直接返回拒绝说明,绝不落到 usbscan_do();UI 移除扫描按钮主动禁用
启用蓝牙/api/enable 改走 act_power(1),不再执行会复位 USB 的 bt_restart.sh安全化
配对 / 连接 / 代理依赖 BlueZ 的旧端点保留但不再出现在 UI(当前 ROM 无 BlueZ 服务)不可用
btctl.c · 把危险入口封死(真实源码)C
if (strcmp(path, "/api/enable") == 0) {
    /* 不再跑 bt_restart.sh:它复位 USB 层,在 3.0.36/dwc 上有楔死风险 */
    rc = act_power(1, err, sizeof(err));
} else if (strcmp(path, "/api/power") == 0) {
    rc = act_power(json_get_int(body, "on", 1), err, sizeof(err));
} else if (strcmp(path, "/api/name") == 0) {
    ...
} else if (strcmp(path, "/api/discover") == 0) {
    /* 硬限制:实测 HCI_Inquiry 的持续中断 IN 流量会锁死 dwc 主机控制器,
       整机只能硬断电重启。这里直接拒绝,绝不落到 usbscan_do()。 */
    snprintf(err, sizeof(err),
             "主动扫描已禁用:该硬件的 HCI_Inquiry 会锁死 USB 子系统(需硬断电重启)。请改用手机/手柄主动搜索本机。");
    rc = -1;
}

8.2 最终版面板长什么样

为TP-Link TPmini大眼睛路由器添加蓝牙适配器……并让AI帮忙写篇Blog
本地渲染图(非实机截图):这是最终版 ui.html 由无头浏览器渲染的结果,注入的是真机实测的 /api/state 数据(BD_ADDR、HCI 版本、手机的 5 次连接事件)。之所以没有实机截图,是因为该面板在最后一次处置中随开机自启脚本一并停用了,见 §9。图中可见:大号 MAC、固件信息、改名、电源/可发现双开关、连接统计、彩色 HCI 事件流,以及底部「可用 / 受限 / 已禁用」三栏能力边界。

8.3 构建与验证链

构建 / 部署 / 验证 · 命令实录SHELL
# 面板源码 → 内嵌 C 数组 → 单文件 ARM 二进制
$ cd D:/tpmini_bt/release/btctl-kit
$ python src/gen_ui.py                     # ui.html → ui_data.h
$ armv7a-linux-androideabi21-clang.cmd -O2 -o bin/btctl_new src/btctl.c
rc=0   bin/btctl_new = 86308 B

# 离线校验(adb 很脆,白屏排查成本高,先在本地把 UI 验干净)
$ python -c "restore ui_data.h and compare with ui.html"
chunks=19  restored=18924  orig=18924  identical=True
$ node --check _btui_check.js
JS syntax OK

# 部署(本地路径必须 D:/ 风格;设备端以 root 执行)
$ adb push D:/.../bin/btctl_new /data/local/tmp/btctl_new
$ adb shell "su -c 'sh /data/local/tmp/deploy_btctl.sh'"
spawn btctl → PID 3433

$ curl http://192.168.1.15:8787/api/state
mode="usbfs 控制面旁路"  scanDisabled=1  connAttempts=0
hciInfo="HCI 4.0 / LMP 4.0 / CSR (0x000A) / HCI rev 8891 / LMP sub 0x22BB"
hciEvents="#1 Write_Scan_Enable=3 / #2 cmd complete op=0x0c1a st=0x00"
根页面 18924 B,Content-Length 正确,PING 0% 丢包,设备未楔死

构建期还修了一个会导致整页白屏的低级 bug:build_state() 里 "hciEvents" 字段前少了一个逗号,/api/state 返回的是无效 JSON,前端 JSON.parse 直接抛异常。修完后用解析器复验了整个状态对象,并静态核对了前后端所有字段(st.adapter / capabilityNote / connAttempts / hciEvents / hciInfo / lastConn / log / mode)与所有 POST 端点一一对应。

部署脚本的「自杀」bug

deploy_btctl.sh 里原本写着 ps | grep btctl | while read ... kill -9。问题是脚本自身、su 包装进程、adb shell 会话的命令行里全含 "btctl",于是脚本把自己也杀了:旧 btctl 被清掉、新进程从未启动、面板 8787 失联、adb 会话异常断开。改成遍历 /proc/<pid>/comm 精确匹配进程名,并给 spawn 加三路重定向避免子进程继承 adb shell 的 stdout,才彻底解决。设备上没有 awk / wc / head,校验一律只能用 ls -l + grep。

09最后一个坑:开机一两分钟必死

连续几次「部署后短暂正常、随即整机失联」之后,现场观察给出了决定性线索:硬重启后机器能短暂恢复,但大约一两分钟就死机断链。这个时间规律不像 USB 楔死(那种是操作当场触发),更像是某个开机后才启动的东西在慢慢作祟。

9.1 抓住上线窗口

因为窗口很短,逐轮对话必然错过,于是写了一个后台高频监测脚本(emergency_watch.sh)+ 设备端一键处置脚本(emergency_device.sh):一旦探测到 5555 通就立刻 adb 接入,停掉事件泵、停掉自启脚本、抓取 ps / 日志 / 脚本源码。

22:43:14
后台监测启动
每轮检查 TCP 5555 / HTTP 8787,窗口 9 分钟。
22:44:49
捕获上线:5555 通、8787 未起
adbd 先起、面板还没起 —— 这正好说明 当时没有 btctl 进程,事件泵不是这次死机的直接原因。
22:44:57
adb 接通,第一轮处置执行
HTTP 关事件泵因 8787 未监听而失败,但设备端脚本成功执行。
22:45:10
bt_boot.log 打出 [bt_boot] btctl started
此时文件已被禁用,实际并没有起来 —— 但这条日志把嫌疑直接指向了开机自启链。
22:47:05
第二轮:读取全部自启脚本 + 禁用 + 取证
拿到 install-recovery.sh 的 hook、bt_boot.sh / bt_up.sh / bt_restart.sh 源码、BlueZ 初始化日志、进程清单。
之后 9 轮 × 10s
稳定性确认
adb5555=OPEN 全程无波动,明显越过了用户描述的 1~2 分钟故障窗口;最终 ps 只剩 dbus-daemon 与 bluetoothd 两个当次残留进程,无 btctl / bt_boot / bt_up / spawn。

9.2 根因:两条蓝牙路径同时抢同一个 dongle

真正的开机自启入口是 /system/etc/install-recovery.sh 里 10-28 加的那段 hook:

install-recovery.sh / bt_boot.sh · 取证日志中的真实源码SH
# --- /system/etc/install-recovery.sh ---
# --- TPMINI_BT_HOOK: bring up BlueZ (dbus + bluetoothd) after boot ---
if [ -x /data/local/tmp/bt_boot.sh ]; then
    /system/bin/sh /data/local/tmp/bt_boot.sh >/dev/null 2>&1 &
fi

# --- /data/local/tmp/bt_boot.sh(关键片段) ---
while [ $i -lt 90 ]; do                       # 等 sys.boot_completed=1
    b=$(getprop sys.boot_completed 2>/dev/null)
    [ "$b" = "1" ] && break
    sleep 2; i=$((i + 1))
done
sleep 3
if [ -f /data/local/tmp/bt_up.sh ]; then
    sh /data/local/tmp/bt_up.sh                  # ← 路径 A:拉起原生 BlueZ,btusb 开始收发 HCI
fi
...
if [ -x /data/local/tmp/btctl ]; then
    sleep 6
    /data/local/tmp/spawn /data/local/tmp/btctl_run.sh /data/local/tmp/btctl.log
                                                   # ← 路径 B:起 usbfs 旁路 btctl,unbind btusb + claim
    echo "[bt_boot] btctl started"
fi
为TP-Link TPmini大眼睛路由器添加蓝牙适配器……并让AI帮忙写篇Blog
图 4 · 延迟 = boot_completed + 3s + BlueZ 初始化 + 6s + 流量累积,正好解释「开机 1~2 分钟才死」

取证日志里 Adapter /org/bluez/2665/hci0 has been enabled 与 [bt_boot] btctl started 相隔仅几秒,完全吻合。这也解释了此前多次「部署后短暂正常随即失联」:不是部署动作本身每次都致命,而是我们总在开机自启的 BlueZ 还在跑的时候,又拉起一个抢 dongle 的旁路程序。

9.3 处置:只改 /data,全部可逆

  • 共 19 个文件 chmod 644 + 改名 .disabled:bt_boot.sh、bt_up.sh、bt_restart.sh、bt_down.sh、bt_status.sh、btctl* / deploy_btctl.sh / probe.sh / scan_once.sh、btctl_start/stop/demo、bringup*、go_bt* 等。
  • hook 里的 [ -x bt_boot.sh ] 现在为假 → 下次开机不再拉起整套蓝牙栈。hook 文本仍在(条件为假,无害),没有去 remount /system 改它,风险更低。
  • 保留 adb_tcp.sh(5555 通道)与 tune.sh(LMK / noop 调优)的 hook,设备的基础可用性不受影响。
  • 顺带把一个定时炸弹一起禁了:bt_restart.sh 会做 btusb/unbind + bind 接口重绑定,正是早先实测会楔死 USB 的操作。
结果

处置后连续 9 轮 × 10s 观测,adb5555=OPEN 无波动、panel8787=off,设备稳定越过了故障窗口。代价是面板 8787 随脚本一起停用了 —— 这是「停脚本」的必然结果,也是当前最安全的状态。

需要诚实补一句:这次开机时 ps 里没有 btctl 进程,所以「开机必死」并不能归咎于新版 usbscan_event_pump 单独造成(此前对它的怀疑不成立)。但事件泵每 100ms 读一次 0x81 确实属于持续流量,作为长期风险留在了清单里。

10最终做到了什么程度

✓ 已实现并实测
适配器识别与信息展示
usbfs 独占接管 dongle,稳定读出 BD_ADDR 00:1A:7D:DA:71:13、HCI/LMP 版本、CSR 厂商与固件修订号。
✓ 已实现并实测
设为可被发现 / 可连接
Write_Scan_Enable=3 返回 st=0x00,手机蓝牙能搜到 CSR8510 A10,且设备不楔死。
✓ 已实现并实测
连接尝试可见
完整记录手机的 5 次 CONN REQ → accept → CONN COMPLETE st=0x10,带 HCI 错误码中文解释。
△ 代码完成、验证中断
开关 / 改名 / 新版面板
新版 btctl(86308 B)曾成功部署并返回全部新字段、根页面 18924 B 正常;随后因自启冲突失联,逐项 API 回归没跑完。
✕ 硬件级不可行
主动扫描周边设备
两次实测 HCI_Inquiry 均锁死整机 USB 栈,只能硬断电恢复;已在 API 与 UI 层永久禁用。
✕ 协议栈缺口
配对 / 连接 / 手柄输入
LMP/ACL 链路建立超时;缺 L2CAP / SDP / HIDP 主机栈,ROM 也没有可用的 BlueZ 服务。连上 ≠ 能玩。

能力边界(这是交付的一部分)

可用 · 实测稳定受限 · 需接受失败禁用 · 会损坏可用性
读 BD_ADDR / 控制器版本
读写本机名称
软件电源开 / 关
可发现 / 可连接开关
被动接受入站连接请求
PIN / SSP 自动应答
连接尝试与 HCI 事件日志
手机能搜到本机、也能发起连接
本机自动 Accept,但链路多结束于 0x10 超时
缺 L2CAP / SDP / HIDP,连上后无法解析按键
BlueZ 服务不可用(ServiceUnknown)
主动扫描附近设备(永久禁用)
btusb 接口 rebind
整设备级 unbind
原生 BlueZ 与 usbfs 旁路并存
任何持续 USB 蓝牙数据流量的尝试
当前设备状态(收尾)

所有蓝牙相关开机自启脚本均已禁用并改名 .disabled;adb 5555 稳定可用,面板 8787 处于停用状态;装置上只剩当次开机的 dbus-daemon / bluetoothd 残留进程,不再与旁路程序竞争,下次重启也不会被拉起。整个收尾只改了 /data/local/tmp,没有动 /system,全部可逆。

如果要把面板再跑起来,正确做法不是把旧自启脚本放回去,而是新建一个最小自启:只 spawn btctl_run.sh、完全不跑 bt_up.sh,让 usbfs 独占 dongle;同时把 100ms 常驻事件泵改成按需 / 低频;先手工启动观察 5~10 分钟确认不锁死,再考虑加自启。

11复盘:踩过的坑与工程教训

11.1 诊断方法上的坑

坑现象修正
Windows ping 统计行不可信目标二层不可达时,本机自己回「来自 192.168.1.10 的回复: 无法访问目标主机」,但统计行写「已接收=3 丢失=0 (0% 丢失)」、退出码 0 —— 我们据此误判过「设备 PING 通、只是用户态服务没起来」,白等了约 160 秒用 Get-NetNeighbor(Unreachable + 全零 MAC = 离线)、ARP 表、TCP 5555/8787 连通性、curl /api/state 作为判据
日志里出现函数名 ≠ 成功StartDiscovery ok 只是 D-Bus 层;hciops_set_name() 只是尝试前的日志看 Command Complete 的 status、看事件计数器、看内核日志
计数器差值恒定比单条报错更有信息量cmd_tx 一直涨、evt_rx 永远 41先量化「发了多少 / 回了多少」,再定位哪一层吞掉
排除法必须真排除autosuspend 那次「先设 control 再重新枚举」会把值重置回 auto,等于没测改成「先枚举、后设 control」重测才成立
对照实验是必要的换 USB 口、冷启动、bind 顺序、驱动参数、配置键,逐个对照才把根因收敛到 btusb一次只改一个变量

11.2 高危操作清单(这台机器专属)

以下操作在这台 3.0.36 + dwc_otg 上会导致整机锁死,需硬断电

① 整设备级 unbind(写 .../usb/unbind);② 接口 rebind 触发 btusb probe;③ HCI_Inquiry 及任何持续中断 IN 流量;④ 原生 BlueZ 与 usbfs 旁路同时运行;⑤ 连续两次 kill/spawn 部署造成竞态。
相对安全的做法:只做一次接口级 unbind(含 ep_81 的 :1.0)、永不 rebind、进程退出只 close(fd)。

11.3 工程环境与流程

类别经验
adbPC 侧 server 极不稳,采用温和重连(只 connect 不 kill-server);长操作写成设备端自包含脚本一次执行,避免中途掉线留下半残状态;MSYS_NO_PATHCONV=1 时本地路径用 D:/...
root / shellsu -c "cmd1; cmd2" 只有第一条是 root(后续降为 shell uid 2000);su -c <裸二进制> 会失败,必须 su -c 'sh /path/x.sh' 包一层;mksh heredoc 需要 $TMPDIR;写内容一律用 echo >>
进程管理没有 pkill / pidof / awk / wc / head;清进程遍历 /proc/<pid>/comm;注意 grep btctl 会匹配到脚本与 adb 会话自身
文件系统mount -o remount,rw /system 返回 0 不代表生效,必须 mount | grep 复核;root 建的目录是 750,adb push(shell uid 2000)写不进去;adb push dir/. /target/ 会多套一层目录
可逆性自启改动尽量只放 /data/local/tmp;停用用 chmod 644 + 改名 .disabled 而不是删除;改 /system 前先备份
验证顺序编译 → 本地校验生成物(HTML 还原一致性、JS 语法、JSON 解析)→ 部署 → HTTP 逐项验证 + 全程盯连通性;设备侧白屏/失联的排查成本远高于本地多花两分钟

11.4 最值得记住的三条

01
先分层,再动手
同一个「蓝牙不能用」,可能是 framework 被裁、守护进程缺失、D-Bus socket 不通、内核驱动缺陷、USB 主机控制器锁死 —— 五层里任何一层都能造成一样的现象。把层次图画出来,才知道自己修的是哪一层。
02
分清控制面与数据面
「能发一条命令」和「能扛住一条流」是完全不同的两件事。这次所有安全能力都在控制面,所有致命问题都在数据面。早一点建立这个二分,能省掉两次硬重启。
03
自己加的东西也要纳入怀疑
最后那个「开机必死」不是内核的锅,是我们 10-28 装的开机自启 hook 与 11-04 新增的旁路程序打起来了。排查时把「我们自己最近改了什么」列进假设清单,和内核缺陷同等对待。

12如果重来 / 后续还能做什么

优先级方向说明
P0保持设备冷静态不再动自启与 USB 绑定。当前状态稳定、adb 可用,是所有后续工作的前提。
P1Write_Class_of_Device 0x0C24让手机把本机显示为「游戏手柄 / 键盘」而不是未知设备,改善识别。单条控制命令,属已验证安全区。
P1Write_Extended_Inquiry_Response 0x0C52在扫描阶段就广播名称与服务 UUID,手机不用再发 Remote Name Request。同样是单条命令。
P1日志持久化 / 状态记忆HCI 事件写文件、按 MAC 统计连接尝试、开机恢复上次的开关状态。纯用户态,不碰 USB。
P1拆分「可发现 / 可连接」Scan_Enable 的 1 / 2 / 3 三态,把「只被搜到」和「允许回连」分开,减少无谓的失败连接。
P2UART 蓝牙协处理器(最推荐)外接 ESP32 / BT-HID→串口模块负责整条蓝牙协议栈,TPmini 只从 UART 收按键字节,完全绕开 dwc USB 栈。项目里已有 uart_probe.c 可复用。这是让怀旧游戏盒真正用上蓝牙手柄最务实的一条。
P22.4G / 有线 USB 手柄走标准 HID 驱动,与 btusb 的批量流量路径不同,值得实测;有线方案最省事、延迟也更低。
P2换 dongle / 补内核 / 补全 BlueZ换芯片组避开 dwc + CSR8510 组合,或替换 3.0.36 的 dwc_otg 驱动、补 firmware 与完整 BlueZ。工程量与不确定性都最大,且 §4 的推论提示换 dongle 胜率不高。
所有后续方案的边界条件

只要还依赖 USB 批量 / 中断端点的持续流量(Inquiry、ACL、HID 报告),这个内核就不可靠。P1 全部是已验证安全的单条控制命令,可以放心加;任何「让手柄输入真正进游戏」的目标,建议从 P2 里选一条绕开 USB 蓝牙栈的路线,而不是继续在 usbfs 上补协议栈。

如果时间倒流,最想改的一个决定是:在第一次拿到 Write_Scan_Enable=3 成功回包时,就把目标从「主动扫描」切到「被动可发现」。当时我们已经手里握着能安全工作的一半能力,却因为执着于最初那条验收链路,又花了两轮硬重启去证明另一半不可行。当然,那两轮也换来了「控制面 / 数据面」这条最硬的结论 —— 不算白花。

A附录:产物、数字与术语

A.1 主要产物

代码 / 二进制 / 文档 / 取证日志
路径说明
release/btctl-kit/src/btctl.c主程序:D-Bus 报文层 + BlueZ 封装 + 单线程 HTTP :8787 + 状态/动作。约 69 KB
release/btctl-kit/src/usbscan.c / .husbfs 旁路模块:接口级 unbind + claim、HCI 控制面、事件泵、连接统计、软件电源
release/btctl-kit/src/ui.html + gen_ui.py + ui_data.h最终版「适配器控制面板」源码、生成器、内嵌 C 数组(18924 B / 19 段)
release/btctl-kit/bin/btctl_new最终 ARM 二进制 86308 B(NDK r30 clang -O2,动态链接)
usbhci.c / disctest.c / hcidbg.c诊断三件套:HCI over usbfs 探针 / DISCONNECT ioctl 穷举 / 控制器窗口测试
bt_launcher.c / bt_bluez.c / spawn.c / nosetuid.soBlueZ 路线的四个基础设施:造 android socket、注入环境、脱离器、禁用降权
bt_boot.sh / bt_up.sh / bt_restart.sh / deploy_btctl.sh自启与部署脚本(现已全部 .disabled)
BT_DIAGNOSIS.md / btdocs/TPmini蓝牙启用指南.md / P0P1_回滚手册.md阶段诊断报告、启用指南、回滚手册
emergency_watch.log / stop_autostart_result.log上线窗口监测与开机自启根因的完整取证(含各脚本源码、BlueZ 初始化日志、禁用清单)
.workbuddy/memory/2026-10-27 ~ 11-04.md四份逐轮工作日志,本文的主要事实来源

A.2 关键数字速查

数字含义
3.0.36+内核版本,一切 USB 怪癖的根源
41evt_rx 永远停在这个数(内核初始化 ~28 + BlueZ setup ~13),之后任何命令无回包
141 → 141发一条 HCI 命令后 urbnum 不变 → URB 在 usb core 前置检查就被拒
0a12:0001 / 00:1A:7D:DA:71:13CSR8510 A10 的 USB ID 与 BD_ADDR
0x0C1A = 3让固件自主应答 Inquiry/Page 的那一条安全命令
0x10Connection Accept Timeout,手机 5 次连接尝试的统一结局
5555 / 8787adb 网络通道 / btctl Web 面板端口
86308 B / 18924 B最终二进制体积 / 内嵌面板 HTML 体积
19被禁用并改名的自启/诊断脚本数量
5+因 USB 栈锁死而需要的硬断电重启次数

A.3 术语

缩写含义
HCIHost Controller Interface,主机与蓝牙控制器之间的命令/事件接口;opcode 高 6 位 OGF + 低 10 位 OCF
ACL / L2CAP / SDP / HIDP蓝牙链路层之上的协议栈:异步链路 / 逻辑链路适配 / 服务发现 / HID 输入。手柄要能用,这四层缺一不可
SSPSecure Simple Pairing,蓝牙 2.1+ 的安全配对流程(IO Capability、User Confirmation、Passkey)
usbfs/dev/bus/usb/* 提供的用户态 USB 访问接口,可绕过内核驱动直接做控制/批量传输
URBUSB Request Block,内核 USB 传输的基本单位;usb_submit_urb() 失败是本次的关键故障点
dwc_otgSynopsys DesignWare USB OTG 控制器驱动,本机有两个实例(usb20_host / usb20_otg)
btusb内核的 USB 蓝牙 HCI 传输驱动,把 HCI 包映射到控制/批量/中断端点
BlueZ 4.93本机原厂采用的 Linux 官方蓝牙协议栈版本;与 framework 的 Bluedroid 互不相通
Scan_EnableHCI 参数:0=都关、1=Inquiry Scan(可被搜到)、2=Page Scan(可被连接)、3=两者都开
关于这篇文档

全文基于 2026-10-27 至 11-04 的真机操作日志、源码、命令回显与取证文件整理。所有代码块中标注「真实源码」的片段均摘自项目文件;终端输出为实机实录;图片来源见各处图注。没有把本地渲染图冒充实机截图,也没有把未跑完的 API 回归写成「已验证」。

TPmini 蓝牙折腾记 · 一次从应用到内核、再从内核回到应用的完整往返
RK3066 / Android 4.2.2 / Linux 3.0.36 · CSR8510 A10 · 2026-10-27 → 11-04
« 上一篇

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

检测中…
召唤 伊丝特瓦尔