跳转至

Hello ReadWrite

读写交互 — SLE SSAP 属性读写、请求-响应模式

前置阅读:Hello SLEHello Notify

学习目标

  • 理解通知推送与读写交互的本质区别:推送是 Server 主动,读写是 Client 主动
  • 掌握 Server 端实现 read_request_cbwrite_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 确认写入完成

程序运行流程:

  1. Server 和 Client 建立连接(hello-connect)
  2. 配对 → MTU → 服务发现(hello-notify)
  3. Server 发送 "hello world" 通知(hello-notify)
  4. Client 主动读取 Server 的属性值 → Server 返回当前值
  5. 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 全部取值

permissionsssap_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_indicationssap_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_READ
  • write_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, 确认写入完成

图中各阶段的职责:

  1. 读请求:Client 指定属性句柄发起读请求,Server 在 read_request_cb 中根据句柄找到对应数据,通过 ssaps_send_response() 返回。Client 在 read_cfm_cb 中拿到返回值
  2. 写请求: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:

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.
[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 resultwrite 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, &param);
    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 通过读来查询设备信息,通过写来调整工作参数。