跳转至

Wi-Fi 配网

SLE SSAP 属性读写、WiFi STA

前置阅读:Hello ReadWriteSTA 连接

学习目标

  • 理解 WiFi 配网的整体流程——手机通过 SLE 下发 WiFi 凭证,WS63 连接路由器,结果回传
  • 掌握配网协议设计——SSID/Password 属性定义、写入校验、连接结果通知
  • 理解配网安全考量——凭证传输加密、配网超时保护、错误重试机制
  • 能够在 WS63 上实现"手机配网"的完整闭环

规格与功能

本案例实现一个 WiFi 配网 Server(待配网设备)和一个配网 Client(手机 APP 模拟端)。Client 通过 SLE 连接 Server 后,将 WiFi SSID/Password 写入 Server 的配网属性,Server 连接路由器后将结果通过 Notify 回传。

规格项 Server 端(待配网设备) Client 端(配网 APP)
Service 注册配网专用 Service,含 3 个 Property 发现配网 Service 及其 Property
SSID Property 可写 String,最大 32 字节 写入目标 WiFi 的 SSID
Password Property 可写 String,长度 8~63 字节 写入目标 WiFi 的密码
Status Property 只读 + Notify,返回连接状态和 IP 读取/订阅连接状态变化
WiFi 连接 收到凭证后调用 wifi_sta_connect()
配网超时 180 秒无操作自动关闭配网服务 超时后需重新触发配网
重试策略 密码错误最多重试 3 次,然后锁定 60 秒 收到错误码后可重新发送

系统架构:一台 WS63 作为待配网设备,另一台 WS63(或手机)作为配网 Client。

flowchart LR
    subgraph Provisioner[配网 Client]
        APP[配网 APP]
        SClient[SLE Client]
    end
    subgraph Device[待配网设备 WS63]
        SServer[SLE Server<br/>配网 Service]
        WiFi[WiFi STA]
    end
    Router[WiFi 路由器]
    Cloud[云端服务]

    APP --> SClient
    SClient -->|SLE SSAP Write<br/>SSID + Password| SServer
    SServer --> WiFi
    WiFi -->|连接 + DHCP| Router
    Router --> Cloud
    SServer -->|SLE Notify<br/>连接结果 + IP| SClient

程序运行流程:

  1. Device 广播配网 Service,Client 扫描并连接
  2. Client 进行服务发现,找到配网 Service 的三个 Property
  3. Client 依次写入 SSID 和 Password
  4. Device 校验凭证,调用 WiFi STA API 连接路由器
  5. Device 通过 Status Property Notify 推送连接结果(成功则附带 IP)
  6. Client 收到结果后断开 SLE(配网完成)

基本概念

典型使用场景

IoT 设备没有屏幕和键盘,无法直接输入 WiFi 密码。配网解决"让设备连上 WiFi"的第一步——

  • 手机 APP 通过 SLE 连接设备
  • APP 发送 WiFi SSID/Password
  • 设备用这些凭证连接路由器
  • 连接结果通过 SLE 回传 APP

与 BLE 配网的对比:

BLE 配网 SLE 配网
链路建立速度 慢(~100ms) 快(~10ms)
数据吞吐 ~100 kbps 最高 12 Mbps
后续数据通信 需切换 WiFi 或维持 BLE 可继续使用 SLE 高速数据通道
手机兼容性 iOS/Android 原生支持 需 SDK 支持

SLE 配网的优势在于更快的连接建立和配网完成后可继续使用 SLE 进行高速数据通信。

配网协议设计

设备端暴露三个 SSAP Property:

Property UUID 偏移 类型 权限 操作指示 说明
WiFi SSID +0 String WRITE WRITE Client 写入目标 WiFi 名称
WiFi Password +1 String WRITE WRITE Client 写入目标 WiFi 密码
Status +2 String READ READ | NOTIFY Device 推送连接状态变化

Client 写 SSID 和 Password,Server 校验后调用 WiFi API 连接,连接结果通过 Notify 推回。

安全考量

SLE 链路在配对后已加密,凭证传输有链路层保护。额外安全建议:

措施 说明
配网超时 180 秒无操作自动关闭配网 Service
重试限制 密码错误累计 3 次锁定 60 秒
配网后关闭 ssaps_delete_all_services() 关闭配网 Service,防止非授权配网
凭证不持久化 配网失败不保存凭证,每次重新输入

通信流程: WiFi 配网

