Hello ReadWrite
读写交互 — SLE SSAP 属性读写、请求-响应模式
前置阅读:Hello SLE、Hello Notify
学习目标
- 理解通知推送与读写交互的本质区别:推送是 Server 主动,读写是 Client 主动
- 掌握 Server 端实现
read_request_cb和write_request_cb来处理 Client 的读写请求 - 掌握 Client 端通过
ssapc_read_req()/ssapc_write_req()主动读写属性 - 理解 Property 权限配置对读写操作的实际约束(权限不足时操作会被拒绝)
- 能够在 hello-notify 的基础上,让 Client 主动读取和写入 Server 的属性
规格与功能
本案例在 hello-notify 的基础上新增 Client 主动读写的能力。Server 维护一个可读可写的属性,Client 先读取当前值,再写入新值。
| 规格项 | Server 端 | Client 端 |
|---|---|---|
| 配对 | 被动等待配对 | 连接成功后主动发起配对 |
| MTU | 配对完成后设置 520 字节 | 配对完成后发起 MTU 交换 |
| 服务发现 | — | 遍历 Server 的 Service / Property / Descriptor |
| 通知推送 | 连接成功后发送 "hello world" | 接收通知(hello-notify 的能力,本案例保留) |
| 属性读取 | 收到读请求 → 返回属性当前值 | 主动发起读请求 → read_cfm_cb 中获取返回值 |
| 属性写入 | 收到写请求 → 校验并更新 → 返回写入成功 | 主动发起写请求 → write_cfm_cb 确认写入完成 |
程序运行流程:
- Server 和 Client 建立连接(hello-connect)
- 配对 → MTU → 服务发现(hello-notify)
- Server 发送 "hello world" 通知(hello-notify)
- Client 主动读取 Server 的属性值 → Server 返回当前值
- Client 主动写入新值到 Server 的属性 → Server 确认写入
hello-notify 讲的是"Server 推数据给 Client"。本篇讲的是反过来的——"Client 主动找 Server 要数据、改数据"。
基本概念
典型使用场景
通知推送覆盖了"传感器主动上报",但还有很多场景需要 Client 主动发起交互:
- Client 读取 Server 的设备信息(固件版本、序列号、运行状态)
- Client 下发配置指令(设置上报间隔、开关某个功能、调节参数)
- Client 写入数据(门禁密码、OTA 固件包分片)
推送 vs 读写对比
SLE 提供了四种数据交互方式,它们的本质区别在于"谁发起"和"数据往哪流":
| 模式 | 方向 | 发起方 | Server 回调 | Client 回调 | 典型场景 |
|---|---|---|---|---|---|
| 通知 | Server → Client | Server | — | notification_cb |
传感器定时上报 |
| 指示 | Server → Client | Server | — | indication_cb |
告警推送(需确认) |
| 读 | Client → Server → Client | Client | read_request_cb |
read_cfm_cb |
查询设备信息 |
| 写 | Client → Server | Client | write_request_cb |
write_cfm_cb |
下发指令/配置 |
前两种是上一篇 hello-notify 的内容。本篇聚焦后两种——读和写。
Property 权限与操作指示
每个 Property 有两个独立配置,它们容易被混淆但职责完全不同:
| 配置 | 字段 | 含义 | 作用对象 |
|---|---|---|---|
permissions |
ssap_permission_t |
授权对端可以对这个 Property 做什么 | 对端设备 |
operate_indication |
ssap_operate_indication_t |
声明本端具备哪些操作能力 | 本端自身 |
简单来说:permissions 是"我允许你做什么",operate_indication 是"我能做什么"。
permissions 全部取值
permissions 是 ssap_permission_t 枚举的位掩码组合,各 bit 可同时设置:
| 枚举值 | 位值 | 含义 |
|---|---|---|
SSAP_PERMISSION_READ |
0x01 |
允许对端读取该属性 |
SSAP_PERMISSION_WRITE |
0x02 |
允许对端写入该属性 |
SSAP_PERMISSION_ENCRYPTION_NEED |
0x04 |
操作该属性需要链路加密 |
SSAP_PERMISSION_AUTHENTICATION_NEED |
0x08 |
操作该属性需要设备已认证 |
SSAP_PERMISSION_AUTHORIZATION_NEED |
0x10 |
操作该属性需要用户已授权 |
前两个(READ / WRITE)控制数据交互的开关,后三个(加密 / 认证 / 授权)控制安全等级。本案例及 hello-notify 均未涉及安全等级,实际产品中建议按需启用。
operate_indication 全部取值
operate_indication 是 ssap_operate_indication_t 枚举的位掩码组合:
| 枚举值 | 位值 | 含义 |
|---|---|---|
SSAP_OPERATE_INDICATION_BIT_READ |
0x01 |
本端支持被对端读取 |
SSAP_OPERATE_INDICATION_BIT_WRITE_NO_RSP |
0x02 |
本端支持被对端写入(写入后不回复确认) |
SSAP_OPERATE_INDICATION_BIT_WRITE |
0x04 |
本端支持被对端写入(写入后回复确认) |
SSAP_OPERATE_INDICATION_BIT_NOTIFY |
0x08 |
本端支持通过通知向对端推送数据 |
SSAP_OPERATE_INDICATION_BIT_INDICATE |
0x10 |
本端支持通过指示向对端推送数据(需确认) |
SSAP_OPERATE_INDICATION_BIT_BROADCAST |
0x20 |
本端支持在广播包中携带该属性值 |
SSAP_OPERATE_INDICATION_BIT_DESCRITOR_WRITE |
0x100 |
本端允许对端写入该属性的用户描述符 |
SSAP_OPERATE_INDICATION_BIT_DESCRIPTOR_CLIENT_CONFIGURATION_WRITE |
0x200 |
本端允许对端写入客户端配置描述符 |
SSAP_OPERATE_INDICATION_BIT_DESCRIPTOR_SERVER_CONFIGURATION_WRITE |
0x400 |
本端允许对端写入服务端配置描述符 |
前 6 个(
0x01~0x20)覆盖了属性值的基本操作,后 3 个(0x100~0x400)是描述符级别的控制,日常使用较少。
两个配置的配合关系
下面通过几个例子说明 permissions 和 operate_indication 如何配合:
| 场景 | permissions | operate_indication | 效果 |
|---|---|---|---|
| 只让对端读,本端只推通知 | READ |
READ \| NOTIFY |
对端只能读;本端不需要对端确认时发通知 |
| 只让对端读,本端推关键告警 | READ |
READ \| INDICATE |
对端只能读;本端需要确认时发指示 |
| 开放读写,本端不主动推 | READ \| WRITE |
READ \| WRITE |
对端可读写;本端只响应请求,不主动推送 |
| 开放读写通知(本案例) | READ \| WRITE |
READ \| WRITE \| NOTIFY |
对端可读写;本端还能主动推通知 |
关键点:
WRITE_NO_RSP(写入不回复)和WRITE(写入后回复确认)的区别。本案例使用WRITE,Server 在write_request_cb中必须调用ssaps_send_response()回复确认;如果使用WRITE_NO_RSP,Server 无需回复,但也拿不到 Client 的写入确认。
权限如何影响读写回调
read_request_cb能被触发的前提:Property 的permissions包含SSAP_PERMISSION_READwrite_request_cb能被触发的前提:Property 的permissions包含SSAP_PERMISSION_WRITE
权限不足时,协议栈在收到请求后直接拒绝,Server 端的回调不会被触发。因此,如果读写测试失败,首先检查权限配置是否正确。
本案例的配置变更
hello-notify 中只开放了读权限和通知能力。hello-readwrite 在此基础上增加写权限和写能力:
/* hello-notify: 仅可读, 仅支持读 + 通知 */
#define SLE_HELLO_TEST_PROPERTIES (SSAP_PERMISSION_READ)
#define SLE_HELLO_TEST_OPERATION_INDICATION (SSAP_OPERATE_INDICATION_BIT_READ | \
SSAP_OPERATE_INDICATION_BIT_NOTIFY)
/* hello-readwrite: 可读可写, 支持读 + 通知 + 写 */
#define SLE_HELLO_TEST_PROPERTIES (SSAP_PERMISSION_READ | SSAP_PERMISSION_WRITE)
#define SLE_HELLO_TEST_OPERATION_INDICATION (SSAP_OPERATE_INDICATION_BIT_READ | \
SSAP_OPERATE_INDICATION_BIT_NOTIFY | \
SSAP_OPERATE_INDICATION_BIT_WRITE)
通信流程: 读写交互
与通知的单向推送不同,读和写是请求-响应模式:Client 发出请求,Server 收到后处理并返回结果,Client 在回调中拿到结果。
下图展示从"已连接可通信"开始,Client 发起一次读和一次写的完整流程:
sequenceDiagram
participant C as Client
participant S as Server
Note over S,C: 已连接可通信, 已完成配对、MTU 协商、服务发现
C->>S: 读请求, 指定 handle 和 type
Note right of S: read_request_cb, 根据 handle 查询属性值
S-->>C: 读响应, 返回属性当前值
Note right of C: read_cfm_cb, 拿到返回值
C->>S: 写请求, 携带 handle、type、value
Note right of S: write_request_cb, 校验数据, 更新属性值
S-->>C: 写响应, 返回写入结果
Note right of C: write_cfm_cb, 确认写入完成
图中各阶段的职责:
- 读请求:Client 指定属性句柄发起读请求,Server 在
read_request_cb中根据句柄找到对应数据,通过ssaps_send_response()返回。Client 在read_cfm_cb中拿到返回值 - 写请求:Client 指定属性句柄和数据发起写请求,Server 在
write_request_cb中校验合法性并更新本地数据,通过ssaps_send_response()确认。Client 在write_cfm_cb中拿到写入结果
涉及 API
本案例新增了 3 个读写相关 API:
| API | 谁调用 | 用途 |
|---|---|---|
ssapc_read_req() |
Client | 发起属性读请求 |
ssapc_write_req() |
Client | 发起属性写请求 |
ssaps_send_response() |
Server | 响应读/写请求(统一入口,通过 ssaps_send_rsp_t 携带数据) |
此外,前两篇用过的 ssaps_register_callbacks() 和 ssapc_register_callbacks() 中新增了回调字段:Server 端新增 read_request_cb / write_request_cb,Client 端新增 read_cfm_cb / write_cfm_cb。
前两篇的 API 不变,本篇新增 3 个。hello 三部曲覆盖了 24 个核心 API,构成 SLE 应用开发的最小完整集合。
案例说明
做什么
在 hello-notify 的基础上,让 Client 主动与 Server 进行双向数据交互:先读一个属性,再写一个新值进去。
与 hello-notify 的关键区别:hello-notify 是 Server 主动推,Client 被动收;本篇是 Client 主动发起,Server 被动响应。两者组合起来,覆盖了物联网设备中最常见的三种数据交互模式。
案例流程说明
sequenceDiagram
participant S as Server
participant C as Client
Note over S: 注册 SSAP Server<br/>添加 Service/Property/Descriptor<br/>permissions = READ | WRITE<br/>operate_indication = READ | NOTIFY | WRITE
Note over S: 配置广播, 启动广播
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
C->>S: 读请求 (handle=property_handle)
Note right of S: read_request_cb
S-->>C: 读响应, value=device_status
Note right of C: read_cfm_cb, 打印读取到的值
C->>S: 写请求 (handle=property_handle, new_value)
Note right of S: write_request_cb, 更新属性值
S-->>C: 写响应, status=success
Note right of C: write_cfm_cb, 写入成功
与前两篇的关系
| 篇 | 主题 | 数据方向 | 谁主动 |
|---|---|---|---|
| hello-connect | 广播与连接 | — | — |
| hello-notify | 通知推送 | Server → Client | Server |
| hello-readwrite | 读写交互 | Client ↔ Server | Client |
三部曲覆盖了 SLE 开发的三块基石:连接建立、数据推送、数据读写。掌握这三篇,后续任何功能(OTA、HID、透传)都是在这三个基础之上的组合。
案例操作指导
第一步:编译 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.
[sle hello server] read request received, handle=0x0010
[sle hello server] read response sent: value=device_status_ok
[sle hello server] write request received, handle=0x0010
[sle hello server] write response sent: success
再给 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
========================================
[sle hello client] read result: device_status_ok
[sle hello client] write cfm: success
第五步:验证读写交互
Client 端看到 read result 和 write cfm: success 即表示读写交互成功。如果只看到通知推送但没有读写结果,检查 Server 端 read_request_cb / write_request_cb 是否正确注册,以及 Property 权限是否包含 SSAP_PERMISSION_READ | SSAP_PERMISSION_WRITE。
关键配置
与 hello-notify 相比,本案例的配置变更汇总如下:
可配置参数速查
| 参数 | hello-notify 取值 | hello-readwrite 取值 | 说明 |
|---|---|---|---|
| Property 权限 | READ |
READ \| WRITE |
新增写权限,允许 Client 写入 |
| 操作指示 | READ \| NOTIFY |
READ \| NOTIFY \| WRITE |
新增写声明 |
| Server 回调 | NULL |
read_request_cb |
从 NULL 改为实际处理函数 |
| Server 回调 | NULL |
write_request_cb |
从 NULL 改为实际处理函数 |
| Client 回调 | — | read_cfm_cb |
新增读结果回调 |
| Client 回调 | — | write_cfm_cb |
新增写结果回调 |
permissions 和 operate_indication 各取值的详细说明见 Hello Notify → Property 权限与操作指示。
代码详解
Server 端实现 read_request_cb
当 Client 发起读请求时,协议栈调用 Server 注册的 read_request_cb。回调参数 read_cb_para 包含请求的句柄和类型,Server 据此找到对应的数据并返回:
static void sle_hello_read_request_cb(uint8_t server_id, uint16_t conn_id,
ssaps_req_read_cb_t *read_cb_para, errcode_t status)
{
osal_printk("read request received, handle=0x%04x, type=0x%x\r\n",
read_cb_para->handle, read_cb_para->type);
if (read_cb_para->need_rsp) {
ssaps_send_rsp_t rsp = {0};
rsp.request_id = read_cb_para->request_id;
rsp.status = ERRCODE_SLE_SUCCESS;
rsp.value = g_sle_hello_property_value; /* 返回属性当前值 */
rsp.value_len = sizeof(g_sle_hello_property_value);
ssaps_send_response(server_id, conn_id, &rsp);
osal_printk("read response sent\r\n");
}
}
need_rsp为 true 时必须调用ssaps_send_response()回复;若need_rsp为 false,不回复也不会导致超时。
Server 端实现 write_request_cb
当 Client 发起写请求时,协议栈调用 write_request_cb。回调参数 write_cb_para 包含要写入的句柄、类型和数据:
static void sle_hello_write_request_cb(uint8_t server_id, uint16_t conn_id,
ssaps_req_write_cb_t *write_cb_para, errcode_t status)
{
osal_printk("write request received, handle=0x%04x, length=%d\r\n",
write_cb_para->handle, write_cb_para->length);
/* 校验数据合法性 */
if (write_cb_para->length > MAX_PROPERTY_SIZE) {
osal_printk("write data too large, rejected\r\n");
return; /* 不调用 ssaps_send_response 表示拒绝 */
}
/* 更新本地属性值 */
memcpy_s(g_sle_hello_property_value, sizeof(g_sle_hello_property_value),
write_cb_para->value, write_cb_para->length);
if (write_cb_para->need_rsp) {
ssaps_send_rsp_t rsp = {0};
rsp.request_id = write_cb_para->request_id;
rsp.status = ERRCODE_SLE_SUCCESS;
ssaps_send_response(server_id, conn_id, &rsp);
osal_printk("write response sent: success\r\n");
}
}
写入前务必校验数据合法性——长度、格式、取值范围。不合法的写入应拒绝(不调用
ssaps_send_response())。
注册 Server 读写回调
在 ssaps_register_callbacks() 时将这两个回调填入:
static errcode_t sle_hello_ssaps_register_cbks(void)
{
ssaps_callbacks_t ssaps_cbk = {0};
/* ... 其他回调保持不变 ... */
ssaps_cbk.read_request_cb = sle_hello_read_request_cb; /* 新增 */
ssaps_cbk.write_request_cb = sle_hello_write_request_cb; /* 新增 */
return ssaps_register_callbacks(&ssaps_cbk);
}
Client 端发起读请求
Client 在服务发现完成后拿到 Property 句柄,通过 ssapc_read_req() 发起读请求:
static void sle_hello_client_read_property(void)
{
/* 使用服务发现阶段拿到的 property handle */
uint16_t handle = g_sle_hello_find_service_result.start_hdl;
ssapc_read_req(0, g_sle_hello_conn_id, handle, SSAP_PROPERTY_TYPE_VALUE);
osal_printk("read request sent\r\n");
}
读结果在 read_cfm_cb 中返回:
static void sle_hello_read_cfm_cb(uint8_t client_id, uint16_t conn_id,
ssapc_handle_value_t *read_data, errcode_t status)
{
osal_printk("read result: %s\r\n", read_data->data);
}
Client 端发起写请求
写入需要组装 ssapc_write_param_t 参数:
static void sle_hello_client_write_property(void)
{
ssapc_write_param_t param = {0};
uint8_t new_value[] = "new_config_value";
param.handle = g_sle_hello_find_service_result.start_hdl;
param.type = SSAP_PROPERTY_TYPE_VALUE;
param.data = new_value;
param.data_len = sizeof(new_value);
ssapc_write_req(0, g_sle_hello_conn_id, ¶m);
osal_printk("write request sent\r\n");
}
写入结果在 write_cfm_cb 中返回:
static void sle_hello_write_cfm_cb(uint8_t client_id, uint16_t conn_id,
ssapc_write_result_t *write_result, errcode_t status)
{
if (status == 0) {
osal_printk("write cfm: success, handle=0x%02x\r\n", write_result->handle);
} else {
osal_printk("write cfm: failed, status=0x%x\r\n", status);
}
}
注册 Client 读写回调
在 ssapc_register_callbacks() 时将这两个回调填入:
static void sle_hello_ssapc_cbk_register(...)
{
/* ... 其他回调保持不变 ... */
g_sle_hello_ssapc_cbk.read_cfm_cb = sle_hello_read_cfm_cb; /* 新增 */
g_sle_hello_ssapc_cbk.write_cfm_cb = sle_hello_write_cfm_cb; /* 新增 */
ssapc_register_callbacks(&g_sle_hello_ssapc_cbk);
}
三种交互模式的选择指南
hello 三部曲介绍了 SLE 的三种数据交互模式,下面这张表帮你根据需求选对模式:
| 需求 | 选择模式 | 案例 |
|---|---|---|
| 设备主动上报数据,不需要对方确认 | 通知 (Notification) | 温湿度定时上报、心跳保活 |
| 设备主动推送关键数据,需要对方确认 | 指示 (Indication) | 告警通知、按键事件 |
| Client 想知道 Server 当前的某个值 | 读 (Read) | 查询固件版本、读取设备状态 |
| Client 想修改 Server 的某个配置 | 写 (Write) | 设置上报间隔、开关功能、参数调节 |
一个实际产品通常是三种模式的组合:传感器用通知定时上报数据,手机 App 通过读来查询设备信息,通过写来调整工作参数。