20、蓝牙日志与调试:HCI日志抓取(btsnoop)、BluetoothAdapter.enable/disable调试、常见异常(GATT_ERROR、CONNECTION_TIMEOUT)分析
做蓝牙开发,最怕什么?
不是代码写不出来,而是出了问题不知道从哪下手。设备连不上、配对失败、服务发现超时……这些坑我几乎都踩过。说实话,蓝牙调试比普通应用调试要麻烦得多,因为它涉及两个设备、多层协议栈,还有射频环境的影响。
这一章,我就把蓝牙调试的核心方法掰开揉碎了讲给你听。你掌握了这些,至少能解决80%的线上问题。
20.1 HCI日志抓取:蓝牙调试的“黑匣子”
HCI日志,业内也叫btsnoop日志。它记录了蓝牙芯片和蓝牙协议栈之间所有的通信数据包。说白了,就是蓝牙的“黑匣子”。
我个人的习惯是:遇到蓝牙问题,第一件事就是抓HCI日志。没有日志,全靠猜,那效率太低了。
20.1.1 如何开启HCI日志
Android系统默认支持btsnoop日志,但需要手动开启。不同Android版本略有差异,但核心方法一致。
方法一:开发者选项开启(推荐)
- 进入「设置」→「开发者选项」
- 找到「蓝牙HCI信息收集日志」或「Enable Bluetooth HCI snoop log」
- 开启后,系统会自动抓取HCI日志
方法二:adb命令开启
如果你在调试开发板或没有UI的设备,可以用adb命令:
# 开启HCI日志
adb shell settings put global bluetooth_btsnoop_log_mode 1
# 设置日志保存路径(可选)
adb shell settings put global bluetooth_btsnoop_log_path /sdcard/btsnoop_hci.log
# 重启蓝牙服务
adb shell svc bluetooth disable
adb shell svc bluetooth enable
20.1.2 日志文件位置与解析
日志文件默认保存在:
/sdcard/btsnoop_hci.log(Android 10及以下)/data/misc/bluetooth/logs/(Android 11及以上)
抓取到日志后,用Wireshark打开。Wireshark对btsnoop格式有原生支持,打开后你会看到这样的结构:
| 帧类型 | 方向 | 描述 |
|---|---|---|
| HCI Command | Host → Controller | 主机发送给蓝牙芯片的命令 |
| HCI Event | Controller → Host | 蓝牙芯片返回的事件通知 |
| ACL Data | 双向 | 实际的数据传输包 |
| SCO Data | 双向 | 语音数据包 |
btsnoop 可以只看蓝牙相关包。我习惯配合 btatt 过滤器来定位GATT相关的问题,非常高效。
20.1.3 实战:从HCI日志定位连接失败
举个例子。有一次,我遇到一个设备总是连接失败。上层回调返回了GATT_CONN_FAIL_ESTABLISH。看代码看不出问题,于是我抓了HCI日志。
日志中关键片段如下:
HCI Command: LE Create Connection (0x08|0x000D)
Peer Address: XX:XX:XX:XX:XX:XX
Conn Interval: 24-40
Supervision Timeout: 2000
HCI Event: Command Status (0x0F)
Status: Success (0x00)
HCI Event: LE Connection Complete (0x3E)
Status: Connection Timeout (0x08) ← 问题在这里!
Connection Handle: 0x0000
看到Connection Timeout (0x08),我立刻明白了——不是协议栈的问题,是蓝牙芯片根本没收到对方的响应。后来排查发现,是设备距离太远,信号太弱。
Status字段非常关键。0x00表示成功,非0值就是错误码。建议把常见的错误码背下来,调试时会快很多。
20.2 BluetoothAdapter.enable/disable调试
开关蓝牙这个操作,看起来简单,但坑不少。我见过很多开发者直接调enable(),然后就不管了。结果呢?蓝牙没完全开启就开始扫描,当然会失败。
20.2.1 正确的开关流程
enable()和disable()是异步操作。调用后,蓝牙状态不会立刻改变。你需要监听ACTION_STATE_CHANGED广播。
// 注册广播接收器
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, filter);
// 开启蓝牙
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
adapter.enable();
// 广播接收器中处理状态变化
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.STATE_OFF);
int prevState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE,
BluetoothAdapter.STATE_OFF);
switch (state) {
case BluetoothAdapter.STATE_ON:
// 蓝牙已开启,可以开始操作
Log.d(TAG, "蓝牙已开启");
break;
case BluetoothAdapter.STATE_TURNING_ON:
// 正在开启中,不要做任何操作
Log.d(TAG, "蓝牙正在开启...");
break;
}
}
};
STATE_ON回调中再执行扫描或连接操作。不要用Thread.sleep()等待,那是不靠谱的。我曾经在一个项目里看到同事用sleep(3000),结果在某些手机上就是不行,因为不同芯片的启动时间差异很大。
20.2.2 调试开关失败的常见原因
如果enable()调用后,状态一直停留在STATE_TURNING_ON,或者直接回到STATE_OFF,通常有以下几个原因:
- 蓝牙芯片固件异常:需要重启系统或重新加载固件
- 权限不足:缺少
BLUETOOTH_ADMIN权限 - 飞行模式冲突:飞行模式下蓝牙会被强制关闭
- 系统服务异常:
com.android.bluetooth进程崩溃
遇到这种情况,我一般先看logcat中蓝牙服务的日志:
adb logcat -s BluetoothAdapter:B BluetoothManagerService:B bt_stack:B
重点关注有没有E/级别的错误日志。如果有BluetoothManagerService报enable timeout,那基本就是芯片没响应了。
20.3 常见异常分析:GATT_ERROR与CONNECTION_TIMEOUT
这两个错误,是蓝牙开发中遇到最多的。我敢说,每个蓝牙开发者都至少被它们折磨过一次。
20.3.1 GATT_ERROR (0x85)
GATT_ERROR是一个“万能错误码”。它表示GATT协议层发生了错误,但具体原因需要进一步分析。
常见的触发场景:
- MTU协商失败
- 服务发现过程中断
- 写入特征值时,对方设备返回了错误
- 连接参数更新被拒绝
我的排查思路:
- 先看HCI日志,定位到出错的GATT操作
- 检查MTU协商是否成功。如果MTU只有23(默认值),说明协商没完成
- 确认对方设备是否支持该UUID的服务
- 检查是否在正确的线程中调用GATT API
GATT_ERROR。查了两天,最后发现是写入的数据包大小超过了MTU。Android的BluetoothGatt.writeCharacteristic()不会自动分包,你需要自己处理。如果数据超过MTU-3,就要拆成多个包发送。
20.3.2 CONNECTION_TIMEOUT (0x08)
这个错误码在HCI层是0x08,在上层回调中表现为GATT_CONN_TIMEOUT。
它表示蓝牙连接在指定时间内没有建立成功。超时时间由Supervision Timeout参数控制,默认一般是20秒。
常见原因:
| 原因 | 排查方法 |
|---|---|
| 设备距离太远 | 靠近设备重试,看是否成功 |
| 设备已连接其他主机 | 检查对方设备是否已被其他手机连接 |
| 蓝牙芯片繁忙 | 重启蓝牙,或等待一段时间重试 |
| 连接参数不合理 | 检查connectGatt()中的autoConnect参数 |
优化建议:
如果你发现连接超时频繁发生,可以尝试调整连接参数。但要注意,这需要对方设备也支持参数更新。
// 请求更短的连接间隔
BluetoothGatt gatt = device.connectGatt(context, false, callback);
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
// 注意:这只是一个请求,对方设备可能拒绝
connectGatt()。每次连接失败后,建议等待至少5秒再重试。否则蓝牙芯片可能因为频繁的连接请求而挂起。我见过一个极端案例,每秒重试3次,结果蓝牙芯片直接死机了。
20.4 本章小结
蓝牙调试的核心,就是用数据说话。HCI日志是你的第一手证据,BluetoothAdapter的状态变化是操作的基础,而错误码分析则是解决问题的钥匙。
你想想看,如果每次遇到问题都能有条不紊地抓日志、看状态、查错误码,还有什么问题解决不了?
嗯,这一章的内容就到这里。调试工具和方法都给你了,剩下的就是多练。遇到问题别慌,按流程走一遍,大部分问题都能定位到根因。