sequenceDiagram
    participant CLI as Client
    participant SRV as Server
    participant Router as WiFi Router

    Note over SRV: 注册配网 Service<br/>SSID Pswd Status 三个 Property
    Note over SRV: 启动广播
    Note over CLI: 扫描设备

    CLI->>SRV: SLE 连接
    SRV-->>CLI: 连接确认
    CLI->>SRV: 配对 + MTU 协商
    CLI->>SRV: 服务发现
    SRV-->>CLI: 返回配网 Service Property 信息

    CLI->>SRV: Write SSID Property
    Note right of SRV: write_request_cb<br/>校验 SSID 长度 1~32
    SRV-->>CLI: Write Response: success

    CLI->>SRV: Write Password Property
    Note right of SRV: write_request_cb<br/>校验 Password 长度 8~63
    SRV-->>CLI: Write Response: success

    Note over SRV: 凭证完整<br/>调用 wifi_sta_connect

    SRV->>Router: WiFi 关联请求
    Router-->>SRV: 关联确认
    SRV->>Router: DHCP 获取 IP
    Router-->>SRV: IP 地址

    SRV->>CLI: Notify Status: connected + IP 地址
    Note right of CLI: 配网成功<br/>可选断开 SLE

涉及 API

API 谁调用 用途
ssaps_add_property_sync(server_id, service_id, &property) Server 注册配网 Property(SSID/Password/Status)
ssaps_write_response(server_id, conn_id, &rsp) Server 回复 Client 写入结果
ssaps_notify_indicate(server_id, conn_id, &notify_param) Server 推送 WiFi 连接状态变化
ssapc_write_req(client_id, conn_id, &param) Client 写入 SSID 和 Password
wifi_sta_enable() Server 使能 WiFi STA 模式
wifi_sta_connect(&config) Server 使用收到的凭证连接路由器
wifi_register_event_cb(&callback) Server 监听 WiFi 连接/DHCP 事件

前置 API 来自 hello-connect(sle_announce / sle_seek / sle_connect_remote_device)和 hello-readwrite(ssaps_register_callbacks / ssapc_register_callbacks / ssapc_read_req)。

案例操作指导

第一步:编译 Server 固件

Top → Application → Samples → BT → SLE → Verticals → [*] WiFi Config Server Sample
fbb build ws63-liteos-app -p menuconfig
fbb build ws63-liteos-app

第二步:编译 Client 固件

Top → Application → Samples → BT → SLE → Verticals → [*] WiFi Config Client Sample

第三步:烧录并运行

Server 先上电,Client 后上电。预期串口输出:

Server 端:

[wifi-config server] init ok, announce started
[wifi-config server] connected, conn_id=0x01
[wifi-config server] pair complete
[wifi-config server] ssid received: MyWiFi
[wifi-config server] password received, length=8
[wifi-config server] connecting to WiFi...
[wifi-config server] WiFi connected, IP: 192.168.1.100
[wifi-config server] status notified to client

Client 端:

[wifi-config client] init ok, scanning...
[wifi-config client] found wifi-config server
[wifi-config client] connected, conn_id=0x01
[wifi-config client] writing SSID...
[wifi-config client] SSID write success
[wifi-config client] writing Password...
[wifi-config client] Password write success
[wifi-config client] status notification: connected, IP=192.168.1.100
[wifi-config client] provisioning complete

第四步:验证配网结果

Client 端收到 provisioning complete 即表示配网成功。Server 端可进一步验证:串口执行 ping 网关地址 确认网络畅通。

关键配置

参数 说明
配网 Service UUID 自定义 16-bit,如 0x5678 与普通业务 Service 区分
SSID 最大长度 32 字节 802.11 标准规定的 SSID 上限
Password 最小长度 8 字节 WPA2-PSK 最低要求
Password 最大长度 63 字节 WPA2-PSK 支持上限
配网超时 180 秒 开启配网后计时,超时自动关闭
最大重试次数 3 次 密码错误累计达上限后锁定
锁定时间 60 秒 锁定期间拒绝任何配网请求
Property permissions SSAP_PERMISSION_WRITE(SSID/Password) 仅允许 Client 写入
Status Property permissions SSAP_PERMISSION_READ 允许 Client 读取并订阅 Notify

代码详解

配网 Service 注册

Server 在连接后注册配网专用 Service,包含三个 Property:

#define PROV_SERVICE_UUID  0x5678
#define PROP_ID_SSID       1
#define PROP_ID_PASSWORD   2
#define PROP_ID_STATUS     3

static errcode_t register_prov_service(uint8_t server_id)
{
    ssaps_add_service_param_t service = {0};
    service.uuid.type = SSAP_UUID_TYPE_16BIT;
    service.uuid.value.u16 = PROV_SERVICE_UUID;

    ssaps_add_property_param_t ssid_prop = {0};
    ssid_prop.handle = PROP_ID_SSID;
    ssid_prop.permissions = SSAP_PERMISSION_WRITE;
    ssid_prop.operate_indication = SSAP_OPERATE_INDICATION_BIT_WRITE;

    ssaps_add_property_param_t pswd_prop = {0};
    pswd_prop.handle = PROP_ID_PASSWORD;
    pswd_prop.permissions = SSAP_PERMISSION_WRITE;
    pswd_prop.operate_indication = SSAP_OPERATE_INDICATION_BIT_WRITE;

    ssaps_add_property_param_t status_prop = {0};
    status_prop.handle = PROP_ID_STATUS;
    status_prop.permissions = SSAP_PERMISSION_READ;
    status_prop.operate_indication = SSAP_OPERATE_INDICATION_BIT_READ
                                   | SSAP_OPERATE_INDICATION_BIT_NOTIFY;

    ssaps_add_service_sync(server_id, &service);
    ssaps_add_property_sync(server_id, service.handle, &ssid_prop);
    ssaps_add_property_sync(server_id, service.handle, &pswd_prop);
    ssaps_add_property_sync(server_id, service.handle, &status_prop);
    return 0;
}

SSID 和 Password 仅开放写权限(Client 写入),Status 开放读 + Notify(Client 读取并订阅推送)。

SSID/Password 写入校验

static errcode_t validate_ssid(uint8_t *data, uint16_t len)
{
    if (len == 0 || len > 32) {
        return ERRCODE_INVALID_PARAM;  /* 非法 SSID 长度 */
    }
    return ERRCODE_SLE_SUCCESS;
}

static errcode_t validate_password(uint8_t *data, uint16_t len)
{
    if (len < 8 || len > 63) {
        return ERRCODE_INVALID_PARAM;  /* 密码长度不满足 WPA2 要求 */
    }
    return ERRCODE_SLE_SUCCESS;
}

static void prov_write_request_cb(uint8_t server_id, uint16_t conn_id,
                                  ssaps_req_write_cb_t *write_cb_para, errcode_t status)
{
    errcode_t ret = ERRCODE_SLE_SUCCESS;
    if (write_cb_para->handle == PROP_ID_SSID) {
        ret = validate_ssid(write_cb_para->value, write_cb_para->length);
        if (ret == 0) {
            memcpy_s(g_ssid, sizeof(g_ssid), write_cb_para->value, write_cb_para->length);
            g_ssid_len = write_cb_para->length;
            osal_printk("SSID saved: %s\r\n", g_ssid);
        }
    } else if (write_cb_para->handle == PROP_ID_PASSWORD) {
        ret = validate_password(write_cb_para->value, write_cb_para->length);
        if (ret == 0) {
            memcpy_s(g_password, sizeof(g_password), write_cb_para->value, write_cb_para->length);
            g_password_len = write_cb_para->length;
            osal_printk("Password saved, length=%d\r\n", g_password_len);
        }
    }

    if (write_cb_para->need_rsp) {
        ssaps_send_rsp_t rsp = {0};
        rsp.request_id = write_cb_para->request_id;
        rsp.status = ret;
        ssaps_write_response(server_id, conn_id, &rsp);
    }

    /* 两个凭证都收到后触发连接 */
    if (g_ssid_len > 0 && g_password_len > 0) {
        start_wifi_connect();
    }
}

校验不通过返回 ERRCODE_INVALID_PARAM,Client 可据此提示用户修改后重试。凭证不完整时不触发连接。

WiFi 连接执行

static void start_wifi_connect(void)
{
    wifi_sta_config_t config = {0};
    memcpy_s(config.ssid, sizeof(config.ssid), g_ssid, g_ssid_len);
    memcpy_s(config.password, sizeof(config.password), g_password, g_password_len);
    config.security_type = WIFI_SEC_TYPE_WPA2_PSK;

    wifi_sta_enable();                           /* 使能 STA 模式 */
    wifi_sta_connect(&config);                   /* 发起连接 */
    osal_printk("WiFi connecting to %s...\r\n", config.ssid);

    g_prov_timeout_timer = osal_start_timer(PROV_TIMEOUT_MS);
}

