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" |
程序运行流程:
- Server 和 Client 建立连接(hello-connect 已完成的部分)
- Client 发起配对 → 配对成功
- 双方协商 MTU → MTU 交换完成
- Client 发现 Server 的服务结构 → 拿到 Property 句柄
- 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。
配对:数据交互的前提
在 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
图中每一阶段都有明确的职责:
- 配对:双方交换加密密钥,验证彼此身份。没有配对,协议栈会拒绝后续的 MTU 交换和所有数据操作
- MTU 协商:决定单次能传输的最大数据量。双方 MTU 不一致时取较小值,后续数据包不能超过这个大小
- 服务发现:Client 遍历 Server 的 Service 和 Property,拿到每个属性的 UUID 和句柄。只有知道 Property 句柄,Client 才能知道收到的数据对应哪个属性
- 通知: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:
这等于设置了 CONFIG_SAMPLE_SUPPORT_SLE_HELLO_SERVER_SAMPLE=y。
第二步:编译 Client 固件
同上,改为启用 Client:
这等于设置了 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, ¶meter);
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, ¶m);
}
type 取值 |
含义 | Client 回调 |
|---|---|---|
SSAP_PROPERTY_TYPE_VALUE |
通知(Notification) | notification_cb |
SSAP_PROPERTY_TYPE_VALUE_IND |
指示(Indication) | indication_cb |
接收通知(Client 端)
Client 在初始化时注册通知回调。sle_hello_notification_cb 在 sle_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 进度推送 | 通知 | 进度是连续的,丢一包只影响百分比显示,下次推送补上 |
| 开关/按键事件 | 指示 | 用户操作不能丢,必须确认送达 |
| 心跳/保活 | 通知 | 定期发送,下一包自然会到 |