多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

ESP32 WiFi配置管理:浏览器直接读写NVS键值

ESP32 WiFi配置管理:浏览器直接读写NVS键值 1. 为什么改个 WiFi 密码要动固件搞过 ESP32 联网项目的朋友大概率都经历过这个场景设备已经装到现场了可能塞在配电箱里、贴在墙上、藏在设备内部突然要换个 WiFi 密码或者迁移到新网络。按照传统做法你得改代码里的 SSID 和密码重新编译然后抱着笔记本和 USB 线跑到设备跟前拆壳、接线、烧录。如果设备装在不好够到的地方这个流程能让人崩溃。问题的根源在于很多人在写 ESP32 联网代码时习惯把 WiFi 凭据直接写成宏定义或者常量字符串编译进固件里。固件一旦烧进去这些值就固定了想改只能重新编译烧录。但 ESP32 本身是带 NVSNon-Volatile Storage非易失性存储的它本来就是用来存这类配置数据的。NVS 是 ESP32 分区表里的一个独立分区掉电不丢数据专门用来存键值对形式的小配置。WiFi 凭据、设备参数、用户设置这些东西天生就该放在 NVS 里而不是硬编码在固件中。那为什么大家还是习惯硬编码因为改 NVS 需要额外的工具和步骤很多人觉得麻烦。常见的做法是通过串口命令行改或者写一段专门的配置代码跑一次。但这些方式都需要物理接触设备本质上和重新烧录没区别只是省了编译时间而已。真正理想的方案是设备本身跑一个 Web 服务你从浏览器访问它的 IP直接在网页上改 WiFi 配置改完保存到 NVS重启生效。整个过程不需要碰设备不需要任何专用工具手机浏览器就能搞定。这就是标题里说的浏览器工具直接改 NVS 键值的核心思路。这篇文章我会把整套方案拆开讲清楚NVS 到底怎么存 WiFi 凭据、Web 服务怎么在 ESP32 上跑起来、浏览器端怎么读写 NVS 键值、以及实际部署中那些文档里不会写的坑。适合已经会用 Arduino 框架或 ESP-IDF 开发 ESP32、做过基础联网项目、但还没系统搞过运行时配置管理的朋友。2. NVS 存 WiFi 凭据的底层逻辑2.1 NVS 分区到底是个什么东西ESP32 的 Flash 通常被划分成几个分区引导程序区、应用程序区、NVS 区、OTA 数据区、文件系统区等。NVS 区的大小在分区表里定义常见配置是 0x9000 起始、大小 0x500020KB够存几百个键值对。NVS 内部用的是日志结构存储写入时不是直接覆盖旧值而是追加新记录空间不够时才做垃圾回收。这个特性意味着 NVS 的写入寿命比直接擦写 Flash 要好得多但也不是无限写频繁写同一个键仍然会加速磨损。NVS 支持的数据类型包括整数int8/16/32/64、uint8/16/32/64、字符串、二进制大对象blob。WiFi 的 SSID 和密码用字符串类型存就行。命名空间namespace是 NVS 里的逻辑分组比如你可以建一个叫 wifi_cfg 的命名空间里面放 ssid 和 pass 两个键。命名空间的好处是避免键名冲突不同功能模块各用各的命名空间。用 Arduino 框架的话NVS 操作被封装在Preferences库里用起来很简单。ESP-IDF 里则是nvs_flash.h和nvs.h那套 API。两者底层是同一套东西只是封装层次不同。2.2 为什么 WiFi 凭据必须放 NVS 而不是硬编码硬编码的问题不只是改起来麻烦还有几个隐性成本。第一固件里明文存 WiFi 密码任何人拿到固件 dump 就能提取出来安全性很差。第二同一份固件没法批量部署到不同网络环境每换一个现场就得改代码重编。第三OTA 升级时如果固件里带着旧密码升级后可能连不上网直接变砖。放 NVS 之后固件本身不含任何环境相关的配置同一份固件可以烧到所有设备上现场通过 Web 界面配置各自的 WiFi。OTA 升级也不会影响 NVS 里的配置升级完照样连原来的网络。这是嵌入式设备配置管理的标准做法只是很多小项目图省事没这么做。还有一点容易被忽略NVS 里的数据在固件重新烧录时默认是保留的除非你执行了全片擦除。这意味着你调试时反复烧固件WiFi 配置不会丢省去每次重配的麻烦。但反过来如果你改了 NVS 的命名空间或键名结构旧数据可能读不出来需要做版本兼容或者手动清理。2.3 读取和写入的典型代码路径先看 Arduino 框架下的写法。初始化 NVS 只需要一行Preferences prefs;然后prefs.begin(wifi_cfg, false);第二个参数 false 表示可读写true 表示只读。读字符串用prefs.getString(ssid, )第二个参数是默认值键不存在时返回它。写用prefs.putString(ssid, value)写完记得prefs.end()关闭命名空间。ESP-IDF 下稍微繁琐一点先nvs_flash_init()然后nvs_open(wifi_cfg, NVS_READWRITE, handle)读用nvs_get_str(handle, ssid, buffer, length)写用nvs_set_str(handle, ssid, value)加nvs_commit(handle)。注意nvs_get_str第一次调用时如果 buffer 传 NULL它会通过 length 参数返回所需长度这样可以先探测长度再分配缓冲区避免缓冲区溢出。一个关键细节NVS 的键名最长 15 个字符命名空间名最长 15 个字符。超了会报错。很多人用完整的 wifi_ssid 这种名字没问题但如果你想起个 device_wifi_configuration_ssid 就会被截断。这个限制在文档里写了但很容易被忽略。3. 在 ESP32 上跑一个能改配置的 Web 服务3.1 Web 服务方案选型WebServer 还是 AsyncWebServerArduino 框架下有两个常用的 Web 服务库WebServer同步和ESPAsyncWebServer异步。同步库的 work 方式是每个请求处理完才处理下一个简单但并发能力差。异步库基于事件回调能同时处理多个连接响应更快但代码结构复杂一些而且需要额外安装库。对于配置页面这种场景其实同步库就够了因为同时访问的人通常只有一个。但有个实际问题如果设备正在连接 WiFi 或者执行耗时操作同步库会阻塞浏览器请求可能超时。异步库在这点上更稳。我的建议是如果只是做个配置页面用WebServer快速搞定如果设备还要同时提供其他 Web 服务比如数据展示、API 接口直接上ESPAsyncWebServer。还有一个选择是 ESP-IDF 自带的httpd组件功能完整但配置项多适合正式产品。快速原型用 Arduino 库更省事。3.2 配网模式的设计AP 模式还是 STA 模式这里有个关键设计决策设备还没连上 WiFi 时你怎么访问它的 Web 界面答案是设备先以 AP接入点模式启动自己发一个热点你用手机连上这个热点访问默认 IP通常是 192.168.4.1打开配置页面填 WiFi 信息保存后设备切换到 STA 模式去连目标网络。这个流程叫配网是 IoT 设备的标准做法。但有个细节要注意AP 模式和 STA 模式可以共存WIFI_MODE_APSTA这样设备在尝试连接目标网络的同时仍然保留自己的热点方便你随时改配置。代价是功耗略高射频要同时维护两个连接。对于插电设备无所谓电池设备就要权衡了。我的做法是设备启动时先读 NVS如果有 WiFi 配置就直接连连上了就关掉 AP如果没配置或者连不上就开 AP 等你来配。这样正常运行时只有 STA省电出问题时 AP 自动出现方便救援。3.3 配置页面的最小实现页面本身不需要多复杂一个表单两个输入框SSID 和密码加一个保存按钮就够了。SSID 可以用下拉列表扫描周围网络让用户选密码用 password 类型输入框。表单提交用 POST后端接收后写入 NVS返回一个保存成功设备将重启的提示页。这里有个容易踩的坑表单提交后如果立刻重启浏览器可能收不到响应显示连接错误用户以为没保存成功。正确做法是先返回响应延迟一两秒再重启。用delay(1500)加ESP.restart()就行或者用定时器异步重启。页面 HTML 可以直接以字符串形式嵌在代码里也可以用PROGMEM存到 Flash 里省 RAM。如果页面复杂建议存成单独的文件放到 SPIFFS/LittleFS 里通过文件系统读取返回。但简单页面直接内嵌最省事不用折腾文件系统。3.4 处理跨域和缓存的实际问题浏览器访问配置页面时如果页面里引用了外部 CDN 的 CSS 或 JS而设备本身没有外网连接这些资源加载会失败页面样式全乱。所以配置页面必须完全自包含所有样式和脚本内联不依赖任何外部资源。这一点在设备还没连上 WiFi 时尤其重要因为它根本没法访问外网。另一个问题是浏览器缓存。你改了页面代码重新烧录但浏览器可能还在用缓存的旧版本。解决办法是在响应头里加Cache-Control: no-cache或者给 URL 加个版本号参数。调试阶段建议直接禁用浏览器缓存省得怀疑人生。4. 浏览器端读写 NVS 键值的完整链路4.1 从表单提交到 NVS 写入的数据流整个链路是这样的浏览器表单 POST 到/saveESP32 的 Web 服务收到请求解析出 SSID 和密码参数调用 NVS 写入函数写入成功后返回响应浏览器显示成功提示设备延迟重启。解析 POST 参数时WebServer库用server.arg(ssid)就能拿到值。但要注意 URL 编码问题如果 SSID 里包含特殊字符比如空格、中文、浏览器会自动做 URL 编码arg()会自动解码一般不用手动处理。但如果密码里有号某些情况下会被解码成空格这是个经典坑。稳妥做法是在前端用encodeURIComponent编码后端对应解码。写入 NVS 前建议做基本校验SSID 不能为空长度不超过 32 字节WiFi 协议限制密码不超过 64 字节。校验不通过就返回错误提示不要盲目写入。写入失败比如 NVS 空间满也要捕获并提示不能假装成功。4.2 读取当前配置回显到页面配置页面打开时应该把当前 NVS 里存的 SSID 回显到输入框里这样用户能看到现在连的是哪个网络改的时候只改需要改的部分。密码出于安全考虑可以不回显留空表示不修改只有填了新值才更新。实现上GET/请求时读取 NVS 里的 SSID拼接到 HTML 字符串里返回。注意 HTML 转义如果 SSID 里有、、这些字符直接拼进去会破坏 HTML 结构需要用转义函数处理。虽然 WiFi SSID 里出现这些字符的概率不高但严谨起见还是处理一下。密码不回显的逻辑要在后端处理如果表单提交的密码字段为空就保留 NVS 里的旧密码不变如果非空才更新。这个逻辑要写清楚否则用户只想改 SSID 却把密码清空了设备就连不上了。4.3 保存后自动重启与连接验证保存成功后重启设备会读取新的 NVS 配置去连接 WiFi。但这里有个问题如果新配置是错的密码打错了设备连不上你又得重新进 AP 模式配。所以最好在保存后先不重启而是立即用新配置尝试连接连接成功再重启或者不重启直接切换连接失败就返回错误让用户重填。这个先验证再保存的流程体验好很多。实现上保存前先用WiFi.begin(ssid, pass)试连等几秒看结果成功了再写 NVS 并重启失败了返回连接失败请检查密码。代价是保存操作会阻塞几秒但比配错了再重来强。如果设备已经连着一个网络你要切换到另一个逻辑类似先试连新的成功了再更新 NVS 并断开旧的。这样保证任何时候设备至少有一个可用连接不会失联。4.4 一个完整的交互时序把上面的逻辑串起来一次完整的配置修改流程是这样的用户连上设备 AP浏览器打开 192.168.4.1设备返回配置页面回显当前 SSID用户填入新 SSID 和密码点保存浏览器 POST 到/save设备解析参数校验格式设备用新凭据尝试连接阻塞 5-10 秒连接成功写入 NVS返回成功页延迟重启连接失败返回错误页提示重试不写 NVS设备重启后读取新 NVS连接新网络用户在浏览器看到成功提示手动切换到新网络验证这个流程里第 6 步的阻塞是必要的但要注意 Web 服务的超时设置。如果连接尝试太久浏览器可能先超时了。建议把连接超时设在 10 秒以内并且在前端加个 loading 提示让用户知道设备正在处理。5. 实际部署中那些文档不会写的坑5.1 NVS 写入失败的各种原因NVS 写入失败最常见的原因是分区空间不足。NVS 分区默认 20KB听起来不少但如果你频繁写入不做清理日志结构会占满空间。表现是nvs_set_str返回ESP_ERR_NVS_NOT_ENOUGH_SPACE。解决办法是定期调用nvs_erase_key删除不用的键或者增大 NVS 分区。但增大分区要改分区表已经量产的设备改不了所以设计阶段就要留够空间。另一个原因是命名空间没打开或者打开模式不对。用只读模式打开却尝试写入会返回ESP_ERR_NVS_READ_ONLY。这个错误在 Arduino 的Preferences库里表现为putString返回 false不抛异常很容易被忽略。所以每次写入后都要检查返回值别假设一定成功。还有一个隐蔽的坑NVS 操作不是线程安全的。如果你在多个任务里同时读写 NVS可能损坏数据。ESP-IDF 下要用互斥锁保护Arduino 下如果只在 Web 回调里操作一般没事但如果你还有别的任务也在读写就要小心了。5.2 WiFi 切换时的连接状态管理设备从 AP 模式切到 STA 模式或者从一个 STA 网络切到另一个状态管理很容易出问题。常见现象是调用了WiFi.begin()但一直连不上WiFi.status()始终是WL_DISCONNECTED。原因可能是上一次的连接还没完全断开或者 WiFi 驱动状态机卡住了。稳妥的做法是切换前先WiFi.disconnect(true)彻底断开并清除状态等一小会儿再WiFi.begin()。如果还是连不上可以WiFi.mode(WIFI_OFF)再重新WiFi.mode(WIFI_STA)相当于软重启 WiFi 驱动。这个技巧在调试时很有用能解决大部分莫名其妙的连接问题。另外WiFi 连接是异步的WiFi.begin()立即返回实际连接在后台进行。你要用循环轮询WiFi.status()或者注册事件回调来获知结果。轮询时记得加超时别死等。事件回调方式更优雅但 Arduino 框架下事件处理不如 ESP-IDF 直观看个人习惯。5.3 浏览器兼容性与移动端适配配置页面主要在手机上用所以必须做移动端适配。viewport meta 标签不能少输入框要够大方便点按按钮要明显。CSS 用简单的 flex 布局就行别引入 Bootstrap 这种大框架加载慢还占空间。不同手机浏览器的行为也有差异。有些安卓浏览器对自签名 HTTPS 证书处理严格但我们的配置页面用 HTTP 就行不涉及证书问题。iOS 的 Safari 对表单自动填充比较激进可能会把保存的密码填进去这个可以通过autocompleteoff缓解但不保证完全生效。还有个实际问题是手机连上设备 AP 后如果 AP 没有外网手机会提示此网络无法访问互联网有些手机会自动切换到移动数据导致你访问不了 192.168.4.1。解决办法是在 AP 配置里设置正确的网关和 DNS或者提示用户手动关闭移动数据。这个坑在安卓上特别常见调试时如果发现页面打不开先检查手机是不是偷偷切回 4G 了。5.4 安全性上的最低限度考虑配置页面虽然在内网但也不能完全不设防。最低限度要做的是配置页面加个简单密码或者只在 AP 模式下开放配置功能STA 模式下关闭。否则同一个网络里的任何人都能改你的设备配置。AP 模式本身也要设密码别用开放热点。虽然配网时麻烦一点但总比被人随便连上改配置强。AP 的密码可以设个默认值印在设备标签上或者用设备 MAC 后几位生成。如果设备要长期运行建议配置页面用 POST 而不是 GET 提交敏感信息避免密码出现在 URL 里被日志记录。虽然内网环境风险不大但养成好习惯没坏处。6. 把这套方案产品化的几个扩展方向6.1 配置项的版本管理与迁移设备固件升级后NVS 里的配置结构可能变了比如键名改了、新增了配置项。如果不做处理旧配置读不出来设备会以为没配置过重新进入配网模式。解决办法是在 NVS 里存一个配置版本号启动时检查版本不匹配就执行迁移逻辑读旧键、写新键、删旧键、更新版本号。这个机制在正式产品里是必须的原型阶段可以先不做但心里要有数。迁移逻辑要写得健壮考虑各种中间状态比如迁移到一半断电了怎么办。简单做法是迁移完成后才写新版本号这样下次启动发现版本还是旧的会重新迁移幂等操作不怕重复。6.2 多网络配置与自动切换有些设备需要在多个已知网络间自动切换比如在办公室连办公网拿回家连家庭网。这要求 NVS 里存多组 WiFi 凭据启动时按优先级依次尝试。实现上可以用多个命名空间或者在一个命名空间里用带索引的键名ssid_0、ssid_1...。扫描到已知网络就自动连接这个逻辑要处理好优先级和超时。别在一个连不上的网络上耗太久快速失败切下一个。同时要限制尝试次数避免无限重试耗电。6.3 配置备份与恢复NVS 里的配置如果能导出备份设备出问题时恢复起来就方便了。可以在 Web 界面加个导出配置按钮把 NVS 里的键值对以 JSON 格式下载下来。恢复时上传 JSON 文件解析后写回 NVS。这个功能在批量部署时特别有用配好一台导出配置其他设备导入就行不用一个个手配。导出时注意别把敏感信息比如密码明文导出或者至少提示用户文件包含敏感信息要妥善保管。恢复时要校验 JSON 格式和键名合法性别让非法数据写进 NVS 把设备搞挂。6.4 OTA 升级与配置保留的配合OTA 升级时NVS 分区默认不动所以配置会保留。但如果你在升级逻辑里做了全片擦除配置就没了。要确保 OTA 流程只擦写应用程序分区不碰 NVS。另外新固件如果改了 NVS 结构升级后第一次启动要执行配置迁移这个逻辑要放在 OTA 完成后的启动流程里。还有个小细节OTA 升级过程中如果断电可能导致固件损坏无法启动。ESP32 的 OTA 机制有回滚保护但配置迁移如果做了一半断电可能留下不一致状态。所以迁移逻辑要设计成可重入的每次启动都检查并修复。7. 我在这套方案上踩过的几个真实坑第一个坑是 NVS 键名长度限制。我一开始用了 wifi_configuration_ssid 这种长名字编译没问题运行时写入失败查了半天才发现是键名超了 15 字符。后来改成 wifi_ssid 就好了。这个错误不报编译错只在运行时返回错误码很容易漏掉。第二个坑是 AP 模式下手机自动切回移动数据。调试时页面死活打不开以为是 Web 服务没起来后来发现是手机觉得这个 WiFi 没网就自动切走了。解决办法是在代码里给 AP 配置一个假的网关地址让手机认为这个网络有网关就不会自动切。或者干脆提示用户手动关闭移动数据。第三个坑是表单提交后立即重启导致浏览器收不到响应。用户看到连接错误以为没保存成功反复提交。后来改成先返回响应延迟 1.5 秒再重启问题解决。这个延迟时间不能太短要给浏览器足够时间接收响应也不能太长用户等得不耐烦。第四个坑是 NVS 写入没检查返回值。有次 NVS 空间满了写入静默失败但代码继续执行重启结果设备用旧配置启动用户以为改了没生效。后来每次写入都检查返回值失败就返回错误页问题清晰多了。这套方案的核心价值在于把配置管理从开发时转移到运行时让设备在现场也能灵活调整。实现难度不高但细节不少尤其是错误处理和状态管理。把这几个坑避开基本就能稳定用了。
返回列表