Hello Notify
通知推送 — BLE GATT Server/Client、CCCD、通知推送
前置阅读:Hello BLE
学习目标
- 理解 BLE GATT 的 Service → Characteristic → Descriptor 三层模型
- 掌握 Peripheral 端注册 GATT Service、添加 Characteristic、发送 Notify
- 掌握 Central 端服务发现、CCCD 使能通知、接收通知
- 理解 CCCD 的作用——Central 通过 CCCD 告知 Peripheral "我要收通知"
- 能够在 hello-connect 的基础上,让 Peripheral 向 Central 推送数据
规格与功能
| 规格项 | Peripheral 端 | Central 端 |
|---|---|---|
| GATT Service | 注册 Service + Characteristic + CCCD | 发现 Service → 匹配 Characteristic |
| 通知使能 | Central 写 CCCD=0x0001 后才可推送 | 发现后主动写 CCCD 使能通知 |
| 数据流向 | Peripheral → Central(单向推送) | 仅接收 |
| 发送内容 | "hello world" |
— |
程序运行流程:
- 连接建立(hello-connect 已完成的部分)
- Peripheral 注册 GATT Service + Characteristic(含 CCCD Descriptor)
- Central 发现服务 → 匹配 Characteristic UUID → 写 CCCD=0x0001
- Peripheral 收到 CCCD 写请求 → 发送 "hello world" Notify
- Central
notify_cb收到数据 → 串口打印
基本概念
GATT 模型 vs SSAP 模型
| 层级 | BLE GATT | SLE SSAP |
|---|---|---|
| 1 | Profile — 应用场景定义(如 HID Profile) | — (无对应) |
| 2 | Service — 功能模块(UUID 标识) | Service — 同 |
| 3 | Characteristic — 数据项(含 Value + Properties + Descriptors) | Property — 数据项 |
| 4 | Descriptor — 附加描述(含 CCCD 使能开关) | Descriptor — 附加描述 |
flowchart TD
S[Service<br/>UUID: 0x1800 自定义] --> C[Characteristic<br/>UUID: 0x2A00<br/>Properties: NOTIFY]
C --> D1[CCCD Descriptor<br/>UUID: 0x2902<br/>0x0001=Notify ON]
C --> D2["User Description<br/>UUID: 0x2901<br/>Hello Data"]
CCCD——BLE 的推送开关
这是 BLE 与 SLE 最大的差异之一。 BLE 中 Peripheral 不能直接推送数据——必须等 Central 写 CCCD 后才开始推送。SLE 中 Notify/Indicate 由 Property 的 operate_indication 预先决定,不需要对端使能。
sequenceDiagram
participant P as Peripheral
participant C as Central
Note over P: Service + Characteristic + CCCD 已注册<br/>CCCD 初始值 = 0x0000(通知关闭)
C->>P: 服务发现(找 Characteristic UUID)
C->>P: Write CCCD = 0x0001(通知开启)
Note over P: CCCD 置 0x0001<br/>Peripheral 可以推送了
P->>C: Notify: "hello world"
C->>C: notify_cb 收到数据
Characteristic Properties
| Property | 含义 | 本案例 |
|---|---|---|
READ |
Central 可读 | — |
WRITE |
Central 可写(带响应) | — |
WRITE_NO_RESPONSE |
Central 可写(无响应) | — |
NOTIFY |
Peripheral 可推送(无确认) | ✓ |
INDICATE |
Peripheral 可推送(需确认) | — |
涉及 API
| API | 谁调用 | 用途 |
|---|---|---|
gatts_add_service() |
Peripheral | 添加 GATT Service |
gatts_add_characteristic() |
Peripheral | 添加 Characteristic(含 CCCD) |
gatts_notify() |
Peripheral | 发送 Notify |
gattc_discover_service() |
Central | 发现所有 Service |
gattc_discover_characteristic() |
Central | 发现 Characteristic |
gattc_write() |
Central | 写 CCCD 使能通知 |
gattc_register_callbacks() |
Central | 注册回调(含 notify_cb) |
案例说明
Peripheral 注册 GATT Service + Characteristic,Central 发现服务 → 写 CCCD → 收到通知数据。
案例操作指导
Peripheral 先上电,Central 后上电。Central 串口输出:
[ble central] connected
[ble central] service discovered
[ble central] cccd written, waiting for notify...
[ble central] received: hello world
关键配置
- Service UUID:16-bit 自定义(基于
0000XXXX-0000-1000-8000-00805F9B34FBbase UUID) - Characteristic Property:
NOTIFY - CCCD 初始值:
0x0000(Central 写0x0001后开启)
UUID 编码对比
// BLE 16-bit UUID — 扩展到 128-bit base UUID
// 16-bit value 填入 base UUID 的 bytes 2~3
#define BT_UUID_16(value) \
{ .uuid128 = { (value >> 8) & 0xFF, value & 0xFF, \
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, \
0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB } }
BLE UUID 编码与 SLE 的
sle_uuid_setu2()不同——SLE 将 16-bit 值写入 base UUID 的 index 14-15,BLE 写入 bytes 2-3。两者 base UUID 也完全不同。
代码详解
Peripheral 端 GATT 服务注册
// 1. 定义 Service 和 Characteristic UUID
bt_uuid_t service_uuid = BT_UUID_16(0x1234);
bt_uuid_t char_uuid = BT_UUID_16(0x5678);
// 2. 添加 Service
gatt_service_t service = { .uuid = service_uuid, .type = PRIMARY_SERVICE };
gatts_add_service(&service, &g_service_handle);
// 3. 添加 Characteristic(含 CCCD Descriptor)
gatt_char_t characteristic = {
.uuid = char_uuid,
.properties = CHAR_PROPERTY_NOTIFY, // 仅 NOTIFY
.permissions = PERM_READ,
};
gatts_add_characteristic(g_service_handle, &characteristic, &g_char_handle);
// CCCD Descriptor 由协议栈自动添加在 Characteristic 后面
Central 端服务发现与 CCCD 使能
// 连接成功后 → 服务发现
gattc_discover_service(g_conn_id);
// 服务发现回调
static void discover_char_cb(uint16_t conn_id, const gatt_char_t *characteristic)
{
// 匹配 Characteristic UUID
if (bt_uuid_cmp(&characteristic->uuid, &g_target_char_uuid) == 0) {
g_target_char_handle = characteristic->handle;
// 找到 CCCD handle(通常是 char_handle + 1 或遍历 descriptor)
uint16_t cccd_handle = g_target_char_handle + 1;
// 写 CCCD = 0x0001(使能通知)
uint16_t cccd_value = 0x0001;
gattc_write(g_conn_id, cccd_handle,
(uint8_t *)&cccd_value, sizeof(cccd_value),
WRITE_TYPE_DEFAULT);
}
}
Peripheral 端发送通知
// Peripheral 在 CCCD 写回调中检测到使能后,发送数据
static void cccd_write_cb(uint16_t conn_id, uint16_t char_handle, uint16_t value)
{
if (value == 0x0001) { // 通知已开启
char *msg = "hello world";
gatts_notify(g_conn_id, g_char_handle,
(uint8_t *)msg, strlen(msg));
}
}
Central 端接收通知
static void notify_cb(uint16_t conn_id, uint16_t char_handle,
uint8_t *data, uint16_t data_len)
{
printf("[ble central] received: %.*s\n", data_len, data);
}