17、蓝牙常见问题与调试:连接失败原因分析(GATT_ERROR)、MTU协商失败、丢包处理、日志分析技巧(Logcat过滤)、抓包工具使用(nRF Sniffer)

蓝牙开发做到一定阶段,你会发现:写代码只占30%的时间,剩下70%全在调bug。尤其是连接失败、莫名其妙断连、数据传着传着就丢了——这些问题,光靠看代码是看不出来的。

这一章,我就把压箱底的调试经验拿出来。咱们从最常见的GATT错误开始,一路聊到抓包工具。你跟着走一遍,以后遇到蓝牙问题至少能有个清晰的排查思路。

核心思路:蓝牙调试 = 分层排查。先看底层连接(GATT状态),再看数据通道(MTU),最后看应用层(丢包)。别一上来就怀疑代码,很多时候是协议栈或环境的问题。

蓝牙调试分层排查流程图 第1层:连接状态 GATT_ERROR分析 第2层:数据通道 MTU协商 第3层:应用数据 丢包处理 辅助手段:Logcat过滤 → nRF Sniffer抓包 → 协议栈日志 常见错误码速查 0x01: 无效句柄 0x04: 权限不足 0x08: 连接超时 0x0D: 无效PDU 0x0E: 不支持 0x13: 资源不足 0x85: 加密不足 0x100: 协议栈内部

17.1 连接失败:GATT_ERROR 到底在说什么?

你调用 connectGatt(),回调里来了个 onConnectionStateChange,状态码是 GATT_ERROR(0x01)。这时候别慌,先看看具体是什么错误码。

我个人习惯是,在回调里把错误码打印出来,而不是只看状态。因为 GATT_ERROR 是个笼统的错误,真正的原因藏在 status 参数里。

错误码 含义 常见原因
0x01 无效句柄 设备已断开,或Service UUID不匹配
0x04 权限不足 未配对就尝试读写加密特征值
0x08 连接超时 距离太远、信号干扰、设备休眠
0x0D 无效PDU 发送了格式错误的数据包
0x85 加密不足 需要配对但未配对
0x100 协议栈内部错误 Android系统蓝牙栈异常,重启蓝牙可恢复

我的经验:遇到0x08(连接超时),先别急着改代码。先检查设备是不是进入了深度休眠。我曾经有个项目,设备5秒不操作就休眠,导致连接总是断。后来在设备端把休眠时间改到30秒,问题就解决了。

还有一个坑:onServicesDiscovered 回调里的错误。很多人只关注连接状态,忽略了服务发现失败。如果服务发现失败,后续所有读写操作都会返回0x01。

// 正确的错误处理姿势
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (status != BluetoothGatt.GATT_SUCCESS) {
        Log.e(TAG, "连接失败,错误码: 0x" + Integer.toHexString(status));
        // 根据错误码做不同处理
        switch (status) {
            case 0x08: // 超时
                // 尝试重连,最多3次
                break;
            case 0x85: // 加密不足
                // 触发配对流程
                break;
            default:
                // 记录日志,上报
                break;
        }
    }
}

17.2 MTU协商失败:数据传着传着就断了

MTU(最大传输单元)决定了单次能传多少数据。默认是23字节,其中3字节是协议头,实际有效载荷只有20字节。如果你要传大包,必须协商更大的MTU。

但MTU协商不是每次都能成功。我遇到过两种情况:

  • 设备端不支持:有些低端蓝牙芯片只支持默认MTU,你请求512,它直接拒绝。
  • 协商时机不对:在服务发现完成之前就请求MTU,会失败。

注意:MTU协商是双向的。你请求的值,设备不一定接受。最终MTU取两者中的较小值。比如你请求512,设备只支持128,那最终MTU就是128。

正确的做法是:在 onServicesDiscovered 回调之后,立即发起MTU请求。

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        // 服务发现成功,立即请求MTU
        gatt.requestMtu(512);
    }
}

@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "MTU协商成功: " + mtu);
        // 根据实际MTU计算分包大小
        int payloadSize = mtu - 3; // 减去协议头
    } else {
        Log.w(TAG, "MTU协商失败,使用默认值23");
        // 降级处理,按20字节分包
    }
}

我记得有个项目,设备端MTU只支持128,但我们的协议要求每次传200字节。如果不做分包,数据就会丢失。后来我在应用层做了分包逻辑,每次发125字节(留3字节给协议头),分两包发送,问题就解决了。

17.3 丢包处理:别指望蓝牙100%可靠

蓝牙BLE本身有重传机制,但在实际环境中,丢包还是会发生。尤其是当两个设备距离较远,或者周围有Wi-Fi干扰时。

