ESP32-WROOM-32 设备管理页从零到上线

一、背景

玩园子这么多年了一直是在看园子的文章,但我自己没怎么发过。这两天突然发现了我几年前买的esp32 开发板。几年之前准备用nano Framework 做的,试了几次没成功后来就吃灰了。但是呢,现有有AI了。于是我想到了一个点子,用AI做一下我之前没搞过的嵌入式开发。我准备做一个跑在 ESP32 上的「设备管理系统」网页(之前也不知道做什么,后来想到路由器,后来才想到的点子)。用浏览器连到板子,就能完成类似路由器的管理。它包括:设备拓扑实时状态、WiFi AP/STA 双模、BLE 扫描、MQTT 收发、串口交互、引脚监控、系统设置,甚至用 GPIO2 上的 LED 做光学数据传输------让摄像头 30fps 采样解调中文文本。

前端准备用HTML单页面+JS来搞。套个皮肤大小在90K左右。这个芯片的Flash4MB可以了。后端准备用Rust。

真正把它跑稳(实际上还是开蓝牙,开MQTT,开STA,开LED光学发报文还是有可能会挂),踩了不少坑,尤其是内存连接数这两个 ESP32 的命门。这篇把从环境搭建到最终版本的过程与坑整理出来,帮你少走弯路。

二、环境搭建

2.1 组件清单

  • ESP-IDF 5.2.8(内核与库)
  • Rustxtensa-esp32-espidf 目标,esp-idf-svc 框架,std 模式
  • 编译器:`xtensa-esp32-elf-clang`(esp-clang)处理 C 侧
  • 工具脚本tools/build.sh(编译)/ tools/flash.sh(烧录)------禁用裸 cargo build / espflash flash

环境坑:crates.io / static.crates.io 在国内被墙,直接拉新依赖会卡死,必须先切成 sparse+https://rsproxy.cn/index/。这也是我坚持不新增任何 Rust 依赖的根本原因。

2.2 编译

复制代码
cd H:/Aiprojects/esp32/rust
bash tools/build.sh release
# 成功标志:末尾 ===PIPE_RC=0=== 且无 error:
# 产物 ELF:H:/esp32bld/xtensa-esp32-espidf/release/esp32-manage

全量 C 重编有杀软竞争 .tmp 文件导致 cc1: Permission denied 的坑,build.sh 内置了 5 次自动重试;增量编译从不失败。

2.3 烧录

复制代码
bash tools/flash.sh release COM6 90   # 90 = 手动按键窗口秒数
  • 串口 CH340 / COM6 / 115200;烧录前关掉占用 COM6 的串口助手/WebSerial/Arduino。
  • 0x0 整片擦除(MicroPython 才写 0x1000,别混)。
  • 板子下载电路残缺,需手动进下载模式按住 BOOT → 点一次 EN/RST → 松开 BOOT,烧完点一下 EN/RST 启动。
  • --erase-all 会把 NVS 一起清掉 → 恢复出厂(登录名密码回默认,WiFi/MQTT 配置需重设)。

图 1 引脚监控页:ESP32-WROOM-32 排针、USB 串口 CH340 标注与状态图例

三、页面功能一览

  • 设备拓扑:模块三态着色(正常=主题色/异常=红/未启用=灰),MQTT 已连接用绿区别于未启用的灰;点模块跳对应管理页。
  • WiFi 管理 :AP/STA 双模,修复了让 STA 客户端拿同网段 IP (如 192.168.71.x)而非隔离在 192.168.4.x 的问题。
  • BLE 扫描:0 结果通常只是射频环境,非错误。
  • MQTT 收发:连接 + 订阅/发布 + 确认回调。
  • 串口终端esp32:~$ 输入 + 快捷命令 tag(help/status/wifi/ble/mqtt/mem/pin/restart)点击即发。
  • 引脚监控 :排针图型号修正为 ESP32-WROOM-32 (官方 Datasheet),并补上 USB 串口 CH340,与开发板布局对齐。
  • 系统设置 → 配置管理:请求日志与汇总、改密、退出。

图 2 设备拓扑主页面:模块连线 + 串口终端 + RAM 内存明细卡片

图 3 串口终端:esp32:~$ 提示符与「可用命令」快捷 tag

四、前端请求架构:为什么把「并发」砍成 1

4.1 问题:请求把 socket 用光

IDF httpd 是单线程 selectmax_open_sockets 有限(本机 6 客户 + 3 内部)。而页面 events/status/mem/led 多个定时器互不知晓可同时发请求 → socket 打满 → 刷新打不开、接口全超时。

4.2 方案:全局串行请求队列

  • 恒 1 并发:同一时刻只发 1 个,其余排队,与 httpd「一次处理一个」天然匹配。
  • 去重合并:相同 GET 未开始就重复排队时丢旧留新,避免轮询积压。
  • 独立超时:每个请求 10s + AbortController,卡住主动断开。
  • 可取消:中止当前请求,队列自动跑下一个。

4.3 轮询收敛 + 后台暂停

  • events 1s→2s 、status 2s→3s
  • document.hidden:标签页切后台就停轮询,回来即恢复,后台零占用。

4.4 请求日志与汇总

  • 日志:最近 1000 条 、超出删最旧;已取消/超时/出错整行红/琥珀高亮。
  • 汇总:总/完成/已取消/超时/出错/合并/峰值队列,累计计数、刷新才清零,不受 1000 上限影响。

图 4 「请求日志与汇总」卡片:累计汇总 + 最近 1000 条日志(超时/出错高亮)

五、特色功能:GPIO 光学数据传输(LED)

图 5 LED 光学发送页:GB2312 直发、确认链说明与编码说明书(供接收端编程)

  • 编码:GB2312 查表(自建映射、无新依赖)直发中文,也支持摩尔斯。中文=2 字节、ASCII=1 字节、空格=0x20。
  • 帧格式[F5 0A][N][data][XOR 校验],位宽 66ms、帧间隔 333ms、MSB 先发。
  • 链路确认 :等 MQTT 主题 ack/led/decode 回显 {"原始报文":"...","服务端确认收到":"1"},=1 且报文一致才成功;否则/超时 5s 自动重发,最多 3 次。

中文字节统计小坑:前端算「字符/字节/bit」时中文字必须按 2 字节计,很多人会漏。

六、踩坑复盘

坑 1:剪贴板按钮「复制说明」点了没反应

现象: 「复制说明」按钮完全无效。

根因: navigator.clipboard 只在 https/localhost 等安全上下文可用;file:// 打开时不存在;且回调里读了空元素的 textContent 抛 TypeError 吞掉复制动作。

解决: 改用只读文本框自动写入并全选、用户 Ctrl+C。约定:前端复制一律用只读文本框,别用 clipboard API。
坑 2:ESP32 无 PSRAM 触发的 OOM 崩溃(最严重)

现象: LED 光学发送后整机 abort 重启。

日志:

复制代码
I (888955) [APP] [MEM] 空闲 1KB(历史最低 0KB,最大连续块 1KB)
I (889959) [APP] [MEM] 空闲 0KB
memory allocation of 118 bytes failed
abort() was called at PC 0x4013657a on core 0
rst:0xc (SW_CPU_RESET)

根因: 无 PSRAM、可用堆约 230KB;WiFi + NimBLE(BLE 常开)+ std + TLS + httpd 分完只剩约 59KB(历史最低 7KB)。LED 发送(12KB 栈线程+帧缓冲)+ 前端高频轮询把它打穿 → 118B 小分配失败即 abort。

关键: httpd 45062 ESP_ERR_HTTPD_RESP_SENDerror in send:11 只是 OOM 症状,非元凶

解决: ① 发送前内存门槛 (空闲<30KB 优雅拒绝);② LED 线程栈 12KB→6KB;③ 请求收敛降频+后台暂停+串行队列;④ 新增 /api/v1/mem 按堆分区(DEFAULT/8BIT/EXEC/DMA/INTERNAL)统计 + 设备拓扑「RAM 内存明细」卡 + 堆<16KB 自动打串口。BLE 约占 30KB 堆,可禁用释放(配置层、需全量重编,由你决定)。
坑 3:请求把 httpd socket 占满

现象: 刷新打不开、多接口同时超时。**根因/解决:**同 4.1/4.2(全局串行队列恒 1 并发+去重+超时+可取消)。
坑 4:底部「请求中」提示条一直闪

现象: 底部一直弹「请求中 GET /events ...」很闹心。

根因: 触发条件太松------只要队列有排队就显示,而 events/status 连续轮询几乎总有排队。

解决: 先改「真正偏慢才显示」(常规轮询>2.5s、主动操作>0.7s),最后整个移除悬浮条,取消入口统一收进「配置管理 → 取消当前请求」,明细看请求日志卡。
坑 5:引脚页型号错误 + USB 缺失

现象: 排针图标注成别的型号(ESP32-DevKitC V4)且无 USB 位置,与实物对不上。

解决: 以官方文档为准改成 ESP32-WROOM-32 (标题/说明/数据来源统一),并在下缘补画 USB 串口 CH340
坑 6:Rust 交叉编译的几处小坑

  • 32 位 Xtensa 不能用 AtomicI64,换 AtomicI32
  • Mutex::lock() 必须 .unwrap()
  • anyhow! 必须 use anyhow::anyhow;
  • 除非必要不新增依赖(源被墙+长编译),能 FFI 手写就不引 crate。
    坑 7:烧录报「COM6 拒绝访问」(PermissionError 13)

现象: esptool 报 Could not open COM6 ... PermissionError(13)

根因: 端口被瞬态占用(串口助手/监视器/上句柄未释放)。

**解决:**确认无占用、必要时拔插重枚举、等句柄释放后重试即成功;PnP 能看到 CH340 说明端口本身健康。

七、收获与建议

经验 要点
堆是第一资源 无 PSRAM 时 WiFi+BLE+std 已吃掉大半,任何功能都可能压垮堆,先看 BLE/并发/tmp 缓冲
httpd 单线程 前端必须主动把并发压成 1、加超时,别指望设备端
可观测性先行 内存分区统计 + 请求日志,比事后猜快得多
配置 > 全量重编 改一个 sdkconfig(如禁 BLE)能释放几万字/堆,但代价是长编译,权衡再动

条件允许建议换带 PSRAM 的模组,或按需关闭 BLE,把余量留宽裕。