第2章:特征读写属性与权限配置
2.1 概述
GATT 特征(Characteristic)的属性与权限配置是 BLE 服务端开发的核心环节,它决定了客户端能够以何种方式访问特征值,以及是否需要满足安全条件。本章详细讲解读写属性(Read / Write / Notify / Indicate)、安全权限(加密 / 配对)以及 CCCD(Client Characteristic Configuration Descriptor)配置的原理、实现流程与调试方法。
学习目标:掌握特征属性的声明语法,理解属性与权限的层级关系,能够独立完成 CCCD 的使能与同步,并排查常见配置错误。
2.2 读写属性原理与声明
2.2.1 属性类型及含义
每个 GATT 特征在声明时需指定一组属性标志(Properties),它们以位掩码形式组合,决定客户端对特征值的操作权限。
| 属性标志 | 掩码值 | 含义 | 典型用途 |
|---|---|---|---|
| Read | 0x02 | 允许客户端读取特征值 | 传感器数据、状态 |
| Write | 0x08 | 允许客户端写入特征值(需应答) | 控制命令、配置参数 |
| Write Without Response | 0x04 | 允许写入但无需应答 | 高速数据流、日志 |
| Notify | 0x10 | 服务器主动通知(无应用层应答) | 实时测量值 |
| Indicate | 0x20 | 服务器主动指示(需应用层应答) | 可靠事件上报 |
设计要点:Notify 与 Indicate 必须配合 CCCD 描述符才能生效;客户端通过写 CCCD 使能后,服务器方可发送数据。Write Without Response 适用于频繁小数据写入,但需自行处理流控。
2.2.2 属性组合规则
实际开发中,一个特征常组合多个属性。以下为常用组合模式:
- 只读(Read Only):传感器数值、设备信息。
- 读写(Read + Write):可配置参数。
- 通知使能(Read + Notify):允许客户端读取当前值并订阅更新。
- 指示使能(Read + Indicate):需要可靠确认的事件通知。
- 可写通知(Write + Notify):控制命令并回显状态。
2.3 安全权限配置
2.3.1 安全层级模型
BLE 安全定义了三级保护:
- 无安全(No Security):任何设备均可直接访问,无需加密或配对。
- 加密(Encryption):需配对并建立加密链路,未加密连接将被拒绝。
- 配对 + 加密 + MITM 保护:最高级别,要求配对过程具备中间人攻击防护能力。
| 安全级别 | 要求 | 适用场景 |
|---|---|---|
| 无 | 无 | 公开信息、调试 |
| 加密 | 已配对并加密 | 私有传感器数据 |
| 配对+加密+MITM | 带 MITM 保护的配对 | 门锁、支付、健康隐私 |
2.3.2 权限声明 API 示例
以下为 Zephyr RTOS 中声明特征属性及安全权限的典型代码:
/* 特征属性定义:读 + 通知 */
#define MY_CHARACTERISTIC_PROP (BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY)
/* 安全权限定义:加密读(仅允许加密连接读取) */
#define MY_CHAR_READ_PERM BT_GATT_PERM_READ_ENCRYPT
#define MY_CHAR_WRITE_PERM BT_GATT_PERM_WRITE_ENCRYPT
/* 特征声明宏 */
BT_GATT_CHARACTERISTIC(
BT_UUID_DECLARE_128(MY_CHAR_UUID),
MY_CHARACTERISTIC_PROP,
MY_CHAR_READ_PERM,
MY_CHAR_WRITE_PERM,
my_char_read_cb,
my_char_write_cb,
&my_char_value
);
权限粒度:权限可分别设置给读操作和写操作。例如允许未加密读,但加密写,以此平衡易用性与安全性。
2.4 CCCD 配置机制
2.4.1 CCCD 的作用
CCCD 是 GATT 标准描述符(UUID 0x2902),长度为 2 字节,位于特征声明之后。它控制服务器是否主动向客户端发送 Notify 或 Indicate 数据。
- bit 0:Notify 使能(1 开启 / 0 关闭)
- bit 1:Indicate 使能(1 开启 / 0 关闭)
- 注意:Notify 和 Indicate 不可同时使能。
2.4.2 CCCD 配置流程
- 客户端发现特征后,通过 Read By Type Request 找到 CCCD 句柄。
- 客户端向 CCCD 句柄写入 0x0001(使能 Notify)或 0x0002(使能 Indicate)。
- 服务器收到写请求后,更新内部 CCCD 标志位,并返回 Write Response。
- 服务器在后续事件中检查标志位,决定是否发送数据。
/* 客户端使能 Notify 的 ATT 命令示例 */
ATT Write Request:
Handle: 0x001A /* CCCD 句柄 */
Value: 0x0001 /* 使能 Notify */
ATT Write Response:
Handle: 0x001A /* 成功确认 */
2.4.3 服务器端 CCCD 状态同步
服务器必须在特征定义中预留 CCCD 存储空间,通常由 BLE 协议栈自动管理。但在某些平台上,需要手动处理 CCCD 的持久化与恢复,以确保设备重启后状态一致。
/* Zephyr 中为特征添加 CCCD 描述符 */
BT_GATT_DESCRIPTOR(
BT_UUID_GATT_CCC,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
NULL,
my_ccc_write_cb,
&my_ccc_value
);
常见陷阱:CCCD 状态仅保存在 RAM 中,设备断开连接或重启后,客户端必须重新使能 Notify/Indicate。如需持久化,开发者应自行将 CCCD 值写入 NVS 或 Flash。
2.5 调试与验证方法
2.5.1 使用 nRF Connect 验证属性
- 连接设备后,进入 GATT Client 视图,查看特征列表。
- 点击特征行,检查属性标志是否与声明一致(Read/Write/Notify 图标高亮)。
- 尝试读取特征值,观察是否弹出安全对话框(如需要配对)。
- 使能 CCCD Notify,向服务器发送触发事件,确认是否收到通知包。
2.5.2 日志关键点
服务器端应输出以下信息以辅助调试:
/* 建议日志输出 */
[D] GATT: characteristic declared UUID=xxxx, props=0x12
[D] GATT: CCC write handle=0x001A value=0x0001
[D] ATT: notify sent to client handle=0x001B len=8
[E] SEC: access denied – not encrypted
2.6 常见错误与解决方案
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 客户端读取特征值返回 0x0E(Not Supported) | 未声明 Read 属性 | 在属性标志中添加 BT_GATT_CHRC_READ |
| 写操作无响应(Write Timeout) | 未声明 Write 属性或权限拒绝 | 检查属性标志与安全权限配置 |
| 使能 Notify 后收不到通知 | CCCD 未正确写入或服务器未检查标志 | 监听 CCCD 写回调并更新内部状态 |
| 读操作弹出配对对话框但配对后仍失败 | 权限要求 MITM 保护但配对未开启 MITM | 使用 IO Capabilities 并启用 MITM 配对 |
| 属性标志包含 Notify 但客户端看不到 CCCD | 缺少 CCCD 描述符声明 | 在特征后添加 BT_GATT_DESCRIPTOR(BT_UUID_GATT_CCC, ...) |
2.7 性能与稳定性优化
- 减少属性组合的复杂性:一个特征只承担一种职责,避免同时启用 Notify 和 Indicate。
- 安全权限分层:高频读取的数据使用加密即可,无需 MITM,以降低配对耗时。
- CCCD 批量同步:若连接断开后恢复,建议客户端一次性写回所有 CCCD,避免逐个配置产生 ATT 流量风暴。
- 缓存 CCCD 状态:在服务器端 NVS 中缓存 CCCD 值,重启后自动恢复,减少客户端重配置步骤。
- 使用 bonded 设备信息:利用 BLE bonding 存储 CCCD 状态,协议栈可自动恢复通知使能。
本章小结:特征的读写属性、安全权限与 CCCD 配置共同构成了 GATT 服务端数据访问的完整控制面。属性决定操作类型,权限决定安全门槛,CCCD 决定主动推送开关。三者独立配置、协同工作,理解其原理是构建可靠 BLE 应用的基石。