跳转至

设备配置

SLE SSAP 属性读写、设备参数规划、持久化存储

前置阅读:Hello ReadWrite

学习目标

  • 理解设备参数配置与简单属性读写的区别:配置需要设计属性布局、校验、持久化
  • 掌握将多个设备参数映射到 SSAP 属性的规划方法
  • 掌握 Server 端校验写入数据的合法性和生效逻辑
  • 理解只读、可读写、可写不可读三种权限在实际配置场景中的运用
  • 能够在 hello-readwrite 的基础上,实现 App 对设备的完整参数配置

规格与功能

本案例在 hello-readwrite 基础上,实现一个完整的设备参数配置功能——这是所有可配置 IoT 设备的通用模式。

规格项 Server 端 Client 端
连接与配对 继承 hello-connect + hello-readwrite 流程 同 Server
可读属性 设备名称、固件版本、序列号(只读) 读取并显示
可读写属性 上报间隔、告警阈值、设备模式(可读写) 读取当前值、写入新值
数据校验 拒绝非法值(间隔 < 100ms、阈值超范围) 收到拒绝时提示用户
持久化 配置写入后保存到 NV(Flash)
生效策略 部分参数立即生效,部分需重启

程序运行流程:

  1. Client 连接 Server 后,完成配对和 MTU 协商
  2. Client 发现服务结构,拿到各 Property 的 handle
  3. Client 先读取设备名称、固件版本等固定信息
  4. Client 读取当前上报间隔和告警阈值
  5. 用户修改上报间隔 → Client 发起 Write Request → Server 校验合法性 → 写入 NV → 返回成功
  6. 用户修改超出范围的值 → Server 校验失败 → 返回错误码

hello-readwrite 教你"怎么读和写",本篇教你"设计一个设备有哪些可配置属性以及怎么写对"。

基本概念

典型使用场景

任何需要远程管理和配置的 IoT 设备,都会涉及设备参数配置。与传感器上报(数据从设备流向 App)不同,配置是 App 流向设备的指令。例如:

  • 智能灯控:App 配置灯的亮度、色温、定时开关时间
  • 环境传感器:App 配置上报间隔、告警阈值、传感器使能
  • 工业网关:App 配置采样率、滤波参数、RS485 从站地址
  • 穿戴设备:App 配置心率监测频率、屏幕亮度、振动强度

属性分类与权限设计

一个设计良好的设备配置方案,应该将属性按用途和权限分类:

类型 权限 说明 示例
设备信息 只读 出厂固化的标识信息,不允许修改 固件版本、序列号、MAC 地址
运行状态 只读 运行时动态变化的值,只可观测 CPU 温度、信号强度、电池电量
可配置参数 可读写 用户可修改的参数,需校验范围 上报间隔、告警阈值、设备名称
控制指令 只写 触发某个动作,不存储值 重启、恢复出厂设置、立即上报
Service (UUID: 0x5000)
├── Property 1  handle=1  权限: READ        设备名称   "EnvSensor-01"
├── Property 2  handle=2  权限: READ        固件版本   "v1.0.0"
├── Property 3  handle=3  权限: READ|WRITE  上报间隔   1000ms
├── Property 4  handle=4  权限: READ|WRITE  告警阈值   80℃
├── Property 5  handle=5  权限: READ|WRITE  设备模式   0=正常, 1=低功耗
└── Property 6  handle=6  权限: WRITE       恢复出厂   写 1 触发

通信流程:配置读写

sequenceDiagram
    participant App as Client (App)
    participant Dev as Server (设备)

    Note over App,Dev: 已连接、已配对、已发现服务

    App->>Dev: Read Req (handle=1, 设备名称)
    Dev-->>App: Read Resp: "EnvSensor-01"

    App->>Dev: Read Req (handle=3, 上报间隔)
    Dev-->>App: Read Resp: 1000 (ms)

    App->>Dev: Write Req (handle=3, value=500ms)
    Dev->>Dev: 校验: 500 >= 100 ✓
    Dev->>Dev: 写入 NV
    Dev-->>App: Write Resp: SUCCESS
    Note right of Dev: 新间隔立即生效

    App->>Dev: Write Req (handle=3, value=50ms)
    Dev->>Dev: 校验: 50 < 100 ✗ 太短!
    Dev-->>App: Write Resp: ERR_INVALID_PARAM

    App->>Dev: Write Req (handle=6, value=1)
    Dev->>Dev: 恢复出厂设置
    Dev-->>App: Write Resp: SUCCESS
    Note over Dev: 设备重启...

