ESP32 网页配网从零搭建
作者按:网上关于 ESP32 配网的教程不少,但大多直接调 IDF 的
wifi_provisioning组件,封装得太死,想改点东西很费劲。
这篇文章从头写一个 Web 配网方案,所有代码都贴出来,想怎么改都行。
为什么要自己写配网?
市面上现成的配网方案有几个:
| 方案 | 优点 | 缺点 |
|---|---|---|
ESP-IDF wifi_provisioning | 官方维护,功能全 | 依赖 protobuf,二进制交互,调试麻烦 |
| SmartConfig / ESP-TOUCH | 操作简单 | 成功率看人品,路由器兼容性差 |
| BLE 配网 | 体验好 | 得有蓝牙,还得写 App |
| 自己做 Web 配网 | 全可控,Web 页面随心改 | 多写几行代码 |
我选 Web 配网的原因很简单:用户只要有浏览器就行,不管是 iPhone 还是 Android,连上 ESP32 发的热点,打开网页点两下就完事。
整体思路
设备上电后干两件事:
- 看看 NVS 里有没有存 WiFi 账号密码
- 有 → 试着连一下那个 WiFi
- 连上了 → 正常工作
- 连不上 → 擦掉旧的,进配网模式
- 没有存过 → 直接进配网模式
配网模式就是:
开一个 WiFi 热点(ESP32-Config)
├── 启动 DNS 服务器(所有域名指到 192.168.4.1)
└── 启动 Web 服务器
├── GET / → 返回配网页
├── GET /api/scan → 扫描附近 WiFi,返回 JSON 列表
└── POST /save → 保存 WiFi 密码,重启
用户要做的事:
打开手机 WiFi → 连 ESP32-Config → 弹出/访问 192.168.4.1
→ 点选你家 WiFi → 输密码 → 提交 → 等重启
完整代码
整个项目就一个 web_prov.c 文件,500 行左右。我全部贴出来,每一段简单说下在干嘛。
头文件和配置
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_system.h>
#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_log.h>
#include <esp_err.h>
#include <nvs_flash.h>
#include <nvs.h>
#include <lwip/err.h>
#include <lwip/sys.h>
#include <lwip/netdb.h>
#include <lwip/dns.h>
#include <lwip/sockets.h>
#include <lwip/ip_addr.h>
#include <esp_http_server.h>
#define AP_SSID "ESP32-Config"
#define AP_PASSWORD NULL
#define AP_MAX_CONN 4
#define NVS_NAMESPACE "wifi_config"
#define NVS_KEY_SSID "ssid"
#define NVS_KEY_PASSWORD "password"
static const char *TAG = "web_prov";
说几个细节:
AP_PASSWORD我设的 NULL,也就是开放热点。想加密码直接改,比如"12345678"- NVS 那个命名空间
wifi_config注意别跟其他 NVS 数据冲突,不然nvs_erase_all会把你别的东西也清掉
NVS 读写
static esp_err_t load_creds_from_nvs(void)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs);
if (err != ESP_OK) return err;
static char saved_ssid[32], saved_password[64];
size_t len = sizeof(saved_ssid);
err = nvs_get_str(nvs, NVS_KEY_SSID, saved_ssid, &len);
if (err != ESP_OK) { nvs_close(nvs); return err; }
len = sizeof(saved_password);
err = nvs_get_str(nvs, NVS_KEY_PASSWORD, saved_password, &len);
nvs_close(nvs);
return err;
}
static esp_err_t save_creds_to_nvs(const char *ssid, const char *password)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs);
if (err != ESP_OK) return err;
nvs_set_str(nvs, NVS_KEY_SSID, ssid);
nvs_set_str(nvs, NVS_KEY_PASSWORD, password);
err = nvs_commit(nvs);
nvs_close(nvs);
return err;
}
static void clear_creds_from_nvs(void)
{
nvs_handle_t nvs;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_erase_all(nvs);
nvs_commit(nvs);
nvs_close(nvs);
}
}
NVS 这东西看着简单,有个坑:写完要调 nvs_commit,不然断电就丢了。另外 nvs_get_str 的 len 参数既是输入也是输出——输入时告诉函数 buffer 大小,返回时告诉你实际长度。
WiFi 初始化和 AP 模式
static void wifi_init(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
}
static void wifi_ap_start(void)
{
ESP_LOGI(TAG, "Starting AP: %s", AP_SSID);
esp_netif_create_default_wifi_ap();
wifi_config_t cfg = {
.ap = {
.ssid_len = strlen(AP_SSID),
.max_connection = AP_MAX_CONN,
.authmode = WIFI_AUTH_OPEN,
},
};
strncpy((char *)cfg.ap.ssid, AP_SSID, sizeof(cfg.ap.ssid));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &cfg));
ESP_ERROR_CHECK(esp_wifi_start());
}
注意这里 WIFI_INIT_CONFIG_DEFAULT() 是个宏,不能直接塞进函数参数里,得先赋值给一个变量再传地址。我之前就被这个坑过。
WiFi 扫描(最核心的部分)
static SemaphoreHandle_t s_scan_done = NULL;
static int s_scan_count = 0;
static void scan_done_handler(void *arg, esp_event_base_t base,
int32_t id, void *event_data)
{
s_scan_count = 0;
esp_wifi_scan_get_ap_num(&s_scan_count);
if (s_scan_done) xSemaphoreGive(s_scan_done);
}
static esp_err_t scan_get_handler(httpd_req_t *req)
{
// AP 模式下想扫描 WiFi,得先切到混合模式
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
vTaskDelay(pdMS_TO_TICKS(100));
s_scan_done = xSemaphoreCreateBinary();
ESP_ERROR_CHECK(esp_event_handler_register(
WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler, NULL));
wifi_scan_config_t scan_cfg = {
.show_hidden = false,
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
};
ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_cfg, false));
xSemaphoreTake(s_scan_done, pdMS_TO_TICKS(10000));
vSemaphoreDelete(s_scan_done);
s_scan_done = NULL;
uint16_t count = s_scan_count;
wifi_ap_record_t *records = calloc(count, sizeof(wifi_ap_record_t));
if (records) {
esp_wifi_scan_get_ap_records(&count, records);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
char buf[512];
int len = snprintf(buf, sizeof(buf), "{\"networks\":[");
httpd_resp_send_chunk(req, buf, len);
for (int i = 0; i < (int)count; i++) {
if (strlen((char *)records[i].ssid) == 0) continue;
// 转义 JSON 特殊字符
char escaped_ssid[64];
int ei = 0;
for (int si = 0; records[i].ssid[si] && ei < 62; si++) {
if (records[i].ssid[si] == '"' || records[i].ssid[si] == '\\')
escaped_ssid[ei++] = '\\';
escaped_ssid[ei++] = records[i].ssid[si];
}
escaped_ssid[ei] = '\0';
int rssi = records[i].rssi;
const char *auth = "OPEN";
if (records[i].authmode == WIFI_AUTH_WPA2_PSK) auth = "WPA2";
else if (records[i].authmode == WIFI_AUTH_WPA3_PSK) auth = "WPA3";
else if (records[i].authmode == WIFI_AUTH_WPA_WPA2_PSK) auth = "WPA/WPA2";
int bar = (rssi + 100) / 10;
if (bar < 0) bar = 0;
if (bar > 10) bar = 10;
len = snprintf(buf, sizeof(buf),
"%s{\"ssid\":\"%s\",\"rssi\":%d,\"auth\":\"%s\",\"bar\":%d}",
(i > 0) ? "," : "", escaped_ssid, rssi, auth, bar);
httpd_resp_send_chunk(req, buf, len);
}
free(records);
esp_wifi_set_mode(WIFI_MODE_AP);
httpd_resp_send_chunk(req, "]}", 2);
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
说几个容易翻车的地方:
- AP 模式下不能直接扫 WiFi,得先切到
WIFI_MODE_APSTA,扫完再切回来 - 扫描是异步的,
esp_wifi_scan_start第二个参数false表示非阻塞。所以需要信号量等回调 - 扫描结果里的 SSID 可能有特殊字符(比如引号),组 JSON 时得转义
- 信号强度转成信号格数的算法很简单:
(rssi + 100) / 10,-100dBm = 0格,-50dBm = 5格,-10dBm = 9格。实际没那么精确,但够用了
配网页(HTML + JS)
static esp_err_t root_get_handler(httpd_req_t *req)
{
const char *html =
"<!DOCTYPE html><html><head>"
"<meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>ESP32 配网</title>"
"<style>"
"body{font-family:Arial,sans-serif;max-width:500px;margin:30px auto;padding:20px;background:#f5f5f5}"
"h2{color:#333;text-align:center}"
".loading{text-align:center;padding:30px;color:#999}"
".scan-btn{display:block;width:100%;padding:10px;background:#6c757d;color:#fff;border:none;"
"border-radius:4px;font-size:14px;cursor:pointer;margin:10px 0}"
".scan-btn:hover{background:#5a6268}"
".network-list{list-style:none;padding:0}"
".network-list li{padding:12px;margin:6px 0;background:#fff;border:2px solid #ddd;"
"border-radius:6px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}"
".network-list li:hover,.network-list li.selected{border-color:#007bff;background:#f0f7ff}"
".network-list li .ssid{font-weight:bold;font-size:16px}"
".network-list li .rssi{color:#666;font-size:13px}"
"</style></head><body>"
"<h2>🔧 WiFi 配网</h2>"
"<p style='text-align:center;color:#666'>选择网络,输入密码</p>"
"<button class='scan-btn' onclick='scan()'>🔄 重新扫描</button>"
"<div id='loading' class='loading'>🔍 扫描中...</div>"
"<form id='form' action='/save' method='post' style='display:none'>"
"<ul id='list' class='network-list'></ul>"
"<input type='hidden' name='ssid' id='selSsid'>"
"<p><input type='password' name='password' id='pw' "
"placeholder='WiFi 密码(开放网络可不填)' "
"style='width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:16px;box-sizing:border-box'></p>"
"<input type='submit' value='🚀 连接' "
"style='width:100%;padding:12px;background:#007bff;color:#fff;border:none;"
"border-radius:4px;font-size:16px;cursor:pointer'>"
"</form>"
"<script>"
"function scan(){"
" var l=document.getElementById('loading');l.style.display='block';"
" document.getElementById('form').style.display='none';"
" fetch('/api/scan').then(function(r){return r.json()}).then(function(d){"
" l.style.display='none';var list=document.getElementById('list');list.innerHTML='';"
" if(!d.networks||d.networks.length==0){alert('附近没扫到 WiFi');return;}"
" d.networks.sort(function(a,b){return b.rssi-a.rssi});"
" d.networks.forEach(function(n){"
" var bars='';for(var b=0;b<10;b++)bars+=b<n.bar?'█':'░';"
" var li=document.createElement('li');"
" li.onclick=function(){"
" document.querySelectorAll('#list li').forEach(function(l){l.className=''});"
" this.className='selected';"
" document.getElementById('selSsid').value=n.ssid;"
" document.getElementById('pw').focus();"
" };"
" li.innerHTML='<span>'+n.ssid+'</span><span style=color:#666>'+bars+' '+n.rssi+'dBm '+n.auth+'</span>';"
" list.appendChild(li);"
" });"
" document.getElementById('form').style.display='block';"
" }).catch(function(){l.style.display='none';alert('扫描失败');});"
"}"
"scan();"
"</script></body></html>";
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, html, strlen(html));
return ESP_OK;
}
这个页面没什么特别的,就是一个嵌入式单页应用:
- 页面加载就调
scan()扫 WiFi - 结果按信号排序,渲染成列表
- 点选某个网络,SSID 自动写入隐藏表单
- 填密码,提交
我故意没加什么花哨的 CSS 框架,就原生 HTML,体积小,加载快。
表单处理
static esp_err_t save_post_handler(httpd_req_t *req)
{
char buf[256] = {0};
int remaining = req->content_len;
if (remaining >= (int)sizeof(buf)) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Data too long");
return ESP_FAIL;
}
int ret = httpd_req_recv(req, buf, remaining);
if (ret <= 0) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Read failed");
return ESP_FAIL;
}
buf[ret] = '\0';
char ssid[32] = {0}, password[64] = {0};
char *p = buf, *key, *val;
while (*p) {
while (*p == ' ' || *p == '\r' || *p == '\n') p++;
key = p;
while (*p && *p != '=' && *p != '&') p++;
if (*p == '=') {
*p++ = '\0';
val = p;
while (*p && *p != '&') p++;
if (*p == '&') *p++ = '\0';
char decoded[64];
int di = 0;
for (int si = 0; val[si] && di < 63; si++) {
if (val[si] == '%' && val[si+1] && val[si+2]) {
char hex[3] = {val[si+1], val[si+2], 0};
decoded[di++] = strtol(hex, NULL, 16);
si += 2;
} else if (val[si] == '+') {
decoded[di++] = ' ';
} else {
decoded[di++] = val[si];
}
}
decoded[di] = '\0';
if (strcmp(key, "ssid") == 0) strncpy(ssid, decoded, sizeof(ssid)-1);
else if (strcmp(key, "password") == 0) strncpy(password, decoded, sizeof(password)-1);
} else {
if (*p) p++;
}
}
if (strlen(ssid) == 0) {
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req,
"<html><body><h2>没选 WiFi</h2><a href='/'>回去重选</a></body></html>", -1);
return ESP_OK;
}
ESP_LOGI(TAG, "Saving: ssid=%s", ssid);
save_creds_to_nvs(ssid, password);
httpd_resp_set_type(req, "text/html");
httpd_resp_send_chunk(req,
"<html><head><meta charset='utf-8'><meta http-equiv='refresh' content='5'>"
"<title>OK</title><style>body{font-family:sans-serif;text-align:center;margin:50px}"
"h2{color:#28a745}</style></head>"
"<body><h2>✅ 已保存</h2><p>设备重启中,稍后会连 ", -1);
httpd_resp_send_chunk(req, ssid, strlen(ssid));
httpd_resp_send_chunk(req, "</p></body></html>", -1);
httpd_resp_send_chunk(req, NULL, 0);
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK;
}
这里自己手写了一个 URL 解码器,因为 ESP 的 HTTP 服务器不会自动解码 form 数据。其实就是把 %20 转成空格、+ 转成空格,其他正常字符保持原样。
DNS 服务器(Captive Portal 关键)
static void dns_server_task(void *pvParameters)
{
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock < 0) { ESP_LOGE(TAG, "DNS socket failed"); vTaskDelete(NULL); return; }
struct sockaddr_in sa = {
.sin_family = AF_INET,
.sin_port = htons(53),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
close(sock); vTaskDelete(NULL); return;
}
uint8_t buf[512];
struct sockaddr_in client;
socklen_t clen = sizeof(client);
while (1) {
int len = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&client, &clen);
if (len < 12) continue;
buf[2] |= 0x80; // 标记为响应
int qlen = len;
buf[qlen++] = 0xC0; buf[qlen++] = 0x0C; // 名字指针
buf[qlen++] = 0x00; buf[qlen++] = 0x01; // A 记录
buf[qlen++] = 0x00; buf[qlen++] = 0x01; // Class IN
buf[qlen++] = 0x00; buf[qlen++] = 0x00;
buf[qlen++] = 0x00; buf[qlen++] = 0x3C; // TTL 60s
buf[qlen++] = 0x00; buf[qlen++] = 0x04; // 数据长度 4
buf[qlen++] = 192; buf[qlen++] = 168;
buf[qlen++] = 4; buf[qlen++] = 1; // 192.168.4.1
buf[6] = 0; buf[7] = 1; // 回答数 = 1
sendto(sock, buf, qlen, 0,
(struct sockaddr *)&client, clen);
}
}
这个 DNS 服务器极其简陋,不解析具体域名,所有查询一律返回 192.168.4.1。好处是用户连上热点后不管访问什么地址都被导向配网页。
需要注意:DNS 是 UDP 端口 53,有些操作系统(macOS Ventura+)会限制非 root 进程绑定 53 端口。但在 ESP32 上跑没问题。
STA 连接(配网成功后自动执行)
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static EventGroupHandle_t s_sta_eg = NULL;
static int s_sta_retry = 0;
static void sta_evt_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_sta_retry < 3) {
s_sta_retry++;
esp_wifi_connect();
} else {
xEventGroupSetBits(s_sta_eg, WIFI_FAIL_BIT);
}
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *e = (ip_event_got_ip_t *)data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&e->ip_info.ip));
xEventGroupSetBits(s_sta_eg, WIFI_CONNECTED_BIT);
}
}
这里用事件组来阻塞等待连接结果,最大超时 15 秒,断开重试 3 次。这个逻辑你可以改,比如改成永不超时、后台重试之类的。
main 函数(整合所有逻辑)
void app_main(void)
{
// 1. 初始化 NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 2. 读 NVS,有凭证就尝试 STA
if (load_creds_from_nvs() == ESP_OK) {
ESP_LOGI(TAG, "有保存的凭证,尝试 STA...");
wifi_init();
esp_netif_create_default_wifi_sta();
s_sta_eg = xEventGroupCreate();
s_sta_retry = 0;
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&sta_evt_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&sta_evt_handler, NULL);
wifi_config_t wc = { .sta = { .threshold.authmode = WIFI_AUTH_WPA2_PSK } };
strncpy((char *)wc.sta.ssid, g_saved_ssid, sizeof(wc.sta.ssid));
strncpy((char *)wc.sta.password, g_saved_password, sizeof(wc.sta.password));
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &wc);
esp_wifi_start();
EventBits_t bits = xEventGroupWaitBits(s_sta_eg,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE, pdFALSE, pdMS_TO_TICKS(15000));
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "STA 连上了,正常运行");
return;
}
ESP_LOGW(TAG, "连不上,擦掉凭证,进配网模式");
clear_creds_from_nvs();
esp_wifi_stop();
esp_wifi_deinit();
}
// 3. 启动 AP + Web 配网门户
wifi_init();
wifi_ap_start();
xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 5, NULL);
// 注册 HTTP 路由
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.lru_purge_enable = true;
httpd_handle_t server = NULL;
httpd_start(&server, &config);
httpd_register_uri_handler(server, &(httpd_uri_t){
.uri = "/", .method = HTTP_GET, .handler = root_get_handler });
httpd_register_uri_handler(server, &(httpd_uri_t){
.uri = "/save", .method = HTTP_POST, .handler = save_post_handler });
httpd_register_uri_handler(server, &(httpd_uri_t){
.uri = "/api/scan", .method = HTTP_GET, .handler = scan_get_handler });
ESP_LOGI(TAG, "配网门户已启动: AP=%s IP=192.168.4.1", AP_SSID);
}
启动逻辑翻译成人话就是:
开机 → 查有没有 WiFi 密码
├── 有 → 连
│ ├── 连上 → 干活
│ └── 连不上 → 擦密码,开热点等配置
└── 没有 → 开热点等配置
CMakeLists.txt
主工程的:
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(web_prov)
main 目录下的:
idf_component_register(SRCS "web_prov.c"
INCLUDE_DIRS "."
REQUIRES nvs_flash esp_wifi esp_netif esp_event
esp_http_server lwip)
注意 REQUIRES 里要写 esp_http_server,不然编译报 undefined reference。
编译和烧录
环境变量设好(路径根据自己的 IDF 安装位置改):
$env:IDF_PATH = "D:\Espressif_532"
$env:IDF_TOOLS_PATH = "D:\Espressif_532_tool"
$env:IDF_PYTHON_ENV_PATH = "D:\Espressif_532_tool\python_env\idf5.3_py3.11_env"
$python = "$env:IDF_PYTHON_ENV_PATH\Scripts\python.exe"
$idf_py = "$env:IDF_PATH\tools\idf.py"
设置目标芯片:
& $python $idf_py set-target esp32s3
编译:
& $python $idf_py build
烧录(COM19 是你设备上的串口号):
& $python -m esptool --chip esp32s3 -p COM19 -b 460800 `
write_flash --flash_mode dio --flash_size 2MB --flash_freq 80m `
0x0 build/bootloader/bootloader.bin `
0x8000 build/partition_table/partition-table.bin `
0x10000 build/web_prov.bin
如果你是 ESP32、ESP32-C3 等其他芯片,把 esp32s3 改成对应的就行。
使用体验
连上 ESP32-Config 之后,配网页面长这样:
┌──────────────────────────────────┐
│ 🔧 WiFi 配网 │
│ 选择网络,输入密码 │
│ │
│ [🔄 重新扫描] │
│ │
│ ┌ 我家WiFi_5G ████... │
│ ├ CMCC-XXXX ██.... │
│ ├ TP-LINK_XXX █...... │
│ └ ... │
│ │
│ [·······························]│
│ WiFi 密码 │
│ │
│ [ 🚀 连接 ] │
└──────────────────────────────────┘
点一个网络,输密码,提交,等重启,完事。
可以怎么改进
这个版本功能已经够用了,如果想折腾,可以从这几个方向下手:
- Web 页面美化 — 现在就是纯文本,可以加图标、动画、进度条
- 记住多个 WiFi — 现在是只存一个,可以改成存多个轮着连
- 配网超时自动关 AP — 比如 5 分钟没人连热点就关掉省电
- OTA + 配网 — 配网成功后顺便检查固件更新
- 二维码配网 — 生成 QR Code,手机扫码直接传 WiFi 信息,连网页都不用打开
- 接入微信小程序 — 把 /api/scan 的 JSON 接口给小程序用
踩坑记录
最后记几个编译时容易碰到的问题:
- WIFI_INIT_CONFIG_DEFAULT 不能直接当参数传 — 得先赋值给局部变量再传
&cfg - AP 模式下 rssi 扫描不到 — 先切
WIFI_MODE_APSTA,扫完再切回来 - 中文 SSID — ESP-IDF 的 TCP/IP 栈对中文 SSID 支持没问题,但 JSON 转义要注意
- DNS 端口被占用 — 某些 sdkconfig 默认会开 mDNS,记得关或改端口
- 栈溢出 — HTTP server 的 handler 默认栈可能不够,可以调
HTTPD_DEFAULT_CONFIG()里的stack_size