
专栏《WiFi 嵌入式物联网开发全套实战》 专栏定位嵌入式 Linux/ESP32 WiFi 从原理→驱动→配网→协议→稳定性→抓包调试→量产优化全套工业实战适配物联网设备、智能家居、工控网关、无线透传设备、4GWiFi 双模设备 点赞 收藏 关注嵌入式 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 依赖组件开启menuconfigHTTP 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 主页面/submitPOST接收表单提交的 SSID、password 参数/statusGET获取设备当前配网状态前端轮询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 RHTML( !DOCTYPE html html head meta charsetutf-8 meta nameviewport contentwidthdevice‑width, initial‑1 titleESP32设备配网/title /head body div styletext‑align:center;margin‑top:80px; h2设备WiFi配网/h2 form action/submit methodpost divSSID:brinput typetext namessid maxlength32/div div密码:brinput typepassword namepassword maxlength64/div divbrinput typesubmit 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;charsetutf‑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}; /* 解析表单 keyvalue */ 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 模式下最大接入数配置过小。优化页面提示用户关闭移动数据APmax_connection设置≥4。坑 2提交 WiFi 参数之后设备死机、WiFi 状态混乱根因WiFi 模式切换没有 stop直接 set_modeHTTP 服务没有 stop 就关闭 AP。优化模式切换顺序esp_wifi_stop()→ 修改模式 →esp_wifi_start()。坑 3中文 SSID 配网乱码根因网页表单编码为 UTF‑8WiFi 驱动内部处理编码问题。优化HTML 页面指定meta charsetutf‑8部分路由器中文 SSID 兼容性差产品文档提示尽量使用英文 SSID。坑 4反复提交表单多次写入 NVS磨损 flash根因用户网页重复点击提交按钮频繁调用 nvs_set_str。优化前端增加提交按钮置灰业务层判断参数没有变化时跳过 NVS 写入。坑 5断电刚好发生在 NVS 写入过程参数损坏根因写 NVS 过程中断电NVS 条目损坏。优化上电读取 NVS 增加校验读取失败直接判定未配网进入配网模式。坑 6配网成功之后 AP 热点没有关闭根因配网完成忘记停止 http 服务、没有关闭 AP 接口。危害设备同时跑 APSTA功耗升高还允许其他人访问配网页面。优化配网成功后调用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 章 蓝牙辅助配网 BLEWiFi 双模配网最优方案