数据校验的重要性

Client 写配置值时,Server 不能无条件信任。必须校验:

  1. 范围校验:值是否在允许范围内(如上报间隔 ≥ 100ms)
  2. 类型校验:值的数据类型是否匹配(如期望 uint16_t,收到的是否为 2 字节)
  3. 逻辑校验:多个参数之间是否冲突(如低功耗模式 + 上报间隔 100ms 矛盾)
  4. 权限校验:是否试图写只读属性(协议栈会自动拦截,Server 回调不会触发)

校验失败的写入必须返回明确的错误码,帮助 App 端定位问题。不要静默忽略——这会让开发者困惑为什么配置不生效。

涉及 API

本案例在 hello-readwrite 的 API 基础上新增属性布局规划和 NV 存储相关 API:

API 谁调用 用途
ssapc_read_req() Client 发起属性读请求(hello-readwrite 中已介绍)
ssapc_write_req() Client 发起属性写请求(hello-readwrite 中已介绍)
ssaps_read_response() Server 回复读请求(hello-readwrite 中已介绍)
ssaps_write_response() Server 回复写请求(hello-readwrite 中已介绍)
ssaps_add_property_sync() Server 添加属性,配置权限和操作指示位(hello-notify 中已介绍)
uapi_nv_write() Server 将配置值写入 NV(Flash 持久化)
uapi_nv_read() Server 从 NV 读取已保存的配置值

案例说明

做什么

实现一个设备配置方案:Server 端暴露 6 个 Property,包含设备信息(只读)、运行参数(可读写)、控制指令(只写)。Client 端通过标准的 Read/Write Request 读取信息和修改配置。

属性布局

Handle 名称 权限 数据类型 默认值 校验规则
1 设备名称 R string[16] "EnvSensor-01"
2 固件版本 R string[8] "v1.0.0"
3 上报间隔 R/W uint16_t (ms) 1000 100 ~ 60000
4 告警阈值 R/W int16_t (×10℃) 800 -200 ~ 1000
5 设备模式 R/W uint8_t 0 0=正常, 1=低功耗
6 恢复出厂 W uint8_t 写 1 触发

属性布局是设备端最重要的设计决策之一。新增属性时只追加不修改已有 handle 编号,保证新旧固件兼容。

关键配置

属性权限与操作指示配置

每个 Property 的权限和操作指示决定它的行为:

// Property 1: 设备名称 — 只读
#define DEV_NAME_PERMISSIONS    SSAP_PERMISSION_READ
#define DEV_NAME_OP_IND         SSAP_OPERATE_INDICATION_BIT_READ

// Property 3: 上报间隔 — 可读写
#define REPORT_INTERVAL_PERMISSIONS  (SSAP_PERMISSION_READ | SSAP_PERMISSION_WRITE)
#define REPORT_INTERVAL_OP_IND       (SSAP_OPERATE_INDICATION_BIT_READ | \
                                      SSAP_OPERATE_INDICATION_BIT_WRITE)

// Property 6: 恢复出厂 — 只写
#define FACTORY_RESET_PERMISSIONS    SSAP_PERMISSION_WRITE
#define FACTORY_RESET_OP_IND         SSAP_OPERATE_INDICATION_BIT_WRITE

配置持久化

// 设备配置的 NV 存储结构
#define NV_ITEM_DEVICE_NAME       0x01
#define NV_ITEM_REPORT_INTERVAL   0x03
#define NV_ITEM_ALARM_THRESHOLD   0x04
#define NV_ITEM_DEVICE_MODE       0x05

// 上电时从 NV 读取已保存的配置
static void load_config_from_nv(void)
{
    uint16_t interval;
    if (uapi_nv_read(NV_ITEM_REPORT_INTERVAL, &interval, 2) == ERRCODE_SUCC) {
        g_device_config.report_interval = interval;
    } else {
        // NV 中无数据,使用默认值
        g_device_config.report_interval = 1000;
    }
}

