专栏:《WiFi 嵌入式物联网开发全套实战》 专栏定位:嵌入式 Linux/ESP32 WiFi 从原理→驱动→配网→协议→稳定性→抓包调试→量产优化全套工业实战适配:物联网设备、智能家居、工控网关、无线透传设备、4G+WiFi 双模设备 💖 点赞 + 收藏 + 关注,嵌入式 WiFi 量产实战全套教程持续更新!
本章前言
前面两章讲解了 SmartConfig、AirKiss 一键配网,这类无热点配网方案依赖广播 / 组播报文,受路由器策略、5G‑2.4G 合并 SSID、企业网络环境限制,总会出现配网失败场景。网页配网(SoftAP 配网)是 IoT 产品必备的兜底配网方案。设备开启 AP 热点,手机连接设备热点,浏览器访问设备内置 HTTP 网页,填写家里 WiFi 的 SSID 与密码提交给设备。不依赖路由器广播组播转发,不受 5G 合并路由影响,兼容性远高于一键配网。
很多 demo 只实现简单网页提交 WiFi 账号密码,量产落地会缺失大量工程逻辑:HTTP 服务健壮性、WiFi 参数 NVS 持久化存储、参数校验、异常断电保护、恢复出厂触发逻辑、配网超时退出、页面中文乱码、重复提交防护、配网失败回退配网页面。
本章完整实现 ESP32 SoftAP 网页配网全套工程:搭建嵌入式 HTTP 服务、前端网页页面、WiFi 参数接收解析、NVS 非易失存储保存网络参数、配网业务状态机、恢复出厂逻辑、超时退出配网模式、量产各类坑点。提供可直接编译完整 IDF 工程代码,覆盖从 demo 到产品的全部必要逻辑。
前置阅读:第 15 章 ESP32 AP 模式:自建热点、网页配网、最大接入设备配置。
23.1 网页配网整体业务流程
- 设备触发进入配网模式:上电检测无保存 WiFi 参数 / 用户长按按键触发恢复出厂,启动 SoftAP 热点;
- ESP32 开启 AP 模式,提供 DHCP 服务,分配 IP 地址,默认网关:
192.168.4.1; - 设备内部启动 HTTP Web 服务,监听 80 端口;
- 用户手机 WiFi 列表连接设备 AP 热点;手机浏览器自动弹窗或者手动访问
192.168.4.1; - 打开配网网页,下拉选择或者手动输入目标路由器 SSID、WiFi 密码,提交表单;
- HTTP 服务接收 POST 表单数据,解析 SSID、密码,做长度、字符合法性校验;
- 将 WiFi 配置写入 NVS 分区做持久保存;关闭 AP 模式,切换 STA 模式,尝试连接用户路由器;
- STA 连接成功获取 IP:配网完成,可以重启设备或者跳转成功页面;
- STA 连接失败:切回 AP 模式,重新返回配网网页,提示密码错误 / 路由器不可达;
- 配网模式设置超时定时器,长时间没有用户操作,自动退出配网模式;
- 按键触发恢复出厂:清除 NVS 内保存 WiFi 参数,设备重启进入配网模式。
核心优势:不依赖广播、组播报文;不受路由器 AP 隔离、广播抑制影响;5G 路由环境同样可用,作为一键配网失败后的兜底方案。 缺点:用户需要手动切换手机 WiFi 连接设备热点,操作步骤比 SmartConfig 多。
23.2 软硬件资源与组件依赖
ESP‑IDF 依赖组件开启(menuconfig)
- HTTP Server:启用
esp_http_server组件; - NVS:开启 NVS 分区,用于保存 WiFi SSID、密码;
- WiFi 组件:支持 AP‑STA 模式切换;
- DHCP 服务器:AP 模式下启用内置 DHCP 服务,为接入手机分配 IP。
业务状态机定义
typedef enum { WEB_CONFIG_STATE_IDLE, // 正常工作模式,已配网 WEB_CONFIG_STATE_AP_START, // 启动AP配网模式 WEB_CONFIG_STATE_WAIT_SUBMIT, // AP已启动,等待网页表单提交 WEB_CONFIG_STATE_TRY_CONNECT, // 收到WiFi参数,尝试STA连接路由 WEB_CONFIG_STATE_CONNECT_OK, // WiFi连接成功,配网完成 WEB_CONFIG_STATE_CONNECT_FAIL, // WiFi连接失败,退回AP配网 } web_config_state_t;23.3 HTTP 服务路由设计
| URI 路径 | 请求方法 | 功能说明 |
|---|---|---|
/ | GET | 返回配网 HTML 主页面 |
/submit | POST | 接收表单提交的 SSID、password 参数 |
/status | GET | 获取设备当前配网状态,前端轮询 |
23.4 完整可编译工程代码
#include <stdio.h> #include <string.h> #include <stdlib.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "nvs_flash.h" #include "nvs.h" #include "esp_netif.h" #include "esp_http_server.h" #include "esp_sleep.h" #define TAG "WEB_CONFIG" #define AP_SSID "ESP32_WEB_CONFIG" #define AP_PASS "12345678" #define AP_IP_ADDR "192.168.4.1" #define WEB_CONFIG_TIMEOUT_MS (120*1000) // 配网超时 2分钟 static esp_http_server_t *httpd_handle = NULL; static web_config_state_t g_web_state = WEB_CONFIG_STATE_IDLE; static EventGroupHandle_t wifi_event_group; #define WIFI_STA_CONNECTED_BIT BIT0 /* 保存的WiFi参数全局缓存 */ static char g_sta_ssid[33] = {0}; static char g_sta_password[65] = {0}; /* 简单HTML配网页面 */ static const char* html_index = R"HTML( <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device‑width, initial‑1"> <title>ESP32设备配网</title> </head> <body> <div style="text‑align:center;margin‑top:80px;"> <h2>设备WiFi配网</h2> <form action="/submit" method="post"> <div>SSID:<br><input type="text" name="ssid" maxlength="32"></div> <div>密码:<br><input type="password" name="password" maxlength="64"></div> <div><br><input type="submit" value="提交配网"></div> </form> <p>提交后设备将尝试连接WiFi,请等待</p> </div> </body> </html> )HTML"; /** * @brief NVS读写WiFi参数 */ esp_err_t wifi_param_save(const char *ssid, const char *password) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("wifi_storage", NVS_READWRITE, &nvs_handle); if(err != ESP_OK) return err; nvs_set_str(nvs_handle, "sta_ssid", ssid); nvs_set_str(nvs_handle, "sta_pass", password); nvs_commit(nvs_handle); nvs_close(nvs_handle); strncpy(g_sta_ssid, ssid, sizeof(g_sta_ssid)-1); strncpy(g_sta_password, password, sizeof(g_sta_password)-1); return ESP_OK; } esp_err_t wifi_param_load(void) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("wifi_storage", NVS_READONLY, &nvs_handle); if(err != ESP_OK) return err; size_t len = sizeof(g_sta_ssid); err = nvs_get_str(nvs_handle, "sta_ssid", g_sta_ssid, &len); len = sizeof(g_sta_password); err |= nvs_get_str(nvs_handle, "sta_pass", g_sta_password, &len); nvs_close(nvs_handle); return err; } esp_err_t wifi_param_erase(void) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("wifi_storage", NVS_READWRITE, &nvs_handle); if(err != ESP_OK) return err; nvs_erase_all(nvs_handle); nvs_commit(nvs_handle); nvs_close(nvs_handle); memset(g_sta_ssid,0,sizeof(g_sta_ssid)); memset(g_sta_password,0,sizeof(g_sta_password)); return ESP_OK; } /* http GET / 返回网页 */ esp_err_t get_index_handler(httpd_req_t *req) { httpd_resp_set_type(req, "text/html;charset=utf‑8"); httpd_resp_send(req, html_index, strlen(html_index)); return ESP_OK; } /* http POST /submit 接收表单 */ esp_err_t post_submit_handler(httpd_req_t *req) { char buf[256]; int ret = httpd_req_recv(req, buf, sizeof(buf)-1); if(ret <=0){ return ESP_FAIL; } buf[ret] = 0; char ssid[33]={0}, password[65]={0}; /* 解析表单 key=value */ sscanf(buf, "ssid=%32[^&]&password=%64s", ssid, password); ESP_LOGI(TAG,"收到提交 ssid:%s pass:%s", ssid, password); if(strlen(ssid)==0 || strlen(ssid)>32){ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "SSID参数非法"); return ESP_FAIL; } /* 保存参数,进入STA连接流程 */ wifi_param_save(ssid, password); g_web_state = WEB_CONFIG_STATE_TRY_CONNECT; httpd_resp_sendstr(req, "接收成功,设备正在尝试连接WiFi,请等待..."); return ESP_OK; } /* 启动HTTP服务 */ void start_http_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.max_open_sockets = 3; config.server_port = 80; if(httpd_start(&httpd_handle, &config) == ESP_OK){ httpd_uri_t uri_get_index = { .uri = "/", .method = HTTP_GET, .handler = get_index_handler, .user_ctx = NULL }; httpd_uri_t uri_post_submit = { .uri = "/submit", .method = HTTP_POST, .handler = post_submit_handler, .user_ctx = NULL }; httpd_register_uri_handler(httpd_handle, &uri_get_index); httpd_register_uri_handler(httpd_handle, &uri_post_submit); ESP_LOGI(TAG,"HTTP服务启动完成"); } } void stop_http_server(void) { if(httpd_handle){ httpd_stop(httpd_handle); httpd_handle = NULL; } } /* 启动AP配网模式 */ void web_config_ap_start(void) { esp_wifi_stop(); esp_netif_t *ap_netif = esp_netif_create_default_wifi_ap(); wifi_config_t ap_cfg = { .ap = { .ssid = AP_SSID, .password = AP_PASS, .max_connection = 4, .authmode = WIFI_AUTH_WPA2_PSK } }; esp_wifi_set_mode(WIFI_MODE_AP); esp_wifi_set_config(WIFI_IF_AP, &ap_cfg); esp_wifi_start(); g_web_state = WEB_CONFIG_STATE_WAIT_SUBMIT; start_http_server(); ESP_LOGI(TAG,"AP配网模式启动,热点:%s", AP_SSID); } /* 切换STA模式连接保存的WiFi */ void web_config_sta_connect(void) { esp_wifi_stop(); esp_netif_create_default_wifi_sta(); wifi_config_t sta_cfg = { .sta = { .ssid = "", .password = "", } }; strncpy((char*)sta_cfg.sta.ssid, g_sta_ssid, sizeof(sta_cfg.sta.ssid)-1); strncpy((char*)sta_cfg.sta.password, g_sta_password, sizeof(sta_cfg.sta.password)-1); esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, &sta_cfg); esp_wifi_start(); esp_wifi_connect(); ESP_LOGI(TAG,"STA开始连接:%s", g_sta_ssid); } static void wifi_event_handler(void* arg, esp_event_base_t base, int32_t id, void* data) { if(base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED){ xEventGroupClearBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); if(g_web_state == WEB_CONFIG_STATE_TRY_CONNECT){ ESP_LOGW(TAG,"STA连接失败,退回AP配网模式"); g_web_state = WEB_CONFIG_STATE_CONNECT_FAIL; } }else if(base == IP_EVENT && id == IP_EVENT_STA_GOT_IP){ xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); if(g_web_state == WEB_CONFIG_STATE_TRY_CONNECT){ g_web_state = WEB_CONFIG_STATE_CONNECT_OK; ESP_LOGI(TAG,"网页配网全部完成"); } } } void web_config_task(void *arg) { wifi_event_group = xEventGroupCreate(); esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL); esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, NULL); /* 上电加载NVS WiFi参数 */ esp_err_t ret = wifi_param_load(); if(ret != ESP_OK || strlen(g_sta_ssid)==0){ /* 无保存WiFi参数,直接进入AP配网 */ web_config_ap_start(); }else{ /* 有保存参数,尝试STA连接 */ web_config_sta_connect(); } TickType_t tick_start = xTaskGetTickCount(); while(1) { vTaskDelay(pdMS_TO_TICKS(100)); if(g_web_state == WEB_CONFIG_STATE_TRY_CONNECT) { /* 等待STA连接结果 */ xEventGroupWaitBits(wifi_event_group, WIFI_STA_CONNECTED_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(5000)); if(g_web_state == WEB_CONFIG_STATE_CONNECT_OK){ stop_http_server(); g_web_state = WEB_CONFIG_STATE_IDLE; }else if(g_web_state == WEB_CONFIG_STATE_CONNECT_FAIL){ stop_http_server(); web_config_ap_start(); } } else if(g_web_state == WEB_CONFIG_STATE_WAIT_SUBMIT) { /* 配网超时检测 */ if((xTaskGetTickCount() - tick_start) > pdMS_TO_TICKS(WEB_CONFIG_TIMEOUT_MS)) { ESP_LOGW(TAG,"网页配网超时退出"); stop_http_server(); esp_wifi_stop(); g_web_state = WEB_CONFIG_STATE_IDLE; } } } } /** * @brief 恢复出厂:清除WiFi参数,重启进入配网 */ void factory_reset(void) { wifi_param_erase(); esp_restart(); } void app_main(void) { nvs_flash_init(); esp_netif_init(); esp_event_loop_create_default(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); xTaskCreate(web_config_task, "web_cfg", 8192, NULL, 5, NULL); }23.5 代码关键点解析
- NVS 持久化与异常断电保护WiFi 账号密码写入 NVS 之后才切换 STA 模式;写 NVS 完成之后设备断电,再次上电可以直接读取参数。禁止连接成功之后才保存 NVS,防止提交完成设备断电丢失参数。
- HTTP 表单解析安全使用
sscanf做长度限制,防止超长表单输入造成缓冲区溢出;SSID 长度校验 1‑32 字节,密码 0‑64 字节。 - 配网超时机制用户进入 AP 配网之后长时间不提交,超时自动关闭 AP 与 HTTP 服务,避免设备一直停留在热点模式无法正常工作。
- 状态机流转收到网页提交参数后,关闭 AP 再启动 STA;连接失败需要重新切回 AP 模式重启 HTTP 服务,用户可以再次提交 WiFi 信息。
- 恢复出厂逻辑调用
factory_reset()清除 NVS 内存储 WiFi 配置,设备重启,上电检测无 WiFi 参数自动进入 AP 网页配网。实际产品绑定按键,长按按键触发恢复出厂函数。
23.6 量产高频坑点汇总
坑 1:手机连接设备 AP 热点,浏览器打不开 192.168.4.1
- 现象:已连上 AP,网页无法访问。
- 根因:手机私有 DNS、HTTP 劫持;手机自动切回 5G 蜂窝网络;AP 模式下最大接入数配置过小。
- 优化:页面提示用户关闭移动数据;AP
max_connection设置≥4。
坑 2:提交 WiFi 参数之后设备死机、WiFi 状态混乱
- 根因:WiFi 模式切换没有 stop,直接 set_mode;HTTP 服务没有 stop 就关闭 AP。
- 优化:模式切换顺序
esp_wifi_stop()→ 修改模式 →esp_wifi_start()。
坑 3:中文 SSID 配网乱码
- 根因:网页表单编码为 UTF‑8,WiFi 驱动内部处理编码问题。
- 优化:HTML 页面指定
<meta charset="utf‑8">;部分路由器中文 SSID 兼容性差,产品文档提示尽量使用英文 SSID。
坑 4:反复提交表单,多次写入 NVS,磨损 flash
- 根因:用户网页重复点击提交按钮,频繁调用 nvs_set_str。
- 优化:前端增加提交按钮置灰;业务层判断参数没有变化时跳过 NVS 写入。
坑 5:断电刚好发生在 NVS 写入过程,参数损坏
- 根因:写 NVS 过程中断电,NVS 条目损坏。
- 优化:上电读取 NVS 增加校验;读取失败直接判定未配网,进入配网模式。
坑 6:配网成功之后 AP 热点没有关闭
- 根因:配网完成忘记停止 http 服务、没有关闭 AP 接口。
- 危害:设备同时跑 AP+STA,功耗升高,还允许其他人访问配网页面。
- 优化:配网成功后调用
stop_http_server(),停止 AP 接口。
23.7 网页配网产品级优化方向
- 页面增加扫描周围 AP 列表,下拉选择 SSID,减少用户手动输入;
- 增加状态查询接口
/status,前端 AJAX 轮询实时展示:配网中、连接中、密码错误、连接成功; - 增加 captive portal(门户弹窗),手机连上设备 AP 自动弹出配网页面;
- NVS 增加 CRC 校验,防止存储区域异常损坏;
- 按键恢复出厂做长按消抖逻辑,短按不触发,长按 3‑5s 才执行擦除重启;
- 增加配网模式 LED 指示灯提示,LED 快闪代表进入网页配网模式。
23.8 本章小结
网页 SoftAP 配网作为一键配网的兜底方案,在整套 IoT 配网体系中不可替代。demo 只实现表单提交远远达不到量产标准,工程必须完整考虑:HTTP 服务健壮性、NVS 参数持久化与断电保护、业务状态机流转、配网超时、恢复出厂逻辑、各类异常场景回退。
实际项目中,产品配网整体策略:优先 SmartConfig 一键配网;一键配网失败引导用户切换手机 WiFi 连接设备热点,走网页配网流程。两者互相配合,最大程度提升全场景配网成功率。
💖 点赞 + 收藏 + 关注,嵌入式 WiFi 量产实战全套教程持续更新!
下一章:第 24 章 蓝牙辅助配网 BLE+WiFi 双模配网最优方案