零知派——ESP32-S3 AI 小智 使用 Preferences NVS 实现Web配网持久化
本文介绍了零知派ESP32-S3开发板的网页配网方案。该方案通过启动AP热点和Web服务器,让用户通过浏览器配置WiFi连接,实现跨平台、无需安装App的便捷配网。关键技术包括:AP/DNSCaptivePortal自动弹出配置页面、异步WiFi连接处理、Preferences库实现配置持久化存储。系统支持自动重连已保存网络,失败时回退配网模式,并提供按键触发重新配网功能。该方案解决了传统配网方式
✔零知派(零知开源)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 “配置环境” 转移到 “创意实现”,极大降低了技术门槛。零知开源编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知派(零知开源)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
目录
项目概述
本方案利用零知派ESP32-S3内置的NVS(非易失性存储)与Preferences库,实现WiFi配置的持久化保存。首次通过网页配网成功后,SSID和密码被自动写入Flash分区,断电不丢失。设备每次重启时,优先从Flash读取已保存的凭证,调用WiFi.begin()自动重连。若网络环境变化导致连接失败,系统自动回退到网页配网模式。
项目难点及解决方案
问题描述:AP/STA模式的无缝切换 与 非阻塞事件处理
解决方案:关闭AP前确保STA已成功连接,采用轮询 WiFi 状态取代 while() 等待 WiFi 连接的阻塞式等待。
一、为什么需要网页配网?
常见方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 硬编码 SSID/密码 | 简单直接 | 无法适应不同网络,毫无通用性 |
| SmartConfig(微信配网) | 无需连接 AP | 兼容性差(部分路由器不支持),成功率低,依赖微信生态 |
| 蓝牙配网 | 稳定 | 需要蓝牙硬件,App 开发成本高 |
| 网页配网 | 跨平台(任何手机/电脑浏览器),无需安装 App,实现简单 | 用户需手动切换 WiFi 连接设备 AP |
网页配网的核心流程:设备上电后如果没有有效 WiFi 配置,自动进入 AP 模式并启动一个 Web 服务器。用户用手机连接设备的热点,在浏览器中打开配置页面(通常会自动弹出),选择 WiFi 并输入密码,设备收到后尝试连接,成功后保存配置并重启。
二、整体架构:从 AP 启动到配置保存
配网模块涉及以下几个关键组件
-
AP 热点:设备作为接入点,供用户连接。
-
DNS Captive Portal:自动劫持 DNS,将任意域名解析到设备 IP,实现配网页面自动弹出。
-
Web Server:提供 HTML 页面和 REST API。
-
异步连接 + 状态轮询:WiFi 连接不阻塞 Web 服务,前端实时获取状态。
-
Preferences(NVS):持久化存储 WiFi 凭据。
-
按键重置:长按按键清除配置,重新进入配网模式。
三、AP 热点与 DNS Captive Portal
3.1 启动 AP 热点
ESP32 可以同时工作在 STA(连接路由器)和 AP(自身作为热点)模式。配网阶段我们使用 AP+STA 模式,但 AP 是核心
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(kApSsid, kApPassword); // SSID: "XiaoZhi-AI", 密码: "12345678"
Serial.printf("AP IP address: %s\n", WiFi.softAPIP().toString().c_str()); // 通常是 192.168.4.1
密码不能为空(至少 8 位),否则部分手机无法连接。设置简单易记的密码即可。
3.2 DNS Captive Portal 实现
Captive Portal(强制门户)技术能让用户连接热点后自动弹出认证/配置页面,而不需要手动输入 IP。原理是:启动一个 DNS 服务器,将所有域名解析到设备的 AP IP。
g_dnsServer = new DNSServer();
g_dnsServer->start(53, "*", WiFi.softAPIP()); // 监听 53 端口,所有域名都解析到 AP IP
然后在主循环中不断处理 DNS 请求:
g_dnsServer->processNextRequest();
配合 Web 服务器的根路径(
/)返回配网 HTML,用户打开任意浏览器或点击弹窗就会显示配置界面。
四、Web 服务器与 API 设计
4.1 建立 Web 服务器
使用
WebServer库,监听 80 端口
g_webServer = new WebServer(80);
g_webServer->on("/", HandleRoot); // 配网主页
g_webServer->on("/connect", HTTP_POST, HandleConnect); // 提交 WiFi 凭据
g_webServer->on("/status", HandleStatus); // 查询连接状态
g_webServer->on("/scan", HandleScan); // 扫描周围 WiFi 列表
g_webServer->on("/clear", HandleClear); // 清除已保存配置
g_webServer->onNotFound(HandleRoot); // 其他路径重定向到根
g_webServer->begin();
4.2 配网页面 HTML
页面内容被转换为 C 字符串数组存储在 webconfig_html.h 中,编译时烧录到 Flash。页面包含:
-
WiFi 列表下拉框(通过 Ajax 调用
/scan填充) -
密码输入框
-
连接按钮
-
状态提示区
这样无需外置文件系统,所有资源内嵌。

