零知派——ESP32-S3 AI 小智 使用 Preferences NVS 实现Web配网持久化

✔零知派(零知开源)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知开源编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知派(零知开源)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!

目录

一、为什么需要网页配网?

[二、整体架构:从 AP 启动到配置保存](#二、整体架构:从 AP 启动到配置保存)

[三、AP 热点与 DNS Captive Portal](#三、AP 热点与 DNS Captive Portal)

[3.1 启动 AP 热点](#3.1 启动 AP 热点)

[3.2 DNS Captive Portal 实现](#3.2 DNS Captive Portal 实现)

[四、Web 服务器与 API 设计](#四、Web 服务器与 API 设计)

[4.1 建立 Web 服务器](#4.1 建立 Web 服务器)

[4.2 配网页面 HTML](#4.2 配网页面 HTML)

[4.3 WiFi 扫描接口](#4.3 WiFi 扫描接口)

[4.4 接收凭据并异步连接](#4.4 接收凭据并异步连接)

[4.5 状态查询接口](#4.5 状态查询接口)

[五、配置持久化:Preferences 库的使用](#五、配置持久化:Preferences 库的使用)

六、主循环中的配网处理

七、按键触发重新配网

八、完整流程图


项目概述

本方案利用零知派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 是核心

cpp 复制代码
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。

cpp 复制代码
g_dnsServer = new DNSServer();
g_dnsServer->start(53, "*", WiFi.softAPIP());   // 监听 53 端口,所有域名都解析到 AP IP

然后在主循环中不断处理 DNS 请求:

cpp 复制代码
g_dnsServer->processNextRequest();

配合 Web 服务器的根路径(/)返回配网 HTML,用户打开任意浏览器或点击弹窗就会显示配置界面。

四、Web 服务器与 API 设计

4.1 建立 Web 服务器

使用 WebServer 库,监听 80 端口

cpp 复制代码
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 数组,包含 ssidrssiencryptedencryptionType 字段。

关键代码片段:

cpp 复制代码
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 接口收到 ssidpassword 后,不能阻塞等待连接成功(否则浏览器会超时)。因此采用 立即返回 + 后台连接 + 前端轮询 的策略。

cpp 复制代码
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,根据返回值更新界面

cpp 复制代码
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 凭据

cpp 复制代码
#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() 函数,其逻辑:

  1. 尝试加载并连接保存的 WiFi(ConnectToSavedWifi())。

  2. 如果成功,直接返回,进入正常应用流程。

  3. 如果失败或没有保存配置,调用 StartWebConfig() 进入配网模式。

  4. 在配网模式中,主循环不断调用 HandleWebConfig(),处理 DNS 和 Web 请求,同时检查异步连接状态。

  5. 一旦连接成功,延迟重启,下次启动就会加载新配置

HandleWebConfig() 的非阻塞实现

cpp 复制代码
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();
            }
        }
    }
}

七、按键触发重新配网

cpp 复制代码
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按键,可以强制重新配网,且在连接成功后注销该功能,不会跟后续的语音打断功能冲突

八、完整流程图

相关推荐
阿亮爱学代码2 小时前
日期与滚动视图
java·前端·scrollview
欧米欧2 小时前
STRING的底层实现
前端·c++·算法
2301_814809862 小时前
踩坑实战pywebview:用 Python + Web 技术打造轻量级桌面应用
开发语言·前端·python
LIO2 小时前
Vue 3 实战——搜索框检索高亮的优雅实现
前端·vue.js
_thought2 小时前
踩坑记录:Vue Devtools(Vue2版)在火狐浏览器上,未在控制台显示
前端·javascript·vue.js
pancakenut2 小时前
从盒模型到画布:以mapbox为例
前端·canvas
珎珎啊2 小时前
前端-闭包
前端
军军君012 小时前
数字孪生监控大屏实战模板:交通云实时数据监控平台
前端·javascript·css·vue.js·typescript·前端框架·echarts
DanCheOo2 小时前
从脚本到 CLI 工具:用 Node.js 打造你的第一个 AI 命令行工具
前端·aigc