20、蓝牙日志与调试:HCI日志抓取(btsnoop)、BluetoothAdapter.enable/disable调试、常见异常(GATT_ERROR、CONNECTION_TIMEOUT)分析

做蓝牙开发,最怕什么?

不是代码写不出来,而是出了问题不知道从哪下手。设备连不上、配对失败、服务发现超时……这些坑我几乎都踩过。说实话,蓝牙调试比普通应用调试要麻烦得多,因为它涉及两个设备、多层协议栈,还有射频环境的影响。

这一章,我就把蓝牙调试的核心方法掰开揉碎了讲给你听。你掌握了这些,至少能解决80%的线上问题。

20.1 HCI日志抓取:蓝牙调试的“黑匣子”

HCI日志,业内也叫btsnoop日志。它记录了蓝牙芯片和蓝牙协议栈之间所有的通信数据包。说白了,就是蓝牙的“黑匣子”。

我个人的习惯是:遇到蓝牙问题,第一件事就是抓HCI日志。没有日志,全靠猜,那效率太低了。

20.1.1 如何开启HCI日志

Android系统默认支持btsnoop日志,但需要手动开启。不同Android版本略有差异,但核心方法一致。

方法一:开发者选项开启(推荐)

  1. 进入「设置」→「开发者选项」
  2. 找到「蓝牙HCI信息收集日志」或「Enable Bluetooth HCI snoop log」
  3. 开启后,系统会自动抓取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 双向 语音数据包
核心技巧:在Wireshark中,用过滤器 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),我立刻明白了——不是协议栈的问题,是蓝牙芯片根本没收到对方的响应。后来排查发现,是设备距离太远,信号太弱。

注意:HCI日志中的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/级别的错误日志。如果有BluetoothManagerServiceenable timeout,那基本就是芯片没响应了。

20.3 常见异常分析:GATT_ERROR与CONNECTION_TIMEOUT

这两个错误,是蓝牙开发中遇到最多的。我敢说,每个蓝牙开发者都至少被它们折磨过一次。

20.3.1 GATT_ERROR (0x85)

GATT_ERROR是一个“万能错误码”。它表示GATT协议层发生了错误,但具体原因需要进一步分析。

常见的触发场景:

  • MTU协商失败
  • 服务发现过程中断
  • 写入特征值时,对方设备返回了错误
  • 连接参数更新被拒绝

我的排查思路:

  1. 先看HCI日志,定位到出错的GATT操作
  2. 检查MTU协商是否成功。如果MTU只有23(默认值),说明协商没完成
  3. 确认对方设备是否支持该UUID的服务
  4. 检查是否在正确的线程中调用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的状态变化是操作的基础,而错误码分析则是解决问题的钥匙。

你想想看,如果每次遇到问题都能有条不紊地抓日志、看状态、查错误码,还有什么问题解决不了?

嗯,这一章的内容就到这里。调试工具和方法都给你了,剩下的就是多练。遇到问题别慌,按流程走一遍,大部分问题都能定位到根因。


公众号:蓝海资料掘金营,微信deep3321