作为一名常年跟蓝牙外设打交道的开发,我太清楚那种“设备就在眼前,回连就是失败,Logcat翻了几百屏也看不出所以然”的滋味了。后来真正解决问题,靠的是小米手机上抓一份HCI log,再用Wireshark打开一帧一帧看协议栈底层的交互,问题基本半小时内就锁定了。这篇就把我一直用的小米手机HCI log抓取与解析流程完整写出来,含开关位置、导出命令、解析要点和避坑记录,照着做五分钟就能上手。
1. 先搞清楚HCI log到底在记录什么
1.1 一次真实的蓝牙调试现场
之前做一款BLE体重秤的适配,遇到一个特别诡异的问题:小米手机上App读历史记录经常超时,但同一台秤换iPhone就一切正常。我在Logcat里看了半天,只看到GATT回调偶尔报133错误,至于为什么报133、是手机发的读请求没到秤端,还是秤端回了数据但手机没收到,完全看不出来。
后来在小米手机上打开蓝牙HCI日志,重新复现了一次超时,导出日志用Wireshark打开,一帧一帧对照,发现是秤端返回的Read Response长度字段和实际数据长度不一致,手机协议栈直接丢弃了,根本没有上抛到应用层。这种问题,不到HCI层是绝对定位不了的。
1.2 HCI log和Logcat的分工
很多初学者容易把Logcat和HCI log混为一谈。简单说,Logcat是Android系统层面的日志,记录App调用、系统服务状态、各种print输出;HCI log则是蓝牙控制器(Controller)和蓝牙协议栈(Host)之间所有HCI报文的全量记录。
用生活类比,Logcat好比公司前台登记访客的记录,写着“谁来了、几点走的”;HCI log则是大厦监控视频,每一帧画面都在,谁按的电梯、在哪个楼层停留、门禁为什么没开,全部有据可查。
所以当问题发生在系统上层,比如权限没开、服务没绑定,Logcat基本够用;但当问题涉及连接建立、断开、配对、加密、断连重连这些协议栈层面的交互时,必须上HCI log。
1.3 什么场景必须依赖HCI log
根据我的经验,这几类问题在小米手机上排查时,HCI log几乎是唯一靠谱的突破口:
- 连接时好时坏,回连成功率不稳定
- 配对反复失败,或配对成功后重启又失效
- 连接后不久自动断开,间隔没有明显规律
- GATT读写超时,或偶尔返回133、257这类系统错误码
- 多设备同时连接时的互相干扰问题
- 不同品牌手机行为不一致,需要对比协议栈层面的差异
一句话:只要问题涉及“蓝牙链路本身”,先抓HCI log,别在Logcat里瞎猜。
2. 小米手机抓取HCI log:从开关到拿到文件
2.1 准备工作:开发者模式与日志开关
在小米手机上抓HCI log,第一步是先确保开发者选项已经打开。路径是“设置 → 我的设备 → 全部参数与信息 → 连续点击OS版本号(或MIUI版本号)7次”,直到提示已进入开发者模式。
接着进“设置 → 更多设置 → 开发者选项”,里面有几个开关容易让人晕,我逐个说清楚:
- “日志抓取”或“日志分析”总开关:部分机型需要先打开这个,下面才会显示更多日志细分项
- “蓝牙HCI日志”:这是核心开关,打开后系统会把HCI报文存到btsnoop文件
- “蓝牙调试日志”:一些机型上是这个叫法,作用一致
需要注意的是,开启蓝牙HCI日志后,很多小米机型会先关闭蓝牙,需要在通知栏重新打开蓝牙。这不是故障,是在重置蓝牙芯片的日志记录上下文,保证后续新开的连接都会被完整记录下来。
2.2 开启HCI日志的两种方式
我平时常用的方式有两种,按场景选。
第一种是纯界面操作,适合临时抓一次日志。设置 → 开发者选项 → 打开“蓝牙HCI日志”,重新打开蓝牙,复现问题,然后到文件管理器里把日志导出来。日志默认在“内部存储 / MIUI / log / bluetooth /”目录下,文件名一般是btsnoop_hci.log或带时间戳的hci_xxx文件。
第二种是配合adb命令,适合需要长时间抓取或者自动化复现的场景。先把开发者选项里的USB调试打开,连接电脑后执行:
# 查看当前蓝牙HCI日志开关状态 adb shell settings get global bluetooth_hci_log # 打开HCI日志(部分平台支持) adb shell settings put global bluetooth_hci_log 1 # 重启蓝牙服务让其生效 adb shell service call bluetooth_manager 6这里多说一句,service call bluetooth_manager 6这个写法在不同Android版本上可能不稳定,我一般直接在手机上手动关开一下蓝牙更省事。日志开关打开后,为了保险起见,我习惯再用命令确认一下配置有没有写进去。
导出日志时,我推荐用adb pull,比在手机上翻文件管理器可靠得多:
# 导出整个蓝牙日志目录 adb pull /sdcard/MIUI/log/bluetooth/ D:/bt_log/ # 如果目录不存在,试着找一下常见路径 adb shell ls /sdcard/MIUI/log/ adb shell ls /sdcard/Android/data/com.android.bluetooth/files/部分小米机型权限收紧后,/sdcard/MIUI/log不一定直接可见,这时候可以先执行一次adb bugreport,把系统诊断包拉到电脑上,里面通常也会附带蓝牙HCI日志,只是路径更深,稍麻烦一点。
2.3 5分钟流程怎么安排最合理
很多人说的5分钟搞定,其实不是打开开关那一下,而是一条完整的操作链路。我自己的固定节奏是这样:
头1分钟:开启开发者选项里的蓝牙HCI日志,关闭再打开蓝牙,确认状态正常。
中间2分钟:按照预先想好的测试步骤“干净地”复现一次问题。注意不要在这个阶段做其他无关操作,比如切Wi-Fi、刷抖音,这些都有可能在日志里引入大量干扰帧。
最后2分钟:用adb pull或文件管理器把日志文件导出,拉到电脑上用Wireshark打开。整个过程看起来简单,但大部分人在第一步就栽了——没复现就切走了,导出来的日志里什么都没有。
另外,如果问题不是必现的,我建议把“蓝牙HCI日志”长期开着,日志文件会滚动覆盖,出问题时再导出,比临时打开等着复现有效得多。
3. 用Wireshark解析HCI log的实战细节
3.1 工具选择:为什么首选Wireshark
市面上能看btsnoop的工具有不少,Frontline、Ellisys这些商业方案功能很强,但价格不菲且上手成本高。对大部分Android开发者来说,Wireshark完全够用,而且免费、跨平台、对btsnoop格式支持得非常好。
小米手机抓出来的日志一般就是标准btsnoop格式,Wireshark可以直接打开。如果遇到文件后缀是.log但Wireshark识别不出来的情况,可以尝试在打开文件时手动选择“Bluetooth Snoop Capture”作为解析格式,多数时候能救回来。
3.2 打开文件与过滤器的使用
用Wireshark打开HCI log后,界面会直接按时间顺序显示所有蓝牙HCI报文。不熟悉的人第一眼会觉得很乱,因为里面全是HCI_Command、HCI_Event、HCI_ACL这种字样。
我的建议是先用过滤栏把协议类别分开。最常用的几个过滤器:
btl2cap 只看L2CAP层,适合分析连接建立和通道管理 btgatt 只看GATT层,适合分析服务发现和读写请求 bthci_evt 只看HCI事件,适合快速找连接、断开、配对状态变化 bthci_cmd 只看HCI命令,适合确认主机到底下发了什么指令 btsmp 只看安全管理层,适合定位配对和加密问题举个例子,如果怀疑是断连问题,直接在过滤栏输入bthci_evt && bthci_evt.code == 0x05,就能把所有Disconnection Complete事件筛出来,每条记录都会带Reason字段,那个数字就是断连原因码。
如果是GATT读写问题,建议用btgatt过滤,然后在“电话”右侧的协议详情里双击某条GATT请求,右键选择“Follow”可以追踪同一条ATT通道上的完整交互序列,很快就能看出是请求没发出去还是响应没回来。
3.3 三种典型故障在log里的样子
解析这块光讲理论容易飘,我拿三个真实遇到过的场景演示一下特征长什么样。
场景一是“扫描不到设备”。在HCI日志里,主机会周期性地下发LE Set Scan Parameters和LE Set Scan Enable命令,事件里会有LE Advertising Report。如果扫描命令正常下发但完全没有Advertising Report,说明手机射频或天线附近干扰严重,或者设备压根没在广播;如果连Scan Enable命令都没有,那问题在协议栈上层,跟设备端无关。
场景二是“配对失败”。配对过程里一定会出现SMP层的Command和Response,如果看到Pairing Failed,后面带一个Reason,例如0x05表示不支持的配对方式,0x08表示密钥或确认值不匹配,排查方向就非常明确了。我曾遇到过一个设备,HCI日志里反复出现Security Request后主机直接返回Negative Reply,一看就是设备端bonding信息没存住,每次都当成新配对来处理。
场景三是“连接建立后立刻断开”。一般会看到三条连续的记录:Connection Complete(成功)、随后不久出现Disconnection Complete、Reason指向0x08或0x13。0x08代表连接超时,常见于设备端没有及时回应握手包;0x13代表对端主动断开,常见于设备端业务逻辑主动踢人。这一步定位后,再联合设备端日志一起看,基本都能收敛。
4. 从HCI log反推根因:错误码与交互链
4.1 错误码速查与责任划分
HCI log最有价值的地方在于断连原因码不会说谎。我把开发中高频遇到的错误码整理成了速查表,解析时对照着看非常方便:
| 错误码 | 含义 | 一般指向 |
|---|---|---|
| 0x08 | Connection Timeout | 对端没有及时响应,链路超时 |
| 0x13 | Remote User Terminated Connection | 对端主动断开 |
| 0x16 | Connection Terminated by Local Host | 手机本地主动断开 |
| 0x0D | Connection Rejected due to Limited Resources | 设备资源不足,常见于设备端连接数满 |
| 0x0E | Connection Rejected due to Security Reasons | 安全机制拒绝连接 |
| 0x05 | PIN or Key Missing | 配对密钥丢失或未保存 |
| 0x3E | Connection Failed to be Established | 连接建立失败,物理层或对端无响应 |
需要说明的是,不同蓝牙芯片厂商在错误码映射上可能存在细微差异,速查表只作为参考方向,拿到具体日志后还是要去对照相应IC的文档确认一次。
判断责任方的逻辑其实很简单:先看谁发的Disconnection Complete,本地芯片给的事件Reason代表手机协议栈或驱动那边的判断,远端断的一般会通过Link Loss或对端主动断开的Reason传回来。再结合时间点前后有没有对端发来的数据,基本就能确定是手机的问题、协议栈的问题还是设备端的问题。
4.2 时间轴对齐与GATT交互追踪
抓HCI log之后只做单看,往往还不够。我习惯把HCI log和Logcat按时间戳对齐来看,特别适合那种“应用层看起来卡了很久,但协议栈层面实际已经走完好几轮交互”的场景。
做法是先在HCI log里找到问题现象对应的时间点,记下时间戳,然后回到Logcat里把同一时间段的日志筛出来,看应用层收到回调的时机。如果HCI log显示数据早就到了,但Logcat回调一直没有触发,那就是数据在内部分发环节丢了;如果HCI log里显示一直在重传或等待ACK,那就要去查空包和干扰问题。
追踪GATT交互时,Wireshark的“电话”追踪功能很好用。两条GATT消息之间的时间间隔非常直观,只要间隔超过预期值几百毫秒甚至几秒,基本就是中间有重传或退避逻辑在作祟。
4.3 业务问题定位思路
除了协议栈层面的问题,HCI log还能帮我们反向验证业务层逻辑是否正确。比如低功耗蓝牙设备常见的电量上报、步数同步、固件升级这类业务,本质上都是GATT上的一次次读写和通知。
排查时先把台账拉出来:连接是哪个设备、服务发现是否完成、MTU协商到了多少、每次Write和Notification的字节数是否符合预期。这些信息在HCI log里全部都有。
我之前遇到一个OTA升级到一半总失败的问题,就是通过HCI log发现每次大包传输时,设备端在收到Write Request后都没有回Write Response,直到主机端触发了超时才中止。看log的时候那个等待时间序列特别明显,几乎是一帧一个hole。后来定位到是设备端Flash写入耗时过长,完全依赖主机的流控保护,手机蓝牙协议栈只要稍调一下连接参数就扛不住了。这种问题,不抓HCI log,单看应用层回调是真的看不出来。
5. 小米手机HCI log抓取的常见问题排查实录
5.1 开关找不到或日志抓不到
这是我被问过最多的问题,没有之一。在小米手机上,开发者选项里如果没有“蓝牙HCI日志”这个选项,先不要急着怪系统,大概率是日志抓取总开关没有打开。先到开发者选项里找到“日志抓取”或“日志分析”,把它打开,有些网上放出的教程会管它叫“日志捕获”,小米澎湃OS上的入口名称可能还会变,多留意一下。
如果总开关已经打开还是看不到蓝牙日志选项,可以试试插上USB线连电脑,开启USB调试后输入:
adb shell settings put global ble_log_on 1 adb shell settings put global bt_hci_log_on 1然后重新开关蓝牙。部分开发版系统支持这两个全局配置项,稳定版不一定全有,但值得一试。
还有一种情况是日志开关都开了,文件也生成了,但内容只有几百字节。这个大概率是打开日志开关之后没有重新开关蓝牙,导致蓝牙芯片的日志功能没有实际加入新的会话。处理方法就是先关掉蓝牙,再打开蓝牙,然后重新连接设备复现问题。
5.2 文件异常与解析失败
Wireshark打不开日志文件,先别急着换工具。btsnoop文件有个固定文件头,如果文件被截断或者中间有坏块,Wireshark就会报错。这时可以用十六进制工具打开看看文件头是不是62 74 73 6E 6F 6F 70,也就是字符btsnoop,如果是,基本没救不了;如果不是,有可能文件被系统转换过格式或者只抓到了部分包,可以试试用text2pcap工具把纯文本的日志转换成pcap格式再打开。
另外,在小米手机上如果用文件管理器直接查看日志文件,有可能会看到两个相似文件,一个带后缀.cfa或.tmp,一个才是当前可用的。优先导出不带后缀修改时间最新的那个。
文本模式下的HCI log解析是另一个坑。小米部分机型导出的日志不是标准btsnoop二进制,而是类似HCI RX、HCI TX开头的人类可读文本。这时候直接拖进Wireshark是打不开的,可以用text2pcap转一下,或者干脆换用Android Studio自带的Device File Explorer从/data/misc/bluetooth/logs/目录导出原始文件。
5.3 容易混淆的细节与经验技巧
最后分享几个我在实际过程中踩过坑才总结出来的经验,每一条都对应过一次真实的加班:
第一,抓HCI log时尽量把手机上的其他蓝牙设备断开。小米手机经常会同时连着耳机、手表、手环这些设备,这些设备产生的周期性连接和断连信息会把日志搞得很杂,分析时很难聚焦到目标设备上。如果条件允许,清空其他配对记录再抓,否则后期过滤时很痛苦。
第二,HCI log里没有“应用名”这个概念,只有蓝牙设备地址。想在日志里定位到自己的设备,先在Logcat里找到目标设备地址,再在Wireshark里用设备地址过滤,而不是盯着日志翻。
第三,抓日志前关闭蓝牙后重开,不只是为了让功能生效,更是为了拿到一条“干净的连接时间线”。从扫描到连接、服务发现、MTU协商再到业务发送,完整走一遍,后面任何一环出问题都能顺着时间线往前找。
第四,动态调整连接参数的问题。如果问题表现为偶尔卡顿、时延忽高忽低,可以重点看HCI log里LE Connection Update Complete事件前后的连接间隔和从机延迟参数。小米手机在系统负载变化时会调整蓝牙参数,这在某些芯片上是正常行为,但如果你做的是对时延敏感的音频或运动数据设备,就得在设备端做兼容。
6. 最后再分享一个小技巧:日志自动化保存
如果你的日常工作需要经常复现蓝牙问题,我强烈建议在小米手机上做一次“半自动化”的日志习惯:把“蓝牙HCI日志”长期开启,手机存储充裕的情况下它会滚动覆盖,问题出现时再快速导出,问题没出现时也不用管它。
导出时除了HCI log,最好同时导出adb bugreport的压缩包,里面有系统版本、蓝牙协议栈版本、已配对设备列表等信息。排查问题时这些资料往往是HCI log之外最有力的佐证。
我自己还会在电脑上建立一个按日期命名的日志归档目录,每份日志都附一段文字描述,记下现象、操作步骤、设备型号和系统版本。坚持一段时间后,你会发现很多“偶发问题”其实是同类原因反复出现,只是之前从没把日志留存下来做对比分析。
小米手机上抓HCI log这件事,难度真的不大,关键是要养成“先抓日志再分析”的习惯。协议栈底下的报文不会说谎,数据在哪、卡在哪、谁拒绝了谁,全部一清二楚。希望这篇内容能帮你少走一些弯路。