Android BLE 为什么连上了却收不到数据

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 是另一回事。

相关推荐
pengyu2 小时前
【Kotlin 协程修仙录 · 炼气境 · 后阶】 | 划定疆域:CoroutineScope 与 Android 生命周期的绑定艺术
android·kotlin
朝星2 小时前
Android开发[5]:组件化之路由+注解
android·kotlin
随遇丿而安2 小时前
Android全功能终极创作
android
随遇丿而安2 小时前
第1周:别小看 `TextView`,它其实是 Android 页面里最常被低估的组件
android
summerkissyou19875 小时前
Android-基础-SystemClock.elapsedRealtime和System.currentTimeMillis区别
android
ian4u5 小时前
车载 Android C++ 完整技能路线:从基础到进阶
android·开发语言·c++
学习使我健康7 小时前
Android 中 Service 用法
android·kotlin
2601_949816687 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql