BLE 里有一种情况特别烦。
设备能扫到,也能连上,onConnectionStateChange() 进了,discoverServices() 也成功了,日志一眼看过去甚至有种"都好了"的错觉。结果你等了半天,onCharacteristicChanged() 根本不来。
这种问题最容易把人带偏。因为从表面上看,蓝牙连接已经成功了,直觉上就会觉得"那剩下不就是等设备发数据吗"。但实际项目里,连接成功和数据真的能回来,中间差得还挺远。
我后来碰这种问题多了,基本都会先提醒自己一句:连上了,不代表链路已经通了。
BLE 这里最容易被忽略的一点,就是 GATT 连接只是把通道建起来。它不负责替你把通知打开,不负责替你初始化设备协议,也不负责保证你现在监听的就是那个真正会推数据的 characteristic。你如果把"连接成功"和"可以收消息"当成一回事,后面就很容易陷进去。
最常见的误区,就是以为这句代码做完事情就结束了:
kotlin
gatt.setCharacteristicNotification(characteristic, true)
很多文章也喜欢写到这里就停,给人的感觉像是"打开通知完成"。其实这一步更像是告诉 Android 本地蓝牙栈:我准备监听这个 characteristic 的变化了。它不等于设备端已经收到命令,也不等于设备现在真的会开始推。
真正关键的通常是下一步,也就是 CCCD。
kotlin
val cccd = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)
很多"连上了却没数据"的问题,说到底就是这里没做对。要么根本没写 descriptor,要么写错了值,要么设备要的是 indicate,你却按 notify 去开。更常见一点的,是 descriptor 还没真正写成功,后面的业务命令已经抢跑了。
这个顺序问题很容易被低估。因为代码写起来就是顺手几行:
kotlin
gatt.setCharacteristicNotification(characteristic, true)
gatt.writeDescriptor(cccd)
sendInitCommand()
看上去很顺,实际经常出事。writeDescriptor() 本身就是异步的,你如果还没等它成功,就先把初始化命令发给设备,设备那边即使已经回了,你也不一定接得住。最后从手机视角看就是一句很熟悉的话:明明都连上了,就是收不到数据。
更稳一点的写法反而很朴素。等 onDescriptorWrite() 成功以后,再继续下一步。
kotlin
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
sendInitCommand()
}
}
这时候你至少能确定,通知链路是真的被打开了,而不是只在本地做了个姿态。
还有一种情况也特别常见,就是 characteristic 本身就拿错了。很多 BLE 设备不是一个 characteristic 走天下,通常会拆成命令通道、响应通道、数据通道。你如果把"用来发命令"的那个 characteristic 拿去开通知,逻辑上未必会立刻报错,但它本来就不是用来上报数据的。结果你在 Android 端等回调,当然等不到。
这个时候最笨但最有效的办法,就是先看 property,不要靠猜。
kotlin
val properties = characteristic.properties
val supportsNotify =
properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0
val supportsIndicate =
properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0
如果一个 characteristic 连 notify / indicate 都不支持,那后面不用看了,方向就不对。
但 BLE 里更麻烦的一层,还不在 GATT,而在设备协议本身。
很多设备并不是"连上就推"。真实逻辑更像这样:
连接成功,服务发现成功,通知链路打开,App 再发一条初始化命令,设备收到以后才进入上报状态。
这一类设备特别容易把人带偏,因为从 Android 侧看,BLE 每一步都成功了,可就是没数据。你如果一直在 BLE 层绕,会觉得很玄。其实问题根本不是 BLE 没通,而是设备协议还没被真正激活。
所以这类问题最好分两层看。
第一层是 BLE 链路有没有通。也就是服务有没有发现到,通知有没有打开,descriptor 有没有写成功。
第二层是设备协议有没有进入工作状态。也就是你该发的初始化命令发了没有,设备是否真的开始上报,当前是不是对的模式。
这两层一旦混在一起,就很容易出现一种假象:所有 BLE API 都成功了,但业务就是没反应。
我现在自己排这种问题,基本不会再问"为什么没数据",而是会把状态拆开看。至少会分成这几段:
kotlin
enum class BleReadyState {
Disconnected,
Connected,
ServicesReady,
NotificationReady,
ProtocolReady
}
这个状态拆分看起来普通,但很有用。因为它能逼你把"连接建立""通知可用""协议 ready"这三件事分开。只要你代码里没有这几个边界,最后所有异常都会被压成同一句话:BLE 不稳定。
实际上,很多 BLE 所谓的不稳定,根本不是链路一直在抖,而是你太早把系统当成 ready 了。
如果把顺序收紧一点,比较稳的流程通常是这样的:
先连上设备。
再发现服务。
拿到正确的 characteristic。
打开本地 notification。
写 CCCD。
等 onDescriptorWrite() 成功。
再发初始化命令。
最后等设备开始上报。
这套流程一点也不酷,甚至显得有点啰嗦,但它比"连上之后一口气往下调"稳得多。BLE 这类项目到后面,拼的往往不是谁更会写 API,而是谁更肯老老实实按顺序做事。
如果你现在就卡在"连上了却收不到数据",最值得先查的不是 Android 蓝牙栈,也不是设备固件,而是下面这几件事:
你拿到的 characteristic 对不对。
CCCD 有没有写。
写的是 notify 还是 indicate。
descriptor 写成功之前是不是已经抢跑发命令了。
设备是不是还要求一条初始化命令,才会真正开始推数据。
很多时候,把这几件事捋顺,问题就已经解决了大半。
所以这类问题最后其实不是"为什么连上了没数据",而是你一开始就把两个阶段混成了一件事。
连接成功只是开始,数据链路 ready 是另一回事。