17、蓝牙常见问题与调试:连接失败原因分析(GATT_ERROR)、MTU协商失败、丢包处理、日志分析技巧(Logcat过滤)、抓包工具使用(nRF Sniffer)
蓝牙开发做到一定阶段,你会发现:写代码只占30%的时间,剩下70%全在调bug。尤其是连接失败、莫名其妙断连、数据传着传着就丢了——这些问题,光靠看代码是看不出来的。
这一章,我就把压箱底的调试经验拿出来。咱们从最常见的GATT错误开始,一路聊到抓包工具。你跟着走一遍,以后遇到蓝牙问题至少能有个清晰的排查思路。
核心思路:蓝牙调试 = 分层排查。先看底层连接(GATT状态),再看数据通道(MTU),最后看应用层(丢包)。别一上来就怀疑代码,很多时候是协议栈或环境的问题。
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,远程用户终止连接)。后来和设备端工程师沟通,发现是设备端的看门狗超时,把蓝牙任务给杀了。这个问题如果不用抓包,光看手机端日志,永远找不到原因。
抓包的基本步骤:
- 把nRF Sniffer插入电脑,打开Wireshark
- 选择nRF Sniffer接口,开始捕获
- 在Wireshark里过滤
btle,只看BLE包 - 找到你的设备广播包,右键「Follow」→「Bluetooth Stream」
- 观察连接事件、数据包、断连原因
常用的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