365 lines
9.0 KiB
Markdown
365 lines
9.0 KiB
Markdown
# ESP32 配网组件设计实践:聚焦功能与实现,而不是项目绑定
|
||
|
||
很多 ESP32 设备在开发阶段都把“配网”当成一个小功能,但真正落地后会发现:
|
||
|
||
- 用户第一次接入要顺畅
|
||
- 失败后要能恢复
|
||
- 日志要便于现场排障
|
||
|
||
这篇文章只讲配网组件本身,聚焦能力设计和实现思路,不依赖具体业务项目。
|
||
|
||
---
|
||
|
||
## 一、目标能力定义
|
||
|
||
一个可落地的配网组件,建议至少包含以下能力:
|
||
|
||
- 按键触发配网
|
||
- 常驻配网模式(可选)
|
||
- SoftAP + Web Portal 配网
|
||
- DNS 劫持与常见 Captive Portal 兼容
|
||
- 凭据持久化(NVS)与重启自动重连
|
||
- 清除历史配置(API + Web)
|
||
- 状态机与可读日志
|
||
|
||
这几项组合起来,才能覆盖“首次成功 + 失败恢复 + 现场维护”三个关键场景。
|
||
|
||
图示(整体功能目标关联):
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[按键任务/业务触发] --> B[进入 provisioning]
|
||
B --> C[启动 SoftAP]
|
||
B --> D[启动 HTTP Server]
|
||
B --> E[启动 DNS Hijack]
|
||
D --> F[/api/scan]
|
||
D --> G[/api/connect]
|
||
D --> H[/api/status]
|
||
D --> I[/api/clear]
|
||
G --> J[设置 STA 参数]
|
||
J --> K[发起 esp_wifi_connect]
|
||
K --> L{Wi-Fi/IP 事件}
|
||
L -->|成功| M[状态=connected]
|
||
L -->|失败| N[状态=failed]
|
||
I --> O[清除 NVS 凭据]
|
||
O --> P[清空运行态缓存]
|
||
P --> B
|
||
```
|
||
|
||
---
|
||
|
||
## 二、组件架构(通用)
|
||
|
||
```text
|
||
[按键任务] --> [进入配网]
|
||
|
|
||
+--> SoftAP + HTTP Server + DNS Hijack
|
||
|
||
[Web] -- /api/scan --> Wi-Fi 扫描
|
||
[Web] -- /api/connect --> 设置 STA 并发起连接
|
||
[Web] -- /api/status --> 轮询状态
|
||
[Web] -- /api/clear --> 清除已保存配置
|
||
|
||
[Wi-Fi/IP 事件] --> 更新状态机 + 打印日志 + 保存凭据
|
||
```
|
||
|
||
推荐状态机:
|
||
|
||
- `idle`
|
||
- `provisioning`
|
||
- `connecting`
|
||
- `connected`
|
||
- `failed`
|
||
- `timeout`
|
||
|
||
图示(状态机):
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> idle
|
||
idle --> provisioning: wifi_connect_start()
|
||
provisioning --> connecting: POST /api/connect
|
||
connecting --> connected: GOT_IP
|
||
connecting --> failed: AUTH_FAIL / NO_AP / ...
|
||
connecting --> timeout: connect timeout
|
||
failed --> provisioning: POST /api/clear
|
||
timeout --> provisioning: POST /api/clear
|
||
connected --> provisioning: 常驻配网继续开放入口(可选)
|
||
connected --> idle: stop provisioning(按策略)
|
||
```
|
||
|
||
上图展示了组件的主要数据流与恢复路径。
|
||
|
||
---
|
||
|
||
## 三、对外 API 设计建议
|
||
|
||
推荐保持“少而稳”的接口:
|
||
|
||
```c
|
||
esp_err_t wifi_connect_init(void);
|
||
esp_err_t wifi_connect_start(void);
|
||
esp_err_t wifi_connect_stop(void);
|
||
wifi_connect_status_t wifi_connect_get_status(void);
|
||
esp_err_t wifi_connect_get_config(wifi_connect_config_t *config);
|
||
esp_err_t wifi_connect_clear_config(void);
|
||
```
|
||
|
||
设计原则:
|
||
|
||
- `init` 只做初始化和基础恢复
|
||
- `start/stop` 控制配网生命周期
|
||
- `get_status` 作为 UI/接口层统一读取入口
|
||
- `clear_config` 提供失败恢复通道
|
||
|
||
可以展示一段“典型调用顺序”代码:
|
||
|
||
```c
|
||
ESP_ERROR_CHECK(wifi_connect_init());
|
||
|
||
// 按键触发或业务触发时
|
||
ESP_ERROR_CHECK(wifi_connect_start());
|
||
|
||
// UI 侧轮询状态
|
||
wifi_connect_status_t st = wifi_connect_get_status();
|
||
|
||
// 需要恢复出厂配网时
|
||
ESP_ERROR_CHECK(wifi_connect_clear_config());
|
||
```
|
||
|
||
图示(API 生命周期时序):
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant App as 应用层
|
||
participant WC as wifi-connect
|
||
participant WiFi as esp_wifi
|
||
participant Web as 配网页面
|
||
|
||
App->>WC: wifi_connect_init()
|
||
WC-->>App: ESP_OK
|
||
|
||
App->>WC: wifi_connect_start()
|
||
WC->>WiFi: 开启 APSTA / 事件注册
|
||
WC-->>App: ESP_OK
|
||
|
||
Web->>WC: POST /api/connect(ssid,pass)
|
||
WC->>WiFi: esp_wifi_disconnect()
|
||
WC->>WiFi: esp_wifi_set_config()
|
||
WC->>WiFi: esp_wifi_connect()
|
||
|
||
WiFi-->>WC: WIFI_EVENT / IP_EVENT
|
||
alt 获取到 IP
|
||
WC-->>Web: status=connected
|
||
else 连接失败或超时
|
||
WC-->>Web: status=failed|timeout
|
||
end
|
||
|
||
Web->>WC: POST /api/clear
|
||
WC-->>Web: status=provisioning
|
||
```
|
||
|
||
上图对应完整 API 生命周期(init -> start -> connecting -> connected/failed -> clear)。
|
||
|
||
---
|
||
|
||
## 四、关键实现点
|
||
|
||
### 1) Web 配网页面保持轻量
|
||
|
||
不一定要引入前端框架。对于资源受限设备,内嵌简洁 HTML/JS 往往更稳定。
|
||
|
||
建议页面只保留核心动作:
|
||
|
||
- 扫描网络
|
||
- 提交连接
|
||
- 查看状态
|
||
- 清除配置
|
||
|
||
### 2) Captive Portal 兼容是体验关键
|
||
|
||
仅提供首页 URL 通常不够。建议额外处理:
|
||
|
||
- 常见探测路径(如 `generate_204` 等)
|
||
- 未知路径统一 302 到配网页
|
||
|
||
这样手机系统弹门户页面成功率会明显提高。
|
||
|
||
示例代码(伪代码):
|
||
|
||
```c
|
||
static esp_err_t captive_redirect_handler(httpd_req_t *req)
|
||
{
|
||
httpd_resp_set_status(req, "302 Found");
|
||
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
|
||
return httpd_resp_send(req, NULL, 0);
|
||
}
|
||
|
||
// 注册常见探测路径
|
||
httpd_register_uri_handler(server, &uri_generate_204);
|
||
httpd_register_uri_handler(server, &uri_hotspot_detect);
|
||
httpd_register_uri_handler(server, &uri_ncsi);
|
||
```
|
||
|
||
可直接使用以下流程图(对应 Captive Portal 重定向路径):
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A[手机连接设备 AP] --> B[访问任意域名]
|
||
B --> C[DNS Hijack 返回 192.168.4.1]
|
||
C --> D[HTTP 探测路径请求<br/>/generate_204 等]
|
||
D --> E[302 Location: http://192.168.4.1/]
|
||
E --> F[打开配网页面]
|
||
F --> G[扫描 / 连接 / 清除配置]
|
||
```
|
||
|
||
### 3) 连接前主动断开旧连接
|
||
|
||
这是一个高频坑:设备已有 STA 连接时,直接连接新 AP 可能导致超时或异常状态。
|
||
|
||
建议在 `esp_wifi_set_config + esp_wifi_connect` 前先执行 `esp_wifi_disconnect()`,并对异常返回做日志记录。
|
||
|
||
示例代码:
|
||
|
||
```c
|
||
esp_err_t err = esp_wifi_disconnect();
|
||
if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_CONNECT) {
|
||
ESP_LOGW(TAG, "disconnect before reconnect failed: %s", esp_err_to_name(err));
|
||
}
|
||
|
||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &cfg));
|
||
ESP_ERROR_CHECK(esp_wifi_connect());
|
||
```
|
||
|
||
### 4) “清除配置”必须打通全链路
|
||
|
||
建议同时提供:
|
||
|
||
- SDK API:`wifi_connect_clear_config()`
|
||
- HTTP API:`POST /api/clear`
|
||
- 页面按钮:`清除已保存`
|
||
|
||
这样现场人员无需改固件即可恢复设备。
|
||
|
||
建议把清除动作写成“存储层 + 运行态”两段:
|
||
|
||
```c
|
||
esp_err_t wifi_connect_clear_config(void)
|
||
{
|
||
// 1) 清 NVS 凭据
|
||
ESP_RETURN_ON_ERROR(nvs_erase_key(nvs, "ssid"), TAG, "erase ssid failed");
|
||
ESP_RETURN_ON_ERROR(nvs_erase_key(nvs, "pass"), TAG, "erase pass failed");
|
||
ESP_RETURN_ON_ERROR(nvs_commit(nvs), TAG, "nvs commit failed");
|
||
|
||
// 2) 清内存态并回到 provisioning
|
||
memset(&s_ctx.pending_cfg, 0, sizeof(s_ctx.pending_cfg));
|
||
s_ctx.status = WIFI_CONNECT_STATUS_PROVISIONING;
|
||
return ESP_OK;
|
||
}
|
||
```
|
||
|
||
可直接使用以下流程图(对应 clear 后状态回到 provisioning):
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[开始清除配置] --> B[擦除 NVS:ssid]
|
||
B --> C[擦除 NVS:pass]
|
||
C --> D[nvs_commit]
|
||
D --> E[清空 pending 配置缓存]
|
||
E --> F[清空错误原因/中间状态]
|
||
F --> G[状态切回 provisioning]
|
||
G --> H[前端轮询显示 可重新配网]
|
||
```
|
||
|
||
---
|
||
|
||
## 五、日志策略(非常重要)
|
||
|
||
建议日志遵循“状态 + 原因”格式,例如:
|
||
|
||
- `【状态】配网已启动:热点已开启,SSID=...`
|
||
- `【状态】开始连接路由器:目标网络=...`
|
||
- `【状态】联网成功:获取 IP=...`
|
||
- `【状态】连接失败:原因=...`
|
||
|
||
这样做的收益是:
|
||
|
||
- 开发调试快
|
||
- 测试可直接定位阶段
|
||
- 现场人员无需先理解底层驱动日志
|
||
|
||
可展示一个统一日志函数风格:
|
||
|
||
```c
|
||
static void log_state_i(const char *title, const char *detail)
|
||
{
|
||
ESP_LOGI(TAG, "【状态】%s:%s", title, detail ? detail : "-");
|
||
}
|
||
```
|
||
|
||
如需补充非 Mermaid 图,建议仅放一张关键串口日志截图(启动配网、连接中、成功/失败重试)。
|
||
|
||
---
|
||
|
||
## 六、常见问题与排障思路
|
||
|
||
### 问题 1:手机连上 AP 但页面不弹
|
||
|
||
排查:
|
||
|
||
- 手动访问 `http://192.168.4.1`
|
||
- 检查 DNS 劫持和门户探测路径是否启用
|
||
- 检查 HTTP 服务是否启动成功
|
||
|
||
### 问题 2:提交密码后长时间连接失败
|
||
|
||
排查:
|
||
|
||
- 是否先断开旧 STA
|
||
- 是否正确处理了连接超时和重试
|
||
- 失败原因是否上报到状态机和前端
|
||
|
||
### 问题 3:配网失败后无法恢复
|
||
|
||
排查:
|
||
|
||
- NVS 清除逻辑是否真正执行
|
||
- 内存态缓存是否同时清空
|
||
- 配网状态是否回到 `provisioning`
|
||
|
||
---
|
||
|
||
## 七、测试清单(可复用)
|
||
|
||
建议每次迭代最少覆盖:
|
||
|
||
1. 首次配网成功
|
||
2. 密码错误后重试成功
|
||
3. 连接旧网状态下切换新网成功
|
||
4. 清除配置后重新配网成功
|
||
5. 重启后自动重连
|
||
6. 空闲超时与手动停止路径可用
|
||
|
||
这 6 条通过后,组件稳定性通常会显著提升。
|
||
|
||
---
|
||
|
||
## 八、可继续增强的方向
|
||
|
||
- 配网页面安全增强(鉴权/会话)
|
||
- 多语言提示
|
||
- 更细粒度错误码映射
|
||
- BLE 辅助配网
|
||
- 命令行/远程维护接口联动
|
||
|
||
---
|
||
|
||
## 结语
|
||
|
||
配网组件的核心价值,不是“让设备连上一次网”,而是:
|
||
|
||
- 功能完整
|
||
- 异常可恢复
|
||
- 排障可落地
|
||
|
||
当这三件事做好后,它才是一个能复用、能维护、能上线的基础能力组件。 |