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

目录

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

二、整体架构:从 AP 启动到配置保存

三、AP 热点与 DNS Captive Portal

3.1 启动 AP 热点 

3.2 DNS Captive Portal 实现

四、Web 服务器与 API 设计

4.1 建立 Web 服务器

4.2 配网页面 HTML

4.3 WiFi 扫描接口

4.4 接收凭据并异步连接

4.5 状态查询接口

五、配置持久化: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 是核心

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 数组,包含 ssidrssiencryptedencryptionType 字段。

关键代码片段:

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() 函数,其逻辑:

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

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

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

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

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

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

八、完整流程图

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