跳转至

Hello Notify

通知推送 — SLE SSAP 服务模型、通知/指示、配对

前置阅读:Hello SLE

学习目标

  • 理解 SSAP 的 Server → Service → Property → Descriptor 四级模型
  • 掌握 Server 端注册服务、添加可通知属性、发送通知的流程
  • 掌握 Client 端配对、MTU 交换、服务发现、接收通知的流程
  • 理解通知(Notification,无需确认)与指示(Indication,需确认)的区别和适用场景
  • 理解通知推送的单向特性(Server → Client),知道何时用它、何时需要双向读写
  • 能够在 hello-connect 的基础上,让 Server 向 Client 推送 "hello world"

规格与功能

本案例在 hello-connect 的连接基础上,实现 Server 向 Client 的单向数据推送

规格项 Server 端 Client 端
配对 被动等待配对 连接成功后主动发起配对
MTU 配对完成后设置 520 字节 配对完成后发起 MTU 交换
服务发现 遍历 Server 的 Service / Property / Descriptor
数据流向 Server → Client(单向推送) 仅接收,不发送
发送方式 Notification(通知,本案例使用)
发送内容 "hello world"(11 字节)
接收确认 打印 "Received: hello world"

程序运行流程:

  1. Server 和 Client 建立连接(hello-connect 已完成的部分)
  2. Client 发起配对 → 配对成功
  3. 双方协商 MTU → MTU 交换完成
  4. Client 发现 Server 的服务结构 → 拿到 Property 句柄
  5. Server 发送 "hello world" 通知 → Client 接收并打印

hello-connect 停在了"已连接"状态。本篇告诉你连接之后怎么把数据从一端推到另一端。

基本概念

典型使用场景

通知推送是 SLE 最常用的数据交互模式——Server 是数据产生方,Client 是数据消费方,Server 有新数据时主动推给 Client,Client 无需轮询。例如:

  • 传感器定时上报温度/湿度数据
  • 设备状态变化通知(按键被按下、电量低于阈值)
  • 固件升级进度推送(每完成 10% 通知一次)
  • 告警信息即时推送(非法开门、超温报警)

通知是单向的(Server → Client)。如果还需要 Client 主动读/写 Server 属性,那是下一篇 hello-readwrite 的内容。

SSAP 模型:四级结构

SSAP(SLE Service Access Protocol)定义了数据交互的组织方式,分为四级:

flowchart TD
    S[Server<br/>一个设备注册一个 Server] --> SV[Service<br/>一组相关功能的集合, 如: 温度服务]
    SV --> P[Property<br/>一个具体的数据项, 如: 当前温度值]
    P --> D[Descriptor<br/>属性的附加描述, 如: 温度单位是摄氏度]
层级 职责 类比
Server 顶层容器,代表一个数据提供者。调用 ssaps_register_server() 注册 一个设备上的数据服务
Service 一组相关功能的集合。通过 ssaps_add_service_sync() 添加,用 UUID 唯一标识 一个功能模块(如"温度服务")
Property 一个具体的数据项,是数据读写的实际对象。通过 ssaps_add_property_sync() 添加 一个数据字段(如"当前温度")
Descriptor 属性的附加说明信息,通过 ssaps_add_descriptor_sync() 添加 数据字段的注释(如"单位: ℃")

理解这个层级关系是后续所有数据操作的基础——发送通知要指定 Property 句柄,读写数据也要指定 Property 句柄。

UUID:服务的唯一标识

UUID 用于唯一标识一个 Service 或 Property。SSAP 支持两种格式:

格式 长度 适用场景
16bit UUID 2 字节 自定义服务,资源占用小,适合嵌入式设备
128bit UUID 16 字节 标准化服务(如 HID、心率),需全局唯一

本案例使用 16bit UUID:Service UUID 为 0x3333,Property UUID 为 0x3434

#define SLE_HELLO_SERVICE_UUID    0x3333
#define SLE_HELLO_NTF_REPORT_UUID 0x3434

配对:数据交互的前提

在 hello-connect 中,Client 连接成功后只是打印了一句 "connected"。为什么不直接发数据?因为 SLE 要求先配对才能进行服务发现和数据交互

配对(Pair)是连接建立后的安全握手——双方交换加密密钥,验证彼此身份。配对成功后,协议栈才会放行 SSAP 服务发现和数据传输。

