一、背景
玩园子这么多年了一直是在看园子的文章,但我自己没怎么发过。这两天突然发现了我几年前买的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(内核与库)
- Rust :
xtensa-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 是单线程 select 、max_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_SEND 和 error 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,把余量留宽裕。