Wi-Fi 配网
SLE SSAP 属性读写、WiFi STA
前置阅读:Hello ReadWrite、STA 连接
学习目标
- 理解 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
程序运行流程:
- Device 广播配网 Service,Client 扫描并连接
- Client 进行服务发现,找到配网 Service 的三个 Property
- Client 依次写入 SSID 和 Password
- Device 校验凭证,调用 WiFi STA API 连接路由器
- Device 通过 Status Property Notify 推送连接结果(成功则附带 IP)
- 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, ¬ify_param) |
Server | 推送 WiFi 连接状态变化 |
ssapc_write_req(client_id, conn_id, ¶m) |
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 固件
第二步:编译 Client 固件
第三步:烧录并运行
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, ¶m);
}
配网结果 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。