3. Notification/Indication 使能不生效:CCCD 配置流程、权限检查与日志分析
Notification 和 Indication 不生效,这问题我碰到的次数太多了。说白了,就是手机收不到设备主动发上来的数据。很多开发者第一反应是“代码写错了”,其实大部分时候,是 CCCD 没配好,或者权限没开对。
今天咱们就把这个流程彻底捋一遍。你跟着我的思路走,以后遇到这类问题,十分钟内就能定位。
3.1 CCCD 到底是什么?
CCCD,全称是 Client Characteristic Configuration Descriptor。名字很长,但作用很简单——它是一个开关。这个开关放在服务端(比如你的蓝牙设备),由客户端(比如手机 App)来写。
客户端写 0x0001,表示“我要收 Notification”。
客户端写 0x0002,表示“我要收 Indication”。
客户端写 0x0000,表示“我不收了”。
嗯,就是这么直白。但为什么很多人配了还是不生效?
核心要点:CCCD 必须由客户端主动写入,服务端不能自己给自己开。很多设备端工程师在初始化时试图在本地把 CCCD 置为 0x0001,这是无效的。GATT 规范明确规定,CCCD 的写入权属于客户端。
3.2 客户端配置流程(标准步骤)
我习惯把客户端配置 CCCD 的流程拆成三步。你按这个顺序检查,基本不会漏。
- 发现服务与特征值
客户端先做 Service Discovery,找到目标特征值的句柄(Handle),以及它下面的 CCCD 句柄。 - 写入 CCCD
客户端向 CCCD 句柄写入 0x0001(Notification)或 0x0002(Indication)。注意,写入长度是 2 字节,小端序。 - 等待写入完成事件
客户端收到写入响应后,才算配置完成。之后服务端才能发 Notification/Indication。
这里有个坑——很多 App 开发者在写入 CCCD 后立刻调用发送接口,但此时服务端还没收到配置变更通知。我建议你在写入完成后,等一个 GATT 回调确认,再开始收数据。
3.3 服务端权限检查(最容易忽略的地方)
客户端配好了,服务端也收不到?那问题大概率出在权限上。我列一个检查清单,你对着看。
| 检查项 | 正确配置 | 常见错误 |
|---|---|---|
| 特征值属性 | Notify 或 Indicate 位必须置 1 | 只配了 Read/Write,没配 Notify |
| CCCD 权限 | 允许客户端写入(Write 权限) | CCCD 被设为只读或不可写 |
| 服务端配置变更回调 | 实现了 CCCD 写入后的回调函数 | 回调里没更新内部状态 |
| 连接句柄绑定 | 每个连接独立维护 CCCD 状态 | 多个连接共用一个 CCCD 标志位 |
我曾经在一个项目里排查了整整两天,最后发现是特征值的属性字段少配了一个 NOTIFY 位。你想想看,服务端自己都不支持发送,客户端配了 CCCD 又有什么用?
特别注意:有些蓝牙协议栈(比如某些国产芯片的 SDK)在定义特征值时,属性字段和 CCCD 权限是分开配置的。你配了 Notify 属性,不代表 CCCD 自动就有了 Write 权限。这两个必须分别设置。
3.4 典型日志分析(实战派)
日志是最好的老师。我一般会同时抓三份日志:设备端日志、手机端 HCI 日志、以及 Sniffer 空口日志。下面我拿一个真实案例来讲。
案例:Notification 发不出去
设备端日志片段:
[GATT] CCCD write received, handle=0x0012, value=0x0001
[GATT] Notification enabled for handle 0x0012
[APP] Sending notification...
[GATT] Error: Notification not sent, CCCD not enabled for this connection
看到没?设备端明明打印了“Notification enabled”,但发送时又说“CCCD not enabled”。这矛盾了。
我当时的分析思路是这样的:
- 第一行日志说明客户端确实写了 CCCD,值也是 0x0001,没问题。
- 第二行是应用层自己打印的,可能只是标记了全局变量,但没绑定到具体连接。
- 第四行报错,说明底层协议栈在发送时检查了连接句柄对应的 CCCD 状态,发现是关闭的。
问题找到了——应用层用一个全局变量 g_notify_enabled 来记录状态,但协议栈内部是按连接句柄来查的。当有多个连接时,全局变量被覆盖了。
我的建议:CCCD 状态一定要按连接句柄(conn_handle)来存储。用数组或哈希表,key 是 conn_handle,value 是 CCCD 值。别偷懒用全局变量。
Sniffer 日志怎么看?
如果你有 Sniffer,抓包后重点关注三个地方:
- Write Request:客户端向 CCCD 句柄发起的写入操作,数据应为 0x0100(小端序的 0x0001)。
- Write Response:服务端返回的确认包。如果没有这个包,说明写入没成功。
- Handle Value Notification:服务端主动发的数据包。如果前两步都对了,但这里没包,那就是服务端发送逻辑有问题。
我见过最离谱的情况是,客户端写的是 0x0001,但 Sniffer 显示写的是 0x0100。后来发现是 App 端字节序搞反了。嗯,这种问题光看代码很难发现,Sniffer 一抓就现原形。
3.5 整体流程示意图
下面这张图把整个 CCCD 配置到 Notification 发送的流程画清楚了。你保存下来,以后排查时对着看。
3.6 避坑指南(我踩过的坑)
最后,我把自己这些年踩过的坑总结一下。你遇到类似情况,直接对照排查。
- 坑一:CCCD 写成功了,但服务端没触发回调。
检查协议栈的 CCCD 写入回调注册接口。有些 SDK 需要你显式注册一个gatt_cccd_write_callback,否则写入事件会被协议栈默默吃掉。 - 坑二:Indication 的确认超时。
Indication 需要客户端回复确认包。如果客户端没回,服务端会一直等,导致后续数据发不出去。我建议你在服务端加一个超时重传机制,3 秒没收到确认就重发。 - 坑三:配对绑定后 CCCD 丢失。
有些设备在配对绑定后会重新初始化 GATT 服务,导致之前配好的 CCCD 被清空。解决办法是在配对完成后,让客户端重新配置一次 CCCD。 - 坑四:MTU 太小导致 Notification 发不出去。
如果特征值的数据长度超过 MTU,协议栈会拒绝发送。先协商 MTU,再发数据。我一般把 MTU 设到 247 字节以上。
好了,CCCD 这块的内容就这么多。你只要把客户端写入流程、服务端权限检查、以及日志分析这三块吃透,Notification/Indication 不生效的问题基本都能搞定。下次遇到类似问题,别急着改代码,先抓日志,再查权限,最后看流程——按这个顺序来,效率最高。