代码详解

Server 端:write_request_cb 实现

Server 收到写请求时需要:识别属性 → 校验值 → 更新本地 → 持久化 → 回复结果:

static void dev_config_write_request_cb(uint16_t conn_id, uint16_t handle,
                                         uint8_t type, uint8_t *value,
                                         uint16_t length)
{
    errcode_t ret = ERRCODE_FAIL;

    switch (handle) {
    case 1: // 设备名称
        if (length > 16) {
            ret = ERRCODE_INVALID_PARAM_SIZE;
            break;
        }
        memcpy(g_device_config.device_name, value, length);
        uapi_nv_write(NV_ITEM_DEVICE_NAME, value, length);
        ret = ERRCODE_SUCC;
        break;

    case 3: // 上报间隔
        if (length != 2) {
            ret = ERRCODE_INVALID_PARAM_SIZE;
            break;
        }
        uint16_t new_interval = *(uint16_t *)value;
        if (new_interval < 100 || new_interval > 60000) {
            ret = ERRCODE_INVALID_PARAM;
            break;
        }
        g_device_config.report_interval = new_interval;
        uapi_nv_write(NV_ITEM_REPORT_INTERVAL, &new_interval, 2);
        update_timer_interval(new_interval);
        ret = ERRCODE_SUCC;
        break;

    case 6: // 恢复出厂设置
        if (*value == 1) {
            factory_reset();  // 恢复默认配置并重启
        }
        ret = ERRCODE_SUCC;
        break;

    default:
        ret = ERRCODE_INVALID_HANDLE;
        break;
    }

    ssaps_write_response(conn_id, handle, type, ret);
}

Client 端:配置读写流程

Client 侧通过服务发现拿到 handle 后,按需读取或写入:

// 读取设备名称
ssapc_read_req(g_client_handle, g_conn_id,
               DEV_NAME_HANDLE,
               SSAP_PROPERTY_TYPE_VALUE);

// 修改上报间隔为 500ms
uint16_t new_interval = 500;
ssapc_write_param_t write_param = {0};
write_param.handle = REPORT_INTERVAL_HANDLE;  // handle = 3
write_param.type   = SSAP_PROPERTY_TYPE_VALUE;
write_param.value  = (uint8_t *)&new_interval;
write_param.length = sizeof(new_interval);
ssapc_write_req(g_client_handle, g_conn_id, &write_param);

配置读写回调处理

// 读结果回调
static void read_result_cb(uint16_t conn_id, uint16_t handle,
                           uint8_t type, errcode_t status,
                           uint8_t *value, uint16_t length)
{
    if (status != ERRCODE_SUCC) {
        printf("[config] read handle=%d failed, err=%d\n", handle, status);
        return;
    }
    switch (handle) {
    case 1: // 设备名称
        printf("[config] device name: %s\n", (char *)value);
        break;
    case 3: // 上报间隔
        printf("[config] report interval: %d ms\n", *(uint16_t *)value);
        break;
    }
}

// 写确认回调
static void write_cfm_cb(uint16_t conn_id, uint16_t handle,
                         uint8_t type, errcode_t status)
{
    if (status == ERRCODE_SUCC) {
        printf("[config] write handle=%d success\n", handle);
    } else {
        printf("[config] write handle=%d failed, err=%d\n", handle, status);
    }
}

属性布局规划原则

设计设备配置的属性布局时,遵循以下原则:

原则 说明 错误示例
一个属性做一件事 不要把上报间隔和告警阈值合并到一个 Property 里 同一 Property 既存温度又存湿度
handle 编号只增不改 新增属性追加到末尾,旧 handle 不变 在中间插入新属性导致 handle 错位
读写权限正确设置 只读信息不设 WRITE 权限 把固件版本设成可读写
有校验必有错误码 每个校验失败返回不同的错误码 所有非法输入都返回 -1
持久化关键配置 用户改过的配置写 NV,断电重启保持 重启后配置丢失,回到默认值

属性布局设计好后,建议做一个表格文档(类似上面"属性布局"表格),作为 Server 和 Client 开发者的共同契约。