1. 为什么改个 WiFi 密码要折腾重刷固件
搞过 ESP32 项目的人大概率都遇到过这个场景:设备已经装到现场了,外壳封好、螺丝拧死,甚至用热熔胶固定在某个角落,结果甲方或者自己突然想换个 WiFi 密码。这时候你打开 Arduino IDE 或者 PlatformIO,改一行ssid和password,重新编译,然后抱着笔记本和 USB 线跑到设备跟前,拆壳、接线、按住 BOOT 键、烧录、等重启。整个过程十分钟起步,如果设备装在吊顶或者配电箱里,那酸爽程度直接翻倍。
这个痛点的根源在于:大多数人把 WiFi 配置信息硬编码在固件里。const char* ssid = "MyWiFi";这种写法编译之后就是二进制的一部分,改它等于改固件。而 ESP32 其实自带了一块叫NVS(Non-Volatile Storage)的非易失存储区域,专门用来存这种"运行时可变的配置数据"。WiFi 的 SSID、密码、服务器地址、设备 ID 这些东西,天生就该放在 NVS 里,而不是烧死在固件中。
那为什么大家还是习惯硬编码?因为改 NVS 传统上也不方便——你得写串口命令、或者跑一段专门的代码去写。直到有人想到:ESP32 本身就能开一个 Web 服务器,那我为什么不做个浏览器页面,连上设备之后直接在网页里改 NVS 键值?这就是标题里说的"浏览器工具"的核心思路。
这篇文章要聊的就是这套方案的完整落地:怎么在 ESP32 上跑一个轻量 Web 服务,怎么把 NVS 的读写暴露成 HTTP 接口,怎么做一个能直接改 WiFi 密码的网页,以及实际部署时会踩哪些坑。适合已经会用 Arduino 或 ESP-IDF 点灯、但对 NVS 和 Web 服务还不太熟的朋友,也适合正在做量产设备、需要现场可配置方案的人。核心关键词就四个:ESP32、WiFi、NVS、浏览器工具,全文围绕它们展开。
2. 方案整体设计与技术选型拆解
2.1 为什么是 NVS 而不是 SPIFFS 或 EEPROM
ESP32 上能持久化存数据的地方主要有三个:NVS、SPIFFS/LittleFS、以及模拟 EEPROM。选哪个不是拍脑袋决定的,得看数据特征。
NVS 是乐鑫专门为键值对设计的存储层,底层跑在 flash 的一个独立分区上,自带磨损均衡和掉电保护。它的 API 就是nvs_set_str、nvs_get_str这种,读写一个字符串键值非常直接。WiFi 密码、MQTT 服务器地址、设备别名这类"小、散、经常改"的数据,放 NVS 是最合适的。
SPIFFS 或 LittleFS 是文件系统,适合存网页资源、日志、图片这类"大块、按文件组织"的数据。你当然可以把配置写成一个 JSON 文件存进去,但为了改一个密码去读写整个文件,属于杀鸡用牛刀,而且文件系统的掉电一致性不如 NVS 稳。
模拟 EEPROM 是 Arduino 框架为了兼容老代码提供的抽象,底层其实也是 flash,但它是按字节偏移寻址的,没有键值概念。你要自己管理"第 0 到 32 字节是 SSID,第 33 到 96 字节是密码"这种布局,改起来容易越界,也不优雅。
所以结论很明确:配置类数据用 NVS,网页资源用文件系统(或者干脆内嵌成字符串),两者分工。这套浏览器工具改的就是 NVS 里的键值。
2.2 Web 服务器选型:同步还是异步
ESP32 上跑 Web 服务有两条主流路线。
一条是 Arduino 自带的WebServer库(旧版叫ESP8266WebServer移植过来的),它是同步阻塞的。每个请求处理完才处理下一个,代码写起来直观,但如果有多个请求并发(比如网页加载时同时请求 CSS、JS、图标),响应会排队,体验略卡。
另一条是ESPAsyncWebServer,异步非阻塞,基于事件回调。它能同时处理多个连接,页面加载明显更顺,而且支持 WebSocket、Server-Sent Events 这些高级玩法。代价是依赖AsyncTCP,代码结构稍微绕一点,回调里不能做太耗时的操作。
对于这个改配置的工具,我推荐ESPAsyncWebServer。原因很实际:配置页面通常会有几个静态资源加几个 AJAX 请求,同步服务器在这种场景下容易出现"点了保存没反应"的假死感,异步的体验好太多。而且异步库对 POST 请求体的处理更干净,解析表单数据不容易出幺蛾子。
2.3 配置页面放哪:内嵌字符串 vs 文件系统
网页的 HTML/CSS/JS 有三种放法:
第一种是直接内嵌成 C 字符串,用R"rawliteral(...)rawliteral"包起来。优点是编译进固件,不依赖文件系统,烧一次就全有了,最省心。缺点是改网页要重新编译,而且大页面会占不少 flash。
第二种是放 SPIFFS/LittleFS,用server.serveStatic提供。优点是网页和固件解耦,改页面只要重新上传文件系统镜像,不用动固件。缺点是多了个上传步骤,量产时容易漏。
第三种是放外部服务器,设备只提供 API。优点是页面随便改,缺点是设备必须能上网,而且首次配网时设备还没联网,鸡生蛋问题。
对于"改 WiFi 密码"这个场景,首次配网时设备根本没联网,所以页面必须由设备自己提供。我倾向第一种——内嵌字符串。因为配置页面本身不复杂,一个表单加一段 fetch 请求,压缩后也就几 KB,内嵌完全扛得住,而且部署最简单,不会出现"固件烧了但文件系统忘了传"的经典事故。
2.4 安全边界:这个工具能开多大口子
这里必须泼一盆冷水。一个能改 NVS 的 Web 接口,本质上就是设备的远程配置后门。如果它无条件对外开放,任何连上同一网络的人都能改你的 WiFi 密码、服务器地址,甚至把设备指向恶意服务器。
所以设计时必须划清边界:
- 只在配网模式或特定触发条件下开启这个配置服务,正常运行时关掉或者只监听局域网。
- 加一层简单认证,哪怕就是一个固定 token 或者一次性密码,也比裸奔强。
- 限制可写的键,不要做成"任意键值都能改"的通用接口,而是白名单——只允许改
wifi_ssid、wifi_pass、server_url这几个。 - 改完 WiFi 后要有回退机制,新密码连不上时自动恢复旧配置,否则设备直接失联。
这几点在后面实操部分会具体展开。记住一句话:方便和安全永远是一对矛盾,工具越好用,越要想清楚谁能用。
3. NVS 核心机制与读写实操要点
3.1 NVS 的命名空间与键值模型
NVS 的数据组织是两层结构:命名空间(namespace)下面挂键值对(key-value)。你可以把命名空间理解成文件夹,键就是文件名,值就是文件内容。不同命名空间下的同名键互不干扰,这点很重要——比如 WiFi 模块和你的业务逻辑可以各用各的命名空间,不会打架。
常用的 API 有这么几个(Arduino 环境下用Preferences库封装,更简单):
#include <Preferences.h> Preferences prefs; // 打开命名空间,false 表示读写模式 prefs.begin("wifi_cfg", false); // 写字符串 prefs.putString("ssid", "MyHomeWiFi"); prefs.putString("pass", "12345678"); // 读字符串,第二个参数是默认值(键不存在时返回) String ssid = prefs.getString("ssid", "default_ssid"); // 删除某个键 prefs.remove("pass"); // 关闭,释放句柄 prefs.end();底层对应的其实是nvs_open、nvs_set_str、nvs_get_str这套 C API,Preferences只是包了一层,用起来更顺手。如果你用 ESP-IDF 原生开发,直接调 C API 也行,逻辑一样。
注意:NVS 的键名有长度限制,最长 15 个字符(不含结尾的
\0)。命名空间名最长 15 个字符。超了会返回错误,而且这个错误在Preferences里可能被静默吞掉,导致你以为写成功了其实没写进去。我踩过这个坑,键名写了个wifi_password_backup,16 个字符,结果死活读不出来,查了半天才发现是长度问题。
3.2 值的类型与容量限制
NVS 支持的类型挺全:u8、i8、u16、i16、u32、i32、u64、i64、string、blob。字符串和 blob 是变长的,其他是定长。
容量上,单个字符串值理论上限是4000 字节左右(受分区大小和页结构影响),实际用的时候别超过几百字节,否则写入会变慢,而且容易触发碎片整理。WiFi 密码撑死 63 个字符,完全不用担心。
但有个细节要注意:NVS 是按页(page)管理的,每页 4KB,一个命名空间的数据尽量集中。如果你频繁地写、删、再写,会产生碎片,NVS 内部会做垃圾回收(GC),GC 的时候会短暂阻塞。对于配置这种低频写入,完全无感;但如果你拿 NVS 当高频日志存储,那就会出问题。
3.3 掉电安全与写入时机
NVS 的一大卖点就是掉电安全。它采用"写新页、标记旧页失效"的方式,写入过程中掉电,要么新数据生效,要么旧数据保留,不会出现写一半的脏数据。这是它比裸写 flash 强的地方。
但这不代表你可以随便写。有两点经验:
第一,不要在中断里写 NVS。NVS 操作涉及 flash 擦写,耗时可能几十毫秒,中断里干这个会阻塞系统,甚至触发看门狗复位。
第二,批量修改时合并写入。比如用户一次改了 SSID 和密码,你应该两个都改完再统一end(),而不是改一个开一次关一次。频繁开关命名空间会增加开销。
// 推荐:一次打开,批量写,一次关闭 prefs.begin("wifi_cfg", false); prefs.putString("ssid", new_ssid); prefs.putString("pass", new_pass); prefs.end();3.4 读取失败的处理策略
读 NVS 一定要处理"键不存在"的情况。首次上电时,NVS 是空的,getString会返回你给的默认值。这个默认值的设计很关键——它决定了设备在"没配置"状态下的行为。
我的做法是:默认值给空字符串,然后在业务逻辑里判断。如果 SSID 为空,就进入配网模式(开热点 + 开配置页面);如果 SSID 有值,就正常连接。这样逻辑清晰,不会出现"默认连到一个不存在的网络然后死等"的情况。
String ssid = prefs.getString("ssid", ""); if (ssid.length() == 0) { // 没配置过,进入配网模式 startConfigPortal(); } else { // 正常连接 WiFi.begin(ssid.c_str(), pass.c_str()); }4. 浏览器配置工具的完整实现
4.1 整体架构与数据流
先把整个方案的骨架理清楚,后面写代码才不会乱。数据流是这样的:
- ESP32 上电,从 NVS 读 WiFi 配置。
- 如果有配置,尝试连接;连接成功则正常运行,配置服务可以关掉或只在局域网开放。
- 如果没配置,或者用户按了某个按键触发配网,ESP32 开启 AP 模式(自己当热点)。
- 用户手机或电脑连上这个热点,浏览器访问
192.168.4.1。 - 设备返回配置页面(内嵌的 HTML)。
- 用户在页面填 SSID 和密码,点保存,页面发 POST 请求到
/api/config。 - ESP32 收到请求,写入 NVS,返回成功。
- 设备重启或重新连接,用新配置联网。
整个链路里,浏览器只负责收集输入和发请求,真正的存储和校验都在 ESP32 端。这样设计的好处是页面可以做得极简,不需要任何后端逻辑,纯静态 HTML + 一段 fetch 就够了。
4.2 依赖库与开发环境准备
Arduino IDE 环境下需要装这几个库(库管理器里搜名字即可):
- ESPAsyncWebServer:异步 Web 服务器核心。
- AsyncTCP:异步服务器的底层依赖,装 ESPAsyncWebServer 时会提示一起装。
- Preferences:ESP32 Arduino 核心自带,不用单独装。
PlatformIO 环境下,在platformio.ini里加:
lib_deps = me-no-dev/ESPAsyncWebServer@^1.2.3 me-no-dev/AsyncTCP@^1.1.1开发板选型上,ESP32-WROOM-32是最通用的,4MB flash 足够。如果做量产,ESP32-C3性价比更高,但注意 C3 的 AsyncTCP 兼容性偶尔有坑,建议用较新的库版本。烧录方式就是常规的 USB 转串口,注意有些板子需要按住 BOOT 键再上电才能进下载模式。
提示:国内下载库有时候会慢,可以在 Arduino IDE 的首选项里把开发板管理器地址换成国内镜像源,库管理器本身一般还好。如果 AsyncTCP 编译报错,八成是版本和 ESP32 核心版本不匹配,试试降级到 1.1.1 或者升级核心到 2.0.14 以上。
4.3 配置页面的 HTML 与交互逻辑
页面不用花哨,一个表单加一段 JS 就够。核心是表单提交时用 fetch 发 POST,而不是传统的 form action,这样页面不刷新,体验好,也方便显示结果。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>设备配置</title> <style> body { font-family: sans-serif; max-width: 400px; margin: 40px auto; padding: 0 16px; } label { display: block; margin-top: 16px; font-size: 14px; color: #333; } input { width: 100%; padding: 10px; margin-top: 6px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } button { width: 100%; padding: 12px; margin-top: 24px; background: #2d7ff9; color: #fff; border: none; border-radius: 6px; font-size: 16px; } #msg { margin-top: 16px; font-size: 14px; } </style> </head> <body> <h2>WiFi 配置</h2> <label>WiFi 名称 (SSID)</label> <input id="ssid" type="text" placeholder="输入 WiFi 名称"> <label>WiFi 密码</label> <input id="pass" type="password" placeholder="输入 WiFi 密码"> <button onclick="save()">保存并重启</button> <div id="msg"></div> <script> function save() { var ssid = document.getElementById('ssid').value.trim(); var pass = document.getElementById('pass').value; if (!ssid) { document.getElementById('msg').innerText = 'SSID 不能为空'; return; } document.getElementById('msg').innerText = '保存中...'; fetch('/api/config', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'ssid=' + encodeURIComponent(ssid) + '&pass=' + encodeURIComponent(pass) }) .then(function(r) { return r.text(); }) .then(function(t) { document.getElementById('msg').innerText = t; }) .catch(function(e) { document.getElementById('msg').innerText = '请求失败:' + e; }); } </script> </body> </html>这段代码有几个细节值得说。encodeURIComponent是必须的,因为 WiFi 密码里可能有&、=、+这些特殊字符,不编码的话服务端解析会出错。Content-Type用application/x-www-form-urlencoded,这是最简单的表单格式,ESP32 端解析起来也方便。返回结果直接显示在页面上,用户能立刻知道成没成。
4.4 ESP32 端:Web 服务与 NVS 写入
服务端代码分三块:启动 AP、注册路由、处理 POST。
#include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include <Preferences.h> AsyncWebServer server(80); Preferences prefs; const char* AP_SSID = "ESP32-Config"; const char* AP_PASS = "12345678"; // 至少 8 位 // 配置页面,内嵌 const char INDEX_HTML[] PROGMEM = R"rawliteral( <!DOCTYPE html> <html>... 上面那段 HTML ...</html> )rawliteral"; void setup() { Serial.begin(115200); // 开启 AP 模式 WiFi.mode(WIFI_AP); WiFi.softAP(AP_SSID, AP_PASS); Serial.print("AP IP: "); Serial.println(WiFi.softAPIP()); // 首页 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", INDEX_HTML); }); // 配置接口 server.on("/api/config", HTTP_POST, [](AsyncWebServerRequest *request){ String ssid, pass; if (request->hasParam("ssid", true)) { ssid = request->getParam("ssid", true)->value(); } if (request->hasParam("pass", true)) { pass = request->getParam("pass", true)->value(); } if (ssid.length() == 0) { request->send(400, "text/plain", "SSID 不能为空"); return; } prefs.begin("wifi_cfg", false); prefs.putString("ssid", ssid); prefs.putString("pass", pass); prefs.end(); request->send(200, "text/plain", "保存成功,设备将在 3 秒后重启"); // 延迟重启,让响应先发出去 // 注意:异步服务器里不能直接 delay,用定时器或标志位 }); server.begin(); } void loop() { // 异步服务器不需要在 loop 里处理请求 }这里有个关键坑点:ESPAsyncWebServer是异步的,你在回调里delay(3000)会阻塞整个事件循环,导致响应发不出去,浏览器一直转圈。正确做法是用一个标志位,在loop()里检测到标志后延时重启。
bool needReboot = false; unsigned long rebootAt = 0; // 在 POST 回调里: needReboot = true; rebootAt = millis() + 3000; request->send(200, "text/plain", "保存成功,设备将在 3 秒后重启"); // 在 loop() 里: void loop() { if (needReboot && millis() > rebootAt) { ESP.restart(); } }这个模式我用了很多次,非常稳。响应先发出去,用户看到提示,三秒后设备重启,体验很顺。
4.5 参数校验与写入前的安全检查
别小看校验,现场出问题十有八九是输入没洗干净。至少要检查这几项:
- SSID 非空,且长度不超过 32 字节(WiFi 规范限制)。
- 密码长度:WPA2 要求 8 到 63 个字符,太短连不上,太长会被截断。
- 特殊字符:虽然
encodeURIComponent处理了传输,但存进 NVS 前最好再确认没有控制字符。 - SSID 是否可见:这个可选,但可以做个扫描,让用户从列表里选,避免手输错。
if (ssid.length() > 32) { request->send(400, "text/plain", "SSID 过长"); return; } if (pass.length() > 0 && (pass.length() < 8 || pass.length() > 63)) { request->send(400, "text/plain", "密码长度需在 8-63 位之间"); return; }密码为空是允许的,代表开放网络,这个要放行。
5. 现场部署的常见问题与排查实录
5.1 保存后连不上,设备失联怎么办
这是最要命的问题:用户改了个密码,结果密码打错了,设备重启后连不上,而配置热点也关了,设备彻底变砖——只能拆下来重刷。
解决办法是加一个"连接失败自动回退"机制。思路是:写入新配置前,先把旧配置备份到另一个 NVS 键;重启后尝试连接新配置,如果 15 秒内没连上,自动读回旧配置再试一次。
// 写入前备份 prefs.begin("wifi_cfg", false); prefs.putString("ssid_bak", prefs.getString("ssid", "")); prefs.putString("pass_bak", prefs.getString("pass", "")); prefs.putString("ssid", new_ssid); prefs.putString("pass", new_pass); prefs.end(); // 启动时尝试连接 String ssid = prefs.getString("ssid", ""); String pass = prefs.getString("pass", ""); WiFi.begin(ssid.c_str(), pass.c_str()); unsigned long start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { delay(500); } if (WiFi.status() != WL_CONNECTED) { // 回退 String ssid_bak = prefs.getString("ssid_bak", ""); String pass_bak = prefs.getString("pass_bak", ""); if (ssid_bak.length() > 0) { WiFi.begin(ssid_bak.c_str(), pass_bak.c_str()); // 再等一次... } }这个机制救过我好几次。现场调试时手抖打错密码,设备自己回退到旧配置,省了拆机。
5.2 页面打不开或加载一半
浏览器访问192.168.4.1没反应,或者页面加载到一半卡住,通常是这几个原因:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全打不开 | AP 没起来 | 串口打印WiFi.softAPIP()确认 |
| 打不开但能连上热点 | 服务器没启动 | 检查server.begin()是否执行 |
| 页面加载一半 | 同步服务器阻塞 | 换异步库,或减少页面资源 |
| 样式丢失 | 静态资源路径错 | 内嵌的话检查 HTML 完整性 |
| 手机能开电脑不能 | 网段冲突 | 电脑可能连了别的网,手动设 IP |
手机端有个常见坑:连上 ESP32 热点后,手机可能因为"该网络无法访问互联网"而自动切回移动数据,导致请求发不到设备。解决办法是在页面上提示用户"请保持连接此 WiFi",或者干脆在 AP 配置里不设网关,让系统认为这是个纯局域网。
5.3 NVS 写入失败或读出来是乱码
写入返回成功但读出来不对,八成是这几个原因:
第一,键名超长。前面说过,15 字符上限,超了会失败。检查你的键名。
第二,命名空间没对上。写入用"wifi_cfg",读取用"wifi",那肯定读不到。命名空间是大小写敏感的。
第三,没调end()。Preferences在end()时才真正提交,如果你写完不end()就重启,数据可能丢。养成"开-写-关"的习惯。
第四,flash 分区表问题。如果你自定义了分区表,把 NVS 分区搞没了或者太小,写入会失败。默认分区表下 NVS 一般有 20KB 左右,够用。用idf.py partition-table或者 Arduino 的分区表选项确认一下。
5.4 多设备同网段的 IP 冲突
量产场景下,如果一批设备都用192.168.4.1这个默认 AP 地址,用户同时配置多台时会懵——连哪个都是同一个 IP。解决办法是给每台设备的 AP SSID 加唯一后缀,比如用 MAC 地址后两位:
uint8_t mac[6]; WiFi.macAddress(mac); char ap_ssid[32]; snprintf(ap_ssid, sizeof(ap_ssid), "ESP32-Config-%02X%02X", mac[4], mac[5]); WiFi.softAP(ap_ssid, AP_PASS);这样用户能看到ESP32-Config-A3F2这种名字,一眼区分。IP 还是192.168.4.1没关系,因为同一时间用户只连一台。
5.5 配置接口被滥用的风险
前面提过安全边界,这里展开说。如果你的设备长期开着这个配置接口,而且没认证,那风险是实打实的。我见过有人把设备放公司内网,结果同事随手访问设备IP/api/config就把 WiFi 改了。
最低成本的防护是加一个 token:
const char* CONFIG_TOKEN = "a1b2c3d4"; // 每台设备不同 // POST 回调里 if (!request->hasParam("token", true) || request->getParam("token", true)->value() != CONFIG_TOKEN) { request->send(403, "text/plain", "无权限"); return; }页面里把这个 token 藏在 JS 变量里,提交时带上。这不是强安全,但能挡住 99% 的误操作和随手尝试。真要安全,得上 HTTPS + 账号密码,但那对 ESP32 来说太重了,配置场景没必要。
另一个做法是配置服务只在启动后前 5 分钟开放,超时自动关闭。这样即使有人知道接口,也得掐着点来。
6. 几个提升体验的进阶玩法
6.1 加个 WiFi 扫描,让用户点选而不是手输
手输 SSID 容易错,尤其是那些带特殊字符或者中文的。ESP32 可以扫描周围 WiFi,把列表返回给页面,用户点一下自动填。
server.on("/api/scan", HTTP_GET, [](AsyncWebServerRequest *request){ int n = WiFi.scanNetworks(); String json = "["; for (int i = 0; i < n; i++) { if (i > 0) json += ","; json += "{\"ssid\":\"" + WiFi.SSID(i) + "\",\"rssi\":" + String(WiFi.RSSI(i)) + "}"; } json += "]"; request->send(200, "application/json", json); });页面加载时调一次/api/scan,把结果渲染成下拉列表。注意扫描是阻塞操作,会卡住几秒,最好放在 AP 启动之后、用户还没访问之前预扫一次,或者用异步扫描 API。
6.2 配置项扩展:不止 WiFi
这套框架稍加改造就能管更多配置。比如加个服务器地址、设备别名、上报间隔:
prefs.putString("server_url", url); prefs.putInt("report_interval", 60); prefs.putString("device_name", name);页面相应加几个输入框,POST 处理里多解析几个参数。关键是维护一个白名单,只允许改这几个键,别做成通用的set(key, value)接口,否则等于把整个 NVS 暴露出去。
6.3 用 WebSocket 做实时反馈
如果想让配置过程更丝滑,可以用 WebSocket 推送状态。比如用户点保存后,设备先返回"正在连接新 WiFi",连上了推"连接成功",连不上推"连接失败,已回退"。这样用户不用盯着页面猜。
ESPAsyncWebServer自带 WebSocket 支持,加个AsyncWebSocket ws("/ws"),在连接状态变化时ws.textAll("...")就行。代价是代码复杂度上升,配置这种低频操作其实用不上,看个人取舍。
6.4 一键恢复出厂设置
页面上加个"恢复出厂"按钮,清空 NVS 里的配置,重启进配网模式。实现很简单:
server.on("/api/reset", HTTP_POST, [](AsyncWebServerRequest *request){ prefs.begin("wifi_cfg", false); prefs.clear(); // 清空整个命名空间 prefs.end(); request->send(200, "text/plain", "已恢复出厂,重启中"); needReboot = true; rebootAt = millis() + 2000; });这个功能在现场特别实用,设备换地方、换网络时,一键清空重新配,比记着旧密码强。
7. 我踩过的坑和几条实在建议
先说一个最容易被忽略的点:AP 模式的密码不能少于 8 位。WiFi.softAP(ssid, pass)如果 pass 短于 8 位,会直接失败,AP 起不来,但串口不一定报错。我第一次用"1234"当 AP 密码,调试了半天以为代码有问题,后来才发现是这个限制。要么给足 8 位,要么传NULL开开放热点。
第二个坑是异步服务器回调里的字符串生命周期。request->getParam()返回的指针在回调结束后可能失效,如果你把它存到全局变量里延迟使用,会读到垃圾数据。正确做法是在回调里立刻拷贝成String,就像我前面代码里那样。
第三个是flash 写入次数。NVS 虽然有磨损均衡,但 flash 擦写寿命有限(一般 10 万次左右)。配置这种低频写入完全不用担心,但如果你在代码里循环写 NVS 做测试,一天写几千次,几个月就能把某个扇区写坏。测试时用内存变量模拟,别真往 flash 里怼。
第四个是串口日志别关。现场出问题时,串口打印是你唯一的线索。至少把 WiFi 连接状态、NVS 读写结果、HTTP 请求路径打出来。我习惯在关键节点加Serial.printf,出问题一眼就能定位。
最后一条建议:这套工具的价值不在于技术多高深,而在于它把"改配置"这件事从"开发者操作"变成了"用户操作"。你把它做进产品里,现场维护成本能降一大截。但记住,方便的另一面是风险,白名单、认证、回退这三样,一个都别省。我自己在实际项目里,凡是带现场配置功能的设备,这三样是标配,缺一个都不敢发货。