ESP32 WiFi配置在线修改:浏览器直接读写NVS,免编译免烧录
1. 从一个让人抓狂的场景说起如果你玩过 ESP32大概率经历过这个场景设备已经焊好、装进外壳、挂在墙上跑了三个月突然要换个 WiFi 密码。你翻出数据线拆壳插 USB打开 Arduino IDE 或者 ESP-IDF改一行ssid和password编译烧录等半天。就为了改两个字符串。更别提有些设备根本没法方便地接线——装在配电箱里的、封在防水盒里的、焊在板子上的。每次改 WiFi 配置都像做一次小手术。这个项目的核心思路很直接ESP32 的 WiFi 配置本来就存在 NVSNon-Volatile Storage里那能不能不重新编译固件直接改 NVS 里的键值答案是能而且可以用浏览器直接改。具体做法是在 ESP32 上跑一个轻量 HTTP 服务提供一个网页界面列出当前 NVS 里的 WiFi 相关键值允许你在线修改并保存。改完之后 ESP32 重新读取 NVS用新配置连网。整个过程不需要数据线不需要编译环境手机浏览器就能操作。这篇文章会把这个方案的来龙去脉讲清楚NVS 到底怎么存 WiFi 配置、为什么直接改 NVS 比重新烧录更合理、HTTP 服务怎么设计、网页端怎么做、有哪些坑。适合有 ESP32 基础、做过 WiFi 连接项目、想提升设备可维护性的开发者。2. 为什么改个 WiFi 密码这么麻烦2.1 传统方式的根本问题大部分人写 ESP32 WiFi 代码是这样的const char* ssid MyWiFi; const char* password 12345678; void setup() { WiFi.begin(ssid, password); // ... }这两个字符串被编译进固件存在 Flash 的代码段里。改密码就意味着改代码、重新编译、重新烧录。这个流程在开发阶段没问题但设备部署之后就变成了负担。有人会说那我用WiFiManager库不就行了WiFiManager确实解决了首次配网的问题——它启动一个热点你用手机连上去在网页里输入 WiFi 账号密码。但它主要面向首次配置场景配置完之后如果还想改要么触发它重新进入配网模式通常需要按某个按键或者断电重启特定次数要么还是得重新烧录。而且WiFiManager把配置存在自己的 SPIFFS 文件里格式是它自己定的你想单独改某一项并不方便。2.2 NVS 才是 WiFi 配置的真正归宿这里要讲一个很多人忽略的事实ESP32 的 WiFi 驱动本身就会把连接信息存到 NVS 里。当你调用WiFi.begin(ssid, password)并成功连接后ESP-IDF 底层会把这次连接的 AP 信息SSID、密码、信道、BSSID 等写入 NVS 的nvs.net80211命名空间。下次上电时即使你不调用WiFi.begin()调用WiFi.begin()不带参数它也会尝试用 NVS 里存的配置去连。这意味着WiFi 配置本来就在 NVS 里只是大多数人不知道也没去用它。NVS 是 ESP32 Flash 里一块专门用来存键值对的分区掉电不丢。它的设计目标就是存配置参数——WiFi 密码、设备名称、校准数据、用户偏好这些都属于 NVS 的管辖范围。2.3 直接改 NVS 的思路既然配置在 NVS 里那思路就清晰了ESP32 启动后先连上 WiFi用 NVS 里的配置同时启动一个 HTTP 服务器提供一个网页读取并展示 NVS 里nvs.net80211命名空间下的 WiFi 配置用户在网页上修改 SSID 和密码提交ESP32 收到新值写入 NVS重启或者重新初始化 WiFi用新配置连接整个过程不需要编译工具链不需要 USB 线只要能访问到 ESP32 的 IP 就行。注意这个方案的前提是 ESP32 已经连上了某个网络你能通过局域网访问它。如果设备完全失联比如路由器换了、密码改了导致连不上那就需要额外的兜底机制比如同时开一个配置热点。这部分后面会讲。3. NVS 键值存储的核心机制3.1 NVS 的物理结构和逻辑结构NVS 在 Flash 上是一个独立分区通常叫nvs大小从 16KB 到 64KB 不等在分区表里定义。它的逻辑结构是「命名空间 → 键 → 值」三层命名空间Namespace类似文件夹比如nvs.net80211、storage、myapp键Key字符串最长 15 个字符值Value可以是整数、字符串、二进制 blobWiFi 相关的配置存在nvs.net80211这个命名空间下。常见的键包括键名类型含义sta.ssidblob已保存的 AP SSID含长度头sta.pswdblob已保存的 AP 密码含长度头sta.authmodeu8认证模式sta.configblob完整的 station 配置sta.bssidblob目标 AP 的 BSSIDsta.chanu8信道这里有个关键细节sta.ssid和sta.pswd存的是blob不是普通字符串。blob 的前几个字节是长度信息后面才是实际内容。如果你直接用nvs_set_str去写格式会对不上WiFi 驱动读的时候会出错。3.2 为什么不能简单用 nvs_set_str这是最容易踩的坑。很多人想当然地写nvs_handle_t handle; nvs_open(nvs.net80211, NVS_READWRITE, handle); nvs_set_str(handle, sta.ssid, NewWiFi); nvs_commit(handle);结果发现 WiFi 连不上了。原因是 WiFi 驱动期望的sta.ssid是一个特定格式的 blob而不是裸字符串。正确的做法是构造符合格式的 blob。以 SSID 为例格式通常是[1 byte: 长度] [N bytes: SSID 内容]密码类似[1 byte: 长度] [N bytes: 密码内容]所以写入代码应该是void save_wifi_to_nvs(const char* ssid, const char* password) { nvs_handle_t handle; esp_err_t err nvs_open(nvs.net80211, NVS_READWRITE, handle); if (err ! ESP_OK) return; // 构造 SSID blob uint8_t ssid_blob[33]; size_t ssid_len strlen(ssid); ssid_blob[0] ssid_len; memcpy(ssid_blob[1], ssid, ssid_len); nvs_set_blob(handle, sta.ssid, ssid_blob, ssid_len 1); // 构造密码 blob uint8_t pswd_blob[65]; size_t pswd_len strlen(password); pswd_blob[0] pswd_len; memcpy(pswd_blob[1], password, pswd_len); nvs_set_blob(handle, sta.pswd, pswd_blob, pswd_len 1); nvs_commit(handle); nvs_close(handle); }提示不同 IDF 版本对 blob 格式的处理可能略有差异。最稳妥的方式是先用nvs_get_blob读出现有的 blob观察它的实际字节结构再照着格式构造新的。不要凭猜测写。3.3 读取现有配置读取比写入简单因为不需要构造格式直接把 blob 读出来解析就行String read_wifi_ssid() { nvs_handle_t handle; if (nvs_open(nvs.net80211, NVS_READONLY, handle) ! ESP_OK) return ; size_t len 0; if (nvs_get_blob(handle, sta.ssid, NULL, len) ! ESP_OK) { nvs_close(handle); return ; } uint8_t* buf (uint8_t*)malloc(len); nvs_get_blob(handle, sta.ssid, buf, len); nvs_close(handle); // 第一个字节是长度 uint8_t ssid_len buf[0]; String ssid ; for (int i 0; i ssid_len i len - 1; i) { ssid (char)buf[i 1]; } free(buf); return ssid; }密码的读取逻辑一样只是键名换成sta.pswd。4. 浏览器端改 NVS 的完整实现4.1 整体架构设计整个系统分三块NVS 读写层封装nvs_open、nvs_get_blob、nvs_set_blob提供read_wifi_config()和save_wifi_config()两个接口HTTP 服务层用WebServer库Arduino或esp_http_serverIDF提供三个路由——GET /返回配置页面GET /api/wifi返回当前配置的 JSONPOST /api/wifi接收新配置并写入 NVS前端页面一个简单的 HTML 页面用fetch调 API展示和修改配置为什么选 HTTP 而不是蓝牙或者串口因为 HTTP 的兼容性最好——手机、电脑、平板只要有浏览器就能用不需要装任何 App。而且 ESP32 跑 HTTP 服务的开销很小几十 KB 内存就够了。4.2 HTTP 服务的搭建用 Arduino 框架的话WebServer库是最省事的#include WiFi.h #include WebServer.h WebServer server(80); void setup() { Serial.begin(115200); // 先用 NVS 里的配置连 WiFi WiFi.mode(WIFI_STA); WiFi.begin(); // 不带参数自动用 NVS 里的配置 // 等待连接最多 15 秒 unsigned long start millis(); while (WiFi.status() ! WL_CONNECTED millis() - start 15000) { delay(500); Serial.print(.); } if (WiFi.status() WL_CONNECTED) { Serial.println(\nConnected: WiFi.localIP().toString()); } else { Serial.println(\nWiFi failed, starting config AP); // 兜底开热点 WiFi.mode(WIFI_AP); WiFi.softAP(ESP32-Config, 12345678); Serial.println(AP IP: WiFi.softAPIP().toString()); } // 注册路由 server.on(/, HTTP_GET, handleRoot); server.on(/api/wifi, HTTP_GET, handleGetWifi); server.on(/api/wifi, HTTP_POST, handlePostWifi); server.begin(); } void loop() { server.handleClient(); }这里有个关键点WiFi.begin()不带参数时ESP32 会自动从 NVS 读取上次保存的配置。如果 NVS 里没有配置或者连接失败就进入兜底模式——开一个热点让用户连上来配置。4.3 配置页面的前端实现前端不需要什么框架一个 HTML 文件搞定!DOCTYPE html html head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleESP32 WiFi Config/title style body { font-family: sans-serif; max-width: 400px; margin: 40px auto; padding: 0 20px; } label { display: block; margin-top: 16px; font-weight: bold; } input { width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box; } button { margin-top: 20px; padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; } #msg { margin-top: 12px; color: green; } /style /head body h2WiFi 配置/h2 labelSSID/label input idssid typetext label密码/label input idpassword typetext button onclicksave()保存并重启/button div idmsg/div script fetch(/api/wifi).then(r r.json()).then(d { document.getElementById(ssid).value d.ssid || ; document.getElementById(password).value d.password || ; }); function save() { const ssid document.getElementById(ssid).value; const password document.getElementById(password).value; fetch(/api/wifi, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ssid, password}) }).then(r r.json()).then(d { document.getElementById(msg).textContent d.message; if (d.success) setTimeout(() location.reload(), 5000); }); } /script /body /html页面逻辑很简单加载时拉取当前配置填充表单保存时 POST 新配置成功后 5 秒刷新页面等 ESP32 重启完成。4.4 后端 API 的实现GET /api/wifi返回当前配置void handleGetWifi() { String ssid read_wifi_ssid(); String password read_wifi_password(); String json {\ssid\:\ ssid \,\password\:\ password \}; server.send(200, application/json, json); }POST /api/wifi接收新配置并写入void handlePostWifi() { if (!server.hasArg(plain)) { server.send(400, application/json, {\success\:false,\message\:\no body\}); return; } String body server.arg(plain); // 简单解析 JSON生产环境建议用 ArduinoJson int ssidStart body.indexOf(\ssid\:\) 8; int ssidEnd body.indexOf(\, ssidStart); String ssid body.substring(ssidStart, ssidEnd); int pwdStart body.indexOf(\password\:\) 12; int pwdEnd body.indexOf(\, pwdStart); String password body.substring(pwdStart, pwdEnd); save_wifi_to_nvs(ssid.c_str(), password.c_str()); server.send(200, application/json, {\success\:true,\message\:\saved, rebooting...\}); delay(1000); ESP.restart(); }注意这里为了演示用了字符串查找来解析 JSON实际项目建议用ArduinoJson库避免特殊字符比如密码里带引号导致解析错误。5. 实操中的坑与排查技巧5.1 blob 格式不匹配导致 WiFi 连不上这是最常见的坑。表现是写入 NVS 成功但重启后 WiFi 连不上串口日志显示wifi:sta is connecting, ssid: xxx但一直失败。排查方法先用nvs_get_blob读出现有的 blob用十六进制打印出来看结构。正常的sta.ssidblob 第一个字节是长度后面是内容。如果你写入的 blob 没有长度头驱动读出来的 SSID 就是乱的。void dump_blob(const char* key) { nvs_handle_t handle; nvs_open(nvs.net80211, NVS_READONLY, handle); size_t len 0; nvs_get_blob(handle, key, NULL, len); uint8_t* buf (uint8_t*)malloc(len); nvs_get_blob(handle, key, buf, len); Serial.printf(%s (%d bytes): , key, len); for (int i 0; i len; i) Serial.printf(%02X , buf[i]); Serial.println(); free(buf); nvs_close(handle); }5.2 NVS 分区太小默认的 NVS 分区是 24KB 左右存 WiFi 配置绰绰有余。但如果你还往 NVS 里存别的东西比如日志、用户数据可能会写满。写满之后nvs_set_blob会返回ESP_ERR_NVS_NOT_ENOUGH_SPACE。解决办法在分区表里把nvs分区调大比如改成 64KB 或 128KB。改完分区表需要重新烧录一次分区表但之后就不用再动了。5.3 写入后不 commitnvs_set_blob只是把数据写到内存缓存必须调用nvs_commit才会真正写入 Flash。忘了 commit 的话重启后数据就丢了。nvs_set_blob(handle, sta.ssid, ssid_blob, ssid_len 1); nvs_set_blob(handle, sta.pswd, pswd_blob, pswd_len 1); nvs_commit(handle); // 这一行不能少 nvs_close(handle);5.4 密码里有特殊字符如果密码里包含、\、{、}这些字符用字符串拼接构造 JSON 会出问题。比如密码是abcdef拼出来的 JSON 就非法了。解决方案有两个一是用ArduinoJson库来序列化和反序列化它会自动处理转义二是前端用encodeURIComponent编码后端解码。5.5 设备完全失联怎么办如果 WiFi 密码改了但写错了或者路由器换了ESP32 连不上任何网络你就没法通过 HTTP 访问它了。这时候需要兜底机制方案 A启动时如果 15 秒内连不上 WiFi自动开一个配置热点AP 模式用户连上热点后访问192.168.4.1配置方案 B留一个物理按键长按 5 秒清除 NVS 里的 WiFi 配置并重启进入 AP 模式方案 C在 NVS 里存一个「上次成功连接时间」如果超过一定时间没连上自动进入 AP 模式方案 A 最简单推荐优先实现。方案 B 最可靠但需要额外的硬件。方案 C 适合无人值守的设备。5.6 常见问题速查表现象可能原因解决方法写入成功但连不上blob 格式不对用 dump_blob 检查格式确保有长度头重启后配置丢失忘了 nvs_commit在 nvs_close 前调用 nvs_commit写入返回空间不足NVS 分区满了扩大分区表里的 nvs 分区密码含特殊字符解析失败JSON 拼接未转义用 ArduinoJson 或前端编码设备完全失联配置错误或路由器变更实现 AP 兜底模式或物理按键清除HTTP 请求无响应服务器未 handleClient确保 loop 里调用 server.handleClient()页面加载但 API 404路由未注册检查 server.on 的路径和方法6. 这个方案还能怎么扩展6.1 从 WiFi 配置扩展到通用 NVS 管理既然能改 WiFi 配置那就能改任何 NVS 里的键值。你可以把页面做成一个通用的 NVS 编辑器列出所有命名空间、所有键、对应的值和类型允许在线增删改查。这对调试特别有用。比如你的设备把传感器校准参数存在 NVS 里现场发现数值不对不用重新烧录直接网页上改。实现上nvs_entry_find和nvs_entry_next可以遍历命名空间下的所有键nvs_get_type可以获取类型然后根据类型调用对应的 get/set 函数。6.2 加个简单的认证现在的方案没有任何认证局域网内任何人都能改配置。如果设备部署在不太可控的网络里建议加一层简单的认证HTTP Basic Auth最简单前端弹窗输入用户名密码后端校验Token首次配置时生成一个随机 token 存在 NVS 里后续请求需要带 token一次性密码设备启动时在串口打印一个随机密码网页上需要输入Basic Auth 实现最简单几行代码if (!server.authenticate(admin, esp32pass)) { return server.requestAuthentication(); }6.3 配置版本管理和回滚每次改配置前把旧配置备份到一个单独的 NVS 键里比如sta.ssid.backup。如果新配置连不上可以自动回滚到旧配置。逻辑是写入新配置后重启如果 30 秒内没连上读取备份配置写回去再重启。这样即使改错了也不会失联。6.4 OTA 升级的配合这个方案和 OTA 升级是绝配。OTA 升级负责更新固件NVS 管理负责更新配置两者结合就能实现完全远程维护——不用碰设备固件和配置都能在线更新。OTA 用ArduinoOTA库或者esp_https_ota组件NVS 管理用本文的方案两者互不干扰因为 OTA 更新的是app分区NVS 在独立的nvs分区。7. 一些实操心得我在几个项目里用了这个方案分享几个文档里不会写的经验。第一blob 格式一定要先读再写。不要凭文档或者猜测去构造 blob。不同 IDF 版本、不同芯片型号blob 的格式可能有细微差异。最稳的做法是设备第一次连上 WiFi 后立刻把sta.ssid和sta.pswd的原始 blob dump 出来存成参考。以后写入就照着这个格式来。第二重启比重新初始化 WiFi 更可靠。写完 NVS 后你可以选择WiFi.disconnect()然后WiFi.begin()重新连也可以直接ESP.restart()。实测下来重启的成功率更高因为 WiFi 驱动内部有一些状态需要完全重置。重启的代价是多等几秒但省去了很多奇怪的连接问题。第三前端页面要做得足够简单。这个页面的使用者可能是现场施工人员不是开发者。所以不要搞复杂的表单验证、不要用花哨的 UI 框架。两个输入框、一个按钮、一个状态提示足够了。页面加载速度要快因为 ESP32 的 HTTP 服务性能有限大页面会卡。第四NVS 写入有寿命限制。Flash 的擦写次数是有限的通常 10 万次左右。虽然改 WiFi 配置的频率很低不会触及这个限制但如果你把 NVS 当数据库用频繁写入就要注意了。NVS 库内部有磨损均衡机制但也不是无限的。第五串口日志是你的朋友。调试 NVS 问题时串口日志比什么都管用。esp_err_t的返回值一定要打印出来ESP_ERR_NVS_NOT_FOUND、ESP_ERR_NVS_INVALID_LENGTH、ESP_ERR_NVS_NOT_ENOUGH_SPACE这些错误码能直接告诉你问题在哪。最后再分享一个小技巧如果你不确定 NVS 里的键名可以用nvs_entry_find遍历打印出来。不同 IDF 版本的键名可能略有不同比如有些版本用sta.ssid有些用ssid。遍历一遍就清楚了。nvs_iterator_t it NULL; esp_err_t res nvs_entry_find(nvs, nvs.net80211, NVS_TYPE_ANY, it); while (res ESP_OK) { nvs_entry_info_t info; nvs_entry_info(it, info); Serial.printf(key: %s, type: %d\n, info.key, info.type); res nvs_entry_next(it); } nvs_release_iterator(it);这个方案的核心价值不在于技术有多复杂而在于它改变了设备维护的方式——从「必须物理接触」变成「网络可达即可维护」。对于部署在难以触及位置的 ESP32 设备这个差别是巨大的。