丢包的表现形式很多:

  • 数据发出去,设备没收到
  • 设备发了通知,手机没收到回调
  • 数据顺序乱了(先发的后到)

我的做法是:在应用层加一个简单的确认机制。

// 发送数据时带上序列号
public void sendData(byte[] data) {
    int seq = getNextSeq();
    byte[] packet = new byte[data.length + 1];
    packet[0] = (byte) seq;
    System.arraycopy(data, 0, packet, 1, data.length);
    
    // 发送并启动超时计时器
    gatt.writeCharacteristic(characteristic, packet);
    startTimeoutTimer(seq, 3000); // 3秒超时
}

// 设备回复确认包
public void onAckReceived(int seq) {
    cancelTimeoutTimer(seq);
    Log.d(TAG, "数据包 " + seq + " 确认收到");
}

// 超时重传
public void onTimeout(int seq) {
    if (retryCount[seq] < 3) {
        retryCount[seq]++;
        resendPacket(seq);
    } else {
        Log.e(TAG, "数据包 " + seq + " 重传3次失败,上报");
        notifyDataLoss(seq);
    }
}

避坑指南:我曾经在某个项目中,发现丢包率高达30%。排查了半天,最后发现是手机和设备的时钟不同步,导致超时时间设置得太短。后来我把超时时间从1秒改成3秒,丢包率降到了5%以下。嗯,有时候问题不在协议,在参数设置上。

17.4 Logcat过滤:从海量日志中找到关键信息

Android的蓝牙日志非常庞大。如果你直接看全部日志,眼睛会看瞎。我一般用这几个过滤条件:

过滤标签 用途
BluetoothGatt 查看GATT操作(连接、读写、通知)
BluetoothDevice 查看设备状态变化
bt_btif 蓝牙协议栈内部日志
bt_hci HCI层日志,能看到原始蓝牙包
你的TAG 应用层自定义日志

在Android Studio的Logcat中,我习惯这样组合过滤:

// 同时过滤多个标签
tag:BluetoothGatt OR tag:bt_hci OR tag:MyApp

// 排除干扰日志
-tag:BluetoothLeScanner -tag:BluetoothAdapter

还有一个技巧:开启蓝牙HCI日志。在开发者选项里,打开「蓝牙HCI信息收集日志」。这样系统会把所有蓝牙HCI包记录到 /sdcard/btsnoop_hci.log 文件中。这个文件可以用Wireshark打开,非常强大。

17.5 nRF Sniffer:抓包才是终极手段

当Logcat已经无法满足你时,就该上抓包工具了。我个人最常用的是nRF Sniffer,配合Wireshark使用。

你需要准备:

  • 一块nRF52840 DK开发板(或nRF Sniffer USB dongle)
  • 安装nRF Sniffer for BLE插件
  • Wireshark(版本3.0以上)

抓包能帮你看到什么?

  • 连接请求和响应是否正常
  • MTU请求和响应的具体数值
  • 数据包是否真的发出来了
  • 设备是否发送了断连请求
  • 空中是否有其他蓝牙设备在干扰

我的实战经验:有一次设备总是断连,Logcat里只显示GATT_ERROR。用nRF Sniffer一抓,发现设备每隔10秒发送一个断连请求(原因码0x13,远程用户终止连接)。后来和设备端工程师沟通,发现是设备端的看门狗超时,把蓝牙任务给杀了。这个问题如果不用抓包,光看手机端日志,永远找不到原因。

抓包的基本步骤:

  1. 把nRF Sniffer插入电脑,打开Wireshark
  2. 选择nRF Sniffer接口,开始捕获
  3. 在Wireshark里过滤 btle,只看BLE包
  4. 找到你的设备广播包,右键「Follow」→「Bluetooth Stream」
  5. 观察连接事件、数据包、断连原因

常用的Wireshark过滤表达式:

// 只看某个设备的包
btle.advertising_address == 11:22:33:44:55:66

// 只看连接请求
btle.opcode == 0x05

// 只看断连包
btle.opcode == 0x06

// 只看MTU请求
btl2cap.cid == 0x0004 && btl2cap.opcode == 0x02

注意:抓包时,手机和设备尽量靠近Sniffer。如果距离太远,Sniffer可能收不到包,导致抓包结果不完整。我一般把Sniffer放在手机和设备中间,距离不超过1米。

好了,这一章的内容就到这里。蓝牙调试是个经验活,遇到问题别急,按分层思路一步步排查。Logcat看应用层,HCI日志看协议栈,nRF Sniffer看空中包——三层结合,大部分问题都能定位到。

记住:蓝牙不是TCP,它天生不可靠。你的代码里,永远要为丢包、断连、超时做好预案。


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