static void wifi_event_cb(wifi_event_t *event)
{
    switch (event->event_id) {
    case WIFI_EVENT_CONNECTION_CHANGED:
        if (event->connection_state == WIFI_CONNECTED) {
            osal_printk("WiFi connected\r\n");
            notify_provisioning_result(PROV_STATUS_CONNECTED, event->ip_addr);
        } else {
            osal_printk("WiFi connection failed, reason=%d\r\n", event->reason);
            notify_provisioning_result(PROV_STATUS_FAILED, NULL);
        }
        break;
    case WIFI_EVENT_DHCP_SUCCESS:
        osal_printk("DHCP success, IP: %s\r\n", event->ip_addr);
        break;
    }
}

WiFi 连接结果在 wifi_event_cb 中异步返回。成功时附带 IP 地址,失败时附带错误原因码。

结果回传

static void notify_provisioning_result(uint8_t status, char *ip)
{
    ssaps_notify_indicate_param_t param = {0};
    param.handle = PROP_ID_STATUS;

    if (status == PROV_STATUS_CONNECTED) {
        snprintf_s(g_status_buf, sizeof(g_status_buf), sizeof(g_status_buf),
                   "connected,ip=%s", ip);
    } else {
        snprintf_s(g_status_buf, sizeof(g_status_buf), sizeof(g_status_buf),
                   "failed,code=%d", g_last_error);
    }

    param.data = (uint8_t *)g_status_buf;
    param.data_len = strlen(g_status_buf);
    ssaps_notify_indicate(g_server_id, g_conn_id, &param);
}

配网结果 Status 值说明:

Status 值 含义 IP 字段
connected WiFi 连接成功 有效 IP 地址
failed WiFi 连接失败 空,code 字段含错误码
timeout 配网超时
locked 重试次数耗尽已锁定

Client 端配网操作

static void start_provisioning(void)
{
    /* 写入 SSID */
    ssapc_write_param_t ssid_param = {0};
    ssid_param.handle = PROP_ID_SSID;
    ssid_param.type = SSAP_PROPERTY_TYPE_VALUE;
    ssid_param.data = (uint8_t *)"MyWiFi";
    ssid_param.data_len = 6;
    ssapc_write_req(g_client_id, g_conn_id, &ssid_param);
}

static void prov_write_cfm_cb(uint8_t client_id, uint16_t conn_id,
                              ssapc_write_result_t *result, errcode_t status)
{
    if (result->handle == PROP_ID_SSID && status == 0) {
        osal_printk("SSID write success\r\n");
        /* SSID 写入成功后继续写 Password */
        ssapc_write_param_t pswd_param = {0};
        pswd_param.handle = PROP_ID_PASSWORD;
        pswd_param.type = SSAP_PROPERTY_TYPE_VALUE;
        pswd_param.data = (uint8_t *)"12345678";
        pswd_param.data_len = 8;
        ssapc_write_req(client_id, conn_id, &pswd_param);
    }
}

static void prov_notification_cb(uint8_t client_id, uint16_t conn_id,
                                  ssapc_handle_value_t *data, errcode_t status)
{
    osal_printk("provisioning result: %s\r\n", data->data);
    if (strstr((char *)data->data, "connected") != NULL) {
        osal_printk("provisioning complete!\r\n");
    }
}

Client 端串行写入:先写 SSID,确认成功后再写 Password。Status Notify 在主回调中处理。

配网完成后的清理

static void cleanup_provisioning(uint8_t server_id)
{
    ssaps_delete_all_services(server_id);    /* 关闭配网 Service */
    osal_stop_timer(g_prov_timeout_timer);   /* 停止超时定时器 */
    memset_s(g_ssid, sizeof(g_ssid), 0, sizeof(g_ssid));      /* 清除凭证 */
    memset_s(g_password, sizeof(g_password), 0, sizeof(g_password));
    osal_printk("provisioning service closed\r\n");
}

配网成功后关闭配网 Service 是关键安全措施,防止后续非授权配网。凭证从内存中擦除以避免泄露。后续通信走 WiFi,SLE 连接可断开。

配网与数据通信的架构选择

方案 配网通道 数据通道 适用场景
SLE 配网 + WiFi 通信 SLE WiFi 高带宽需求(音视频)
SLE 配网 + SLE 通信 SLE SLE CHBA / SSAP 低功耗传感器网络
纯 WiFi 配网(SmartConfig) WiFi 广播 WiFi 单设备快速配网

本案例采用方案一,配网完成后 SLE 连接断开,后续数据通信全部走 WiFi。