☰
ESP32 WiFi配置在线修改:浏览器直接读写NVS,免编译免烧录
2026/10/8 18:13:36 网站建设 项目流程

1. 从一个让人抓狂的场景说起

如果你玩过 ESP32,大概率经历过这个场景:设备已经焊好、装进外壳、挂在墙上跑了三个月,突然要换个 WiFi 密码。你翻出数据线,拆壳,插 USB,打开 Arduino IDE 或者 ESP-IDF,改一行ssid和password,编译,烧录,等半天。就为了改两个字符串。

更别提有些设备根本没法方便地接线——装在配电箱里的、封在防水盒里的、焊在板子上的。每次改 WiFi 配置都像做一次小手术。

这个项目的核心思路很直接:ESP32 的 WiFi 配置本来就存在 NVS(Non-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 里,那思路就清晰了:

  1. ESP32 启动后,先连上 WiFi(用 NVS 里的配置)
  2. 同时启动一个 HTTP 服务器
  3. 提供一个网页,读取并展示 NVS 里nvs.net80211命名空间下的 WiFi 配置
  4. 用户在网页上修改 SSID 和密码,提交
  5. ESP32 收到新值,写入 NVS
  6. 重启或者重新初始化 WiFi,用新配置连接

整个过程不需要编译工具链,不需要 USB 线,只要能访问到 ESP32 的 IP 就行。

注意:这个方案的前提是 ESP32 已经连上了某个网络,你能通过局域网访问它。如果设备完全失联(比如路由器换了、密码改了导致连不上),那就需要额外的兜底机制,比如同时开一个配置热点。这部分后面会讲。

3. NVS 键值存储的核心机制

3.1 NVS 的物理结构和逻辑结构

NVS 在 Flash 上是一个独立分区,通常叫nvs,大小从 16KB 到 64KB 不等,在分区表里定义。它的逻辑结构是「命名空间 → 键 → 值」三层:

  • 命名空间(Namespace):类似文件夹,比如nvs.net80211、storage、myapp
  • 键(Key):字符串,最长 15 个字符
  • 值(Value):可以是整数、字符串、二进制 blob

WiFi 相关的配置存在nvs.net80211这个命名空间下。常见的键包括:

键名类型含义
sta.ssidblob已保存的 AP SSID(含长度头)
sta.pswdblob已保存的 AP 密码(含长度头)
sta.authmodeu8认证模式
sta.configblob完整的 station 配置
sta.bssidblob目标 AP 的 BSSID
sta.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 整体架构设计

整个系统分三块:

  1. NVS 读写层:封装nvs_open、nvs_get_blob、nvs_set_blob,提供read_wifi_config()和save_wifi_config()两个接口
  2. HTTP 服务层:用WebServer库(Arduino)或esp_http_server(IDF)提供三个路由——GET /返回配置页面,GET /api/wifi返回当前配置的 JSON,POST /api/wifi接收新配置并写入 NVS
  3. 前端页面:一个简单的 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 charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ESP32 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> <h2>WiFi 配置</h2> <label>SSID</label> <input id="ssid" type="text"> <label>密码</label> <input id="password" type="text"> <button onclick="save()">保存并重启</button> <div id="msg"></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 写入后不 commit

nvs_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 会出问题。比如密码是abc"def,拼出来的 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 设备,这个差别是巨大的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询