跳转至

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"

程序运行流程:

  1. 连接建立(hello-connect 已完成的部分)
  2. Peripheral 注册 GATT Service + Characteristic(含 CCCD Descriptor)
  3. Central 发现服务 → 匹配 Characteristic UUID → 写 CCCD=0x0001
  4. Peripheral 收到 CCCD 写请求 → 发送 "hello world" Notify
  5. 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-00805F9B34FB base 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);
}