Client 在 connect_state_changed_cbk 中检测到已连接且未配对时,立即发起配对:

if (conn_state == SLE_ACB_STATE_CONNECTED) {
    if (pair_state == SLE_PAIR_NONE) {
        sle_pair_remote_device(&g_sle_hello_remote_addr);  // 发起配对
    }
}

配对完成后触发双方的 pair_complete_cb——这是数据交互的真正起点。

MTU 协商:决定单次能传多少

MTU(Maximum Transmission Unit)是单次能传输的最大数据量。配对完成后双方各自设置 MTU:

  • Server 端:在 pair_complete_cb 中调用 ssaps_set_info() 设置 MTU
  • Client 端:在 pair_complete_cb 中调用 ssapc_exchange_info_req() 发起 MTU 交换
sequenceDiagram
    participant S as Server
    participant C as Client
    C->>S: 配对请求
    S-->>C: 配对响应
    Note right of S: pair_complete_cb, ssaps_set_info(mtu=520)
    Note right of C: pair_complete_cb, ssapc_exchange_info_req(mtu=520)
    C->>S: MTU 交换请求
    S-->>C: MTU 交换响应
    Note over S,C: 后续数据包最多 520 字节

本案例 MTU 设为 520 字节。"hello world" 只需 11 字节,但 520 为后续传输更大数据留有余地。

通知 vs 指示

Server 通过 ssaps_notify_indicate() 发送数据时,可以选择通知或指示两种方式:

对比项 通知(Notification) 指示(Indication)
接收方确认 不需要 必须确认
速度 快,可连续发送 较慢,发送下一包前需等确认
可靠性 不保证送达(链路层重传除外) 保证送达
适用场景 传感器数据、进度更新(丢一两包影响不大) 告警、关键控制指令(每包都必须到)
Client 回调 notification_cb indication_cb

本案例使用通知——"hello world" 丢了对演示无影响。如果要发送关键指令,改为指示即可。

Property 权限与操作指示

每个 Property 有两个独立配置:

配置 含义 本案例取值
permissions 允许对端执行哪些操作 SSAP_PERMISSION_READ(允许读)
operate_indication 本端支持哪些操作方式 READ \| NOTIFY(支持读 + 通知)

permissions 是给对端的授权,operate_indication 是告诉对端"我有这些能力"。两者不相等:

  • 一个 Property 可以 permissions = READ,但 operate_indication = READ | NOTIFY——意思是"允许你读,而且我会主动通知你"
  • 也可以 permissions = READ | WRITE,但 operate_indication 只有 READ——意思是"你可以读写,但我不会主动通知"
#define SLE_HELLO_TEST_PROPERTIES            (SSAP_PERMISSION_READ)
#define SLE_HELLO_TEST_OPERATION_INDICATION  (SSAP_OPERATE_INDICATION_BIT_READ | \
                                               SSAP_OPERATE_INDICATION_BIT_NOTIFY)

通信流程: 通知推送

通知推送不是连接建立后就能直接做的——中间还需要依次完成配对、MTU 协商和服务发现三个阶段。下图展示从连接建立到第一条通知抵达的完整链路:

sequenceDiagram
    participant S as Server
    participant C as Client

    Note over S,C: 已连接
    C->>S: 配对请求
    S-->>C: 配对响应
    Note over S,C: 已配对
    C->>S: MTU 协商
    S-->>C: MTU 确认
    Note over S,C: MTU 一致
    C->>S: 服务发现
    S-->>C: Service 和 Property 列表
    Note over S,C: 可通信
    S->>C: 通知 Notification

图中每一阶段都有明确的职责:

  1. 配对:双方交换加密密钥,验证彼此身份。没有配对,协议栈会拒绝后续的 MTU 交换和所有数据操作
  2. MTU 协商:决定单次能传输的最大数据量。双方 MTU 不一致时取较小值,后续数据包不能超过这个大小
  3. 服务发现:Client 遍历 Server 的 Service 和 Property,拿到每个属性的 UUID 和句柄。只有知道 Property 句柄,Client 才能知道收到的数据对应哪个属性
  4. 通知:Server 通过指定 Property 句柄发送数据,Client 在对应的回调中接收

配对 → MTU → 服务发现 → 通知,这四步缺一不可。常见错误是连接成功后直接调 ssaps_notify_indicate()——会失败,因为还没配对。

涉及 API

本案例聚焦数据交互,新增了 SSAP 相关的 12 个 API:

API 谁调用 用途
sle_pair_remote_device() Client 发起配对
ssaps_register_server() Server 注册 SSAP Server
ssaps_add_service_sync() Server 添加 Service(同步)
ssaps_add_property_sync() Server 添加 Property(同步)
ssaps_add_descriptor_sync() Server 添加 Descriptor(同步)
ssaps_start_service() Server 启动 Service
ssaps_set_info() Server 设置 MTU 等连接参数
ssaps_notify_indicate() Server 发送通知/指示
ssapc_exchange_info_req() Client 发起 MTU 交换
ssapc_find_structure() Client 发起服务发现
ssapc_register_callbacks() Client 注册 SSAP Client 回调
ssaps_register_callbacks() Server 注册 SSAP Server 回调

加上 hello-connect 的 9 个 API,hello 系列共涉及 21 个 API,覆盖了 SLE 开发中最常用的一批接口。

案例说明

做什么

在连接建立的基础上,让 Server 向 Client 推送一条 "hello world" 消息。

这是单向推送:数据只从 Server 流向 Client。这是物联网最常见的模式——设备主动上报数据,网关/手机被动接收。如果需要 Client 主动读/写 Server 属性,那是下一篇 hello-readwrite 的内容。

案例流程说明

sequenceDiagram
    participant S as Server
    participant C as Client
    Note over S: 注册 SSAP Server<br/>添加 Service/Property/Descriptor<br/>配置广播, 启动广播
    Note over C: 注册扫描/连接/SSAP 回调<br/>启动扫描
    loop 广播与连接阶段 (hello-connect)
        S->>C: 广播, 扫描匹配, 连接
    end
    Note over S,C: 已连接
    C->>S: 配对请求
    S-->>C: 配对响应
    Note right of S: pair_complete_cb, 设置 MTU
    Note right of C: pair_complete_cb, 发起 MTU 交换
    C->>S: MTU 交换
    S-->>C: MTU 确认
    Note right of C: exchange_info_cb, 发起服务发现
    C->>S: 服务发现
    S-->>C: 返回 Service/Property 信息
    Note right of C: find_structure_cmp_cb, 发现完成
    S->>C: Notification: hello world
    Note right of C: notification_cb, Received: hello world

如何区分"连上了"和"能传数据了"

很多初学者容易犯一个错误——在 SLE_ACB_STATE_CONNECTED 回调里直接调用 ssaps_notify_indicate(),结果发送失败。原因是:连接建立不等于可以传数据

必须依次完成:配对 → MTU 交换 → 服务发现(Client 端)。这三个步骤全部完成后,才能正常收发数据。代码详解部分会展示每一步的依赖关系。

案例操作指导

第一步:编译 Server 固件

打开 menuconfig,启用 Server:

Top → Application → Samples → BT → SLE → SLE Hello → [*] SLE Hello Server Sample

这等于设置了 CONFIG_SAMPLE_SUPPORT_SLE_HELLO_SERVER_SAMPLE=y

fbb build ws63-liteos-app -p menuconfig
fbb build ws63-liteos-app

第二步:编译 Client 固件

同上,改为启用 Client:

Top → Application → Samples → BT → SLE → SLE Hello → [*] SLE Hello Client Sample

这等于设置了 CONFIG_SAMPLE_SUPPORT_SLE_HELLO_CLIENT_SAMPLE=y

第三步:烧录

将 Server 固件烧录到板子 A,Client 固件烧录到板子 B。

第四步:上电运行

先给 Server 上电,预期串口输出:

[sle hello server] init ok
[sle hello server] start announce success.
[sle hello server] waiting for connection...
[sle hello server] connected, conn_id=0x01
[sle hello server] pair complete ...
[sle hello server] sending hello world...
[sle hello server] hello world sent.

再给 Client 上电,预期串口输出:

[sle hello client] init...
[sle hello client] start seek...
[sle hello client] scan data: hello_server
[sle hello client] found hello_server, stopping seek...
[sle hello client] connecting to remote device...
[sle hello client] connected, conn_id=0x01
[sle hello client] pair complete ...
[sle hello client] exchange info req sent.
[sle hello client] find structure cmp cbk ...
[sle hello client] service discovery complete, ready to receive data.
========================================
[SLE Hello Client] Received: hello world
========================================

第五步:验证通知推送

Client 端收到 Received: hello world 即表示通知推送成功。如果只有 connected 没有 Received,通常是配对或 MTU 交换出了问题——检查双方 pair_complete_cb 是否被触发。

关键配置

Server 端 Service / Property 参数

/* Service UUID: 0x3333 — 自定义 16bit UUID */
#define SLE_HELLO_SERVICE_UUID        0x3333

/* Property UUID: 0x3434 — 唯一标识"hello world 消息"这个属性 */
#define SLE_HELLO_NTF_REPORT_UUID     0x3434

/* Property 权限:允许对端读取 */
#define SLE_HELLO_TEST_PROPERTIES  (SSAP_PERMISSION_READ)

/* 操作指示:支持读 + 通知 */
#define SLE_HELLO_TEST_OPERATION_INDICATION  \
    (SSAP_OPERATE_INDICATION_BIT_READ | SSAP_OPERATE_INDICATION_BIT_NOTIFY)

/* Descriptor 权限:允许对端读取(查看描述信息) */
#define SLE_HELLO_TEST_DESCRIPTOR   (SSAP_PERMISSION_READ)

可配置参数速查

参数 当前值 说明 调大影响 调小影响
MTU 大小 520 单次最大传输字节 单包可传更多数据,但占用更多内存 内存省,大消息需分包
通知/指示选择 Notification type = SSAP_PROPERTY_TYPE_VALUE

代码详解

入口与任务创建

与 hello-connect 完全相同——app_run(sle_hello_entry) 创建 task,Kconfig 区分 Server/Client。Server 和 Client 的初始化代码也保持不变,因为它们从 sle_hello_server_init() / sle_hello_client_init() 开始就包含了通知推送的完整流程。

SSAP 模型构建(Server 端)

下面是 Server 端构建 Service → Property → Descriptor 的完整代码:

static errcode_t sle_hello_server_add(void)
{
    /* 1. 注册 Server */
    sle_uuid_t app_uuid = { .len = 2, .uuid = { 0x12, 0x34 } };
    ssaps_register_server(&app_uuid, &g_server_id);

    /* 2. 添加 Service */
    sle_uuid_t service_uuid = {0};
    sle_uuid_setu2(SLE_HELLO_SERVICE_UUID, &service_uuid);  // UUID = 0x3333
    ssaps_add_service_sync(g_server_id, &service_uuid, 1, &g_service_handle);

    /* 3. 添加 Property */
    ssaps_property_info_t property = {0};
    property.permissions = SLE_HELLO_TEST_PROPERTIES;           // 允许读
    property.operate_indication = SLE_HELLO_TEST_OPERATION_INDICATION; // 读+通知
    sle_uuid_setu2(SLE_HELLO_NTF_REPORT_UUID, &property.uuid); // UUID = 0x3434
    property.value = (uint8_t *)osal_vmalloc(sizeof(g_sle_hello_property_value));
    ssaps_add_property_sync(g_server_id, g_service_handle, &property, &g_property_handle);

    /* 4. 添加 Descriptor */
    ssaps_desc_info_t descriptor = {0};
    descriptor.permissions = SLE_HELLO_TEST_DESCRIPTOR;     // 允许读
    descriptor.type = SSAP_DESCRIPTOR_USER_DESCRIPTION;     // 用户描述符类型
    descriptor.operate_indication = SSAP_OPERATE_INDICATION_BIT_READ;
    uint8_t ntf_value[] = { 0x01, 0x00 };                  // 描述符值:通知已启用
    descriptor.value = ntf_value;
    descriptor.value_len = sizeof(ntf_value);
    ssaps_add_descriptor_sync(g_server_id, g_service_handle, g_property_handle, &descriptor);

    /* 5. 启动 Service */
    ssaps_start_service(g_server_id, g_service_handle);
    return ERRCODE_SLE_SUCCESS;
}

代码中的 _sync 后缀表示同步调用——函数会等待协议栈处理完毕才返回。这简化了代码逻辑,调用顺序即执行顺序。

配对与 MTU 协商

配对是数据交互的触发点。连接成功后,Client 发起配对:

/* Client: 连接成功后立即配对 */
if (conn_state == SLE_ACB_STATE_CONNECTED) {
    if (pair_state == SLE_PAIR_NONE) {
        sle_pair_remote_device(&g_sle_hello_remote_addr);
    }
}

配对完成后,双方各自设置 MTU:

/* Server: 配对完成后设置 MTU,然后发送 hello world */
static void sle_hello_pair_complete_cbk(uint16_t conn_id, const sle_addr_t *addr,
                                         errcode_t status)
{
    ssap_exchange_info_t parameter = { 0 };
    parameter.mtu_size = 520;
    parameter.version = 1;
    ssaps_set_info(g_server_id, &parameter);

    sle_hello_server_send_data(g_hello_msg, sizeof(g_hello_msg) - 1);
}
/* Client: 配对完成后发起 MTU 交换 */
static void sle_hello_pair_complete_cbk(uint16_t conn_id, const sle_addr_t *addr,
                                         errcode_t status)
{
    if (status == 0) {
        ssap_exchange_info_t info = {0};
        info.mtu_size = 520;
        info.version = 1;
        ssapc_exchange_info_req(0, g_sle_hello_conn_id, &info);
    }
}

服务发现(Client 端)

MTU 交换完成后,Client 需要遍历 Server 的服务结构,拿到 Property 句柄(后续收数据时需要定位是哪个 Property 发来的):

/* exchange_info_cb:MTU 交换完成 → 开始服务发现 */
static void sle_hello_exchange_info_cbk(uint8_t client_id, uint16_t conn_id,
                                         ssap_exchange_info_t *param, errcode_t status)
{
    ssapc_find_structure_param_t find_param = { 0 };
    find_param.type = SSAP_FIND_TYPE_PROPERTY;
    find_param.start_hdl = 1;
    find_param.end_hdl = 0xFFFF;
    ssapc_find_structure(0, conn_id, &find_param);
}

/* find_structure_cb:每发现一个 Service/Property/Descriptor 触发一次 */
static void sle_hello_find_structure_cbk(uint8_t client_id, uint16_t conn_id,
                                          ssapc_find_service_result_t *service, errcode_t status)
{
    /* 保存发现的 Service 信息(句柄范围、UUID) */
    g_sle_hello_find_service_result.start_hdl = service->start_hdl;
    g_sle_hello_find_service_result.end_hdl = service->end_hdl;
    memcpy_s(&g_sle_hello_find_service_result.uuid, ... , &service->uuid, ...);
}

/* find_structure_cmp_cb:发现完成。此时可以开始接收数据了 */
static void sle_hello_find_structure_cmp_cbk(...)
{
    osal_printk("service discovery complete, ready to receive data.\r\n");
}

find_structure_cb 可能触发多次(每个 Property 一次),find_structure_cmp_cb 只触发一次(全量发现完成)。

发送通知(Server 端)

ssaps_notify_indicate() 的参数 type 决定了发送方式是通知还是指示:

errcode_t sle_hello_server_send_data(const uint8_t *data, uint16_t len)
{
    ssaps_ntf_ind_t param = {0};
    param.handle = g_property_handle;          // 指定通过哪个 Property 发送
    param.type = SSAP_PROPERTY_TYPE_VALUE;     // "值通知" → 使用 Notification
    param.value = send_buf;
    param.value_len = len;
    return ssaps_notify_indicate(g_server_id, g_sle_conn_hdl, &param);
}
type 取值 含义 Client 回调
SSAP_PROPERTY_TYPE_VALUE 通知(Notification) notification_cb
SSAP_PROPERTY_TYPE_VALUE_IND 指示(Indication) indication_cb

接收通知(Client 端)

Client 在初始化时注册通知回调。sle_hello_notification_cbsle_hello.c 中定义,通过 sle_hello_client_init(notification_cb, indication_cb) 传入:

static void sle_hello_notification_cb(uint8_t client_id, uint16_t conn_id,
                                       ssapc_handle_value_t *data, errcode_t status)
{
    osal_printk("[SLE Hello Client] Received: %s\r\n", data->data);
}

data->handle 告诉你数据来自哪个 Property。如果 Server 有多个可通知属性,通过 handle 区分数据来源。

通知 vs 指示的选择指南

场景 推荐方式 理由
传感器定时上报(温湿度、光照) 通知 数据频繁,偶尔丢一包无影响,通知速度快
电量低于阈值告警 指示 必须确保对端收到,否则可能错过关键操作
OTA 进度推送 通知 进度是连续的,丢一包只影响百分比显示,下次推送补上
开关/按键事件 指示 用户操作不能丢,必须确认送达
心跳/保活 通知 定期发送,下一包自然会到