4.3 WiFi 扫描接口
为了提高用户体验,让用户无需手动输入 SSID,实现了 WiFi 扫描功能。
注意:
WiFi.scanNetworks()是同步阻塞的,可能耗时 1~3 秒。为了避免影响 Web 响应,我们做了以下设计:
-
使用
g_scanning标志防止并发扫描(虽然 WebServer 单线程,但防止定时器或多次点击)。 -
扫描前确保 WiFi 模式为
WIFI_AP_STA(AP 不能关闭)。 -
返回 JSON 数组,包含
ssid、rssi、encrypted、encryptionType字段。
关键代码片段:
int n = WiFi.scanNetworks();
String json = "[";
for (int i = 0; i < n && i < 20; i++) {
json += "{\"ssid\":\"" + EscapeJsonString(WiFi.SSID(i)) + "\",";
json += "\"rssi\":" + String(WiFi.RSSI(i)) + ",";
json += "\"encrypted\":" + String(WiFi.encryptionType(i) != WIFI_AUTH_OPEN) + "}";
if (i < n-1) json += ",";
}
json += "]";
g_webServer->send(200, "application/json", json);
WiFi.scanDelete(); // 释放内存
其中
EscapeJsonString()处理 SSID 中的双引号、反斜杠等特殊字符,防止 JSON 格式错误。
4.4 接收凭据并异步连接
connect 接口收到 ssid 和 password 后,不能阻塞等待连接成功(否则浏览器会超时)。因此采用 立即返回 + 后台连接 + 前端轮询 的策略。
void HandleConnect() {
g_pendingSsid = ssid;
g_pendingPassword = password;
WiFi.begin(ssid.c_str(), password.c_str());
g_connecting = true;
g_connectSuccess = false;
g_connectStartTime = millis();
g_webServer->send(200, "application/json", "{\"success\": true}");
}
4.5 状态查询接口
前端每隔 1 秒请求 status,根据返回值更新界面
if (!g_connecting && !g_connectSuccess) status = "idle";
else if (g_connectSuccess) status = "connected";
else if (g_connecting) {
if (millis() - g_connectStartTime > 20000) { // 超时 20 秒
status = "failed";
g_connecting = false;
WiFi.disconnect();
} else {
wl_status_t wifiStatus = WiFi.status();
if (wifiStatus == WL_CONNECTED) {
status = "connected";
g_connectSuccess = true;
g_connecting = false;
SaveWifiConfig(g_pendingSsid, g_pendingPassword);
} else if (wifiStatus == WL_CONNECT_FAILED || wifiStatus == WL_NO_SSID_AVAIL) {
status = "failed";
g_connecting = false;
} else {
status = "connecting";
}
}
}
当状态变为 connected 时,前端可以跳转到成功页面,设备稍后自动重启。
五、配置持久化:Preferences 库的使用
ESP32 提供
Preferences库,用于在 NVS(Non-Volatile Storage)中存储键值对,非常适合保存 WiFi 凭据
#include <Preferences.h>
Preferences g_preferences;
const char* kNamespace = "wifi_config";
bool SaveWifiConfig(const String& ssid, const String& password) {
g_preferences.begin(kNamespace, false);
g_preferences.putString("ssid", ssid);
g_preferences.putString("password", password);
g_preferences.end();
return true;
}
bool LoadWifiConfig(String& ssid, String& password) {
g_preferences.begin(kNamespace, true);
ssid = g_preferences.getString("ssid", "");
password = g_preferences.getString("password", "");
g_preferences.end();
return ssid.length() > 0;
}
注意:
begin()的第二个参数为false表示可写,为true表示只读。保存配置后,即使断电也不会丢失
六、主循环中的配网处理
在 setup() 中调用 ConfigureWifi() 函数,其逻辑:
-
尝试加载并连接保存的 WiFi(
ConnectToSavedWifi())。 -
如果成功,直接返回,进入正常应用流程。
-
如果失败或没有保存配置,调用
StartWebConfig()进入配网模式。 -
在配网模式中,主循环不断调用
HandleWebConfig(),处理 DNS 和 Web 请求,同时检查异步连接状态。 -
一旦连接成功,延迟重启,下次启动就会加载新配置
HandleWebConfig()的非阻塞实现
void HandleWebConfig() {
if (g_isWebConfigMode && g_dnsServer && g_webServer) {
g_dnsServer->processNextRequest();
g_webServer->handleClient();
if (g_connectSuccess) {
static unsigned long lastRestartTime = 0;
if (lastRestartTime == 0) {
lastRestartTime = millis();
} else if (millis() - lastRestartTime > 5000) {
ESP.restart();
}
}
}
}
七、按键触发重新配网
ESP_ERROR_CHECK(iot_button_register_cb(
g_button_boot_handle, // Boot 按键句柄
BUTTON_PRESS_DOWN, // 按键事件:按下
nullptr,
[](void*, void* data) { // 回调函数
printf("boot button pressed\n");
ClearWifiConfig(); //清除配置后重启
delay(100);
ESP.restart();
},
nullptr));
iot_button_unregister_cb(g_button_boot_handle, BUTTON_PRESS_DOWN, nullptr); //连接成功后注销重新配网功能
在连接WiFi成功前按下Boot按键,可以强制重新配网,且在连接成功后注销该功能,不会跟后续的语音打断功能冲突
八、完整流程图

更多推荐




所有评论(0)