Files
ESP32S3_vt/client
Horldsence 4f893ce140 ESP32-S3 摄像头图传:固件 + Slint 客户端 + 共享协议
两块 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/ 是上游的按芯片分目录,里面是
源码,必须保留。
2026-10-05 18:22:19 +08:00
..

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。