两块 ESP32-S3-WROOM-1-N16R8 图传板(DVP 接口接 OV2640/OV3660/OV5640), 向队伍客户端与裁判终端推 UDP 图传。固件是跑在 ESP-IDF 上的 Rust,用官方 esp32-camera 组件采集;客户端是 Rust + Slint 桌面程序。 三个 crate 各自独立构建: . firmware,交叉编译到 Xtensa protocol/ 线路格式与帧重组,两端共用,无 ESP-IDF 依赖,有单元测试 client/ Slint 桌面客户端 视频走 UDP 而非 TCP:实测传感器产出 22 fps,TCP MJPEG 只送出 1.9 fps, 且只占用链路能力的 1%——链路是空的,帧堵在队头阻塞后面等一个实时画面 等不起的重传计时器。改为独立数据报、不重传、缺片丢整帧后达到 20.8 fps。 发现是双向的:板子每 2 秒广播 Hello,客户端广播 Subscribe,板子据此定向 推流。板子身份由 MAC 派生,重启不变,所以客户端可以把绑定存进配置。 裁判端同时收两块板子(每个发送方一个重组器),队伍端绑定其中一块并只显示 自己那一边的血量。装甲板接口已预留(ArmorSource trait + ARMOR_PORT), 当前无数据源,界面显示占位符。 vendor 了 esp32-camera 2.1.7:组件注册表的版本求解器找不到它,且需要打一处 补丁(见 components/esp32-camera/target/esp32s3/ll_cam.c)。 .gitignore 三条 target/ 都锚定在根目录,因为 protocol/ 与 client/ 各自也是 独立 crate;而 components/esp32-camera/target/ 是上游的按芯片分目录,里面是 源码,必须保留。
cam-client
ESP32-S3 摄像头固件 的桌面查看器。接收 UDP 数据报形式的 JPEG 帧, 解码后显示实时画面,并给出帧率与丢帧统计。
Rust + Slint,不需要额外的系统库。
环境要求
- Rust(普通工具链——不要用固件那个
esp工具链) - 已在运行本固件的摄像头,且能在 2.4GHz WiFi 下访问到
目标架构
编译前先设好。 本目录位于固件源码树内部,Cargo 否则会继承
../.cargo/config.toml 去交叉编译 xtensa-esp32s3-espidf——报错会出现在依赖
深处,而不是一眼可见的地方。
client/.cargo/config.toml 因此把目标钉成宿主架构:
[build]
target = "aarch64-apple-darwin" # Intel Mac:x86_64-apple-darwin
# Linux:rustc -vV | grep host 的输出
查看你的:
rustc -vV | grep host
编译
cargo build --release
因为上面的 target 覆盖,二进制落在带架构名的目录里:
./target/aarch64-apple-darwin/release/cam-client
运行
./target/aarch64-apple-darwin/release/cam-client # 用下面的默认值
./target/aarch64-apple-darwin/release/cam-client 192.168.1.139
./target/aarch64-apple-darwin/release/cam-client 192.168.1.139 5600
用法: cam-client [CAMERA_IP] [PORT]
CAMERA_IP ESP32 摄像头地址(默认 192.168.1.139)
PORT UDP 视频端口 (默认 5600)
改 UI 时:
cargo run --release -- 192.168.1.139
界面读数
streaming 20.8 fps 10.1 kB per frame 35 ms between frames
640x480 shown 146 dropped 0 (0.0%) 1270 datagrams
丢帧率的存在是有理由的。这条链路整帧丢弃而不重试,所以单看帧率是有歧义的:
| 帧率 | 丢帧 | 判断 |
|---|---|---|
| 达到传感器上限 | 0 | 链路正常,瓶颈是传感器 |
| 远低于上限 | 有 | 瓶颈是网络 |
| 低 | 0,且显示 "no frames - camera?" | 板子没在推流 |
状态文字的颜色是同一信息的粗略版本:推流中绿色,否则琥珀色。
帧率是"计数"算出来的,不是"平均间隔"
不是对帧间隔取平均。一次 UI 轮询之间可能已完成好几帧,所以平均间隔量的是 显示的刷新快慢,而不是相机的出帧快慢——那曾把上限 22fps 的传感器报成 36fps。一个笃定的错误数字比没有数字更糟。
诊断
RUST_LOG=info ./target/aarch64-apple-darwin/release/cam-client 192.168.1.139
frames shown 146 | dropped 0 (0.0%) | datagrams 1270 | undecodable 0 | short 0
| 计数器 | 含义 |
|---|---|
frames shown |
完整重组并交给界面的帧 |
dropped |
因分片缺失而被放弃的帧 |
datagrams |
能解析为分片的数据报 |
undecodable |
收到了但解析不了的数据报 |
undecodable 非零意味着板子的数据报到了但没看懂——是协议或端口对不上,不是网络
问题。这两者很容易混淆,而这个计数器正是用来区分它们的。
排查
| 现象 | 原因 |
|---|---|
一直显示 connecting to … |
板子不在网上,或防火墙拦了入站 UDP 5600 |
no frames - camera? |
板子停止推流:重启了,或掉线了 |
| 完全没有输出,也没有日志 | host triple 不对,见目标架构 |
| 只有第一帧然后卡住 | 另一个客户端连在别的端口上;板子只推给最后宣告的那个 |
| 颜色错乱 | 通道顺序。当前构建用 Rgba8Pixel,另一个选项是 Bgra8Pixel |
编译在 libc / memmap2 报错 |
用的是 esp 工具链,或缺少 target triple 覆盖 |
目录结构
client/
├── Cargo.toml
├── build.rs 编译 ui/camera.slint
├── ui/camera.slint 窗口定义
└── src/
├── main.rs 命令行、线程编排、统计
└── receiver.rs socket、重组、JPEG 解码
线路格式在 ../protocol,与固件共用,两端不会漂移。
线程模型
三个线程,且没有任何 worker 线程碰 Slint 组件:
| 线程 | 职责 |
|---|---|
udp-reader |
recv_from、解析、重组 |
jpeg-decode |
JPEG → RGBA |
| 主线程(事件循环) | RGBA → slint::Image、更新属性,每 30ms 一次 |
这是 Slint 的硬性约束而非偏好:组件只能在拥有它的线程上使用,Weak<App> 是
!Send 就是为了强制这一点。worker 线程去 upgrade 一个 weak handle 会拿到 None
然后什么都不做——现象和"界面卡死"完全一样。
解码单独一个线程,是因为一张 VGA 的 JPEG 要花几毫秒,放在收包线程里会挤进数据报 之间,造成链路上本不存在的丢帧。
可调项
| 项目 | 位置 | 说明 |
|---|---|---|
| 轮询间隔 | src/main.rs 的 UI_POLL |
30ms。更快只是白费 CPU,不会多出帧。 |
| 帧率统计窗口 | FPS_WINDOW |
2 秒。在 ~22fps 下更短的窗口会量化失真。 |
| 帧超时 | receiver.rs 的 FRAME_TIMEOUT |
500ms。一帧约 40ms,更慢的就是丢了。 |
| 宣告间隔 | ANNOUNCE_INTERVAL |
500ms。同时兼作保活。 |
| 主题 | build.rs |
默认 fluent;可试 fluent-dark、material、native。 |