Files
ESP32S3_vt/protocol/README.md
T
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

95 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# protocol
摄像头 UDP 图传的线路格式,**固件与客户端共用**。
这是本仓库里最该被测试的部分:帧重组和编码都极易出微妙错误,而这两件事都无法
在固件上验证——你只会看到"不出画面"。
因此它没有 ESP-IDF 依赖,会为宿主架构编译:
```sh
cargo test
```
```
7 passed
```
## 线路格式
每个数据报 = 20 字节头 + 至多 1400 字节 JPEG 数据:
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-------------------------------+-------------------------------+
| magic (u32) | frame_id (u32) |
+-------------------------------+-------------------------------+
| frag_index (u16) | frag_count (u16) |
+-------------------------------+-------------------------------+
| total_len (u32) | width (u16) | height (u16) |
+-------------------------------+---------------+---------------+
| payload (0..=1400) |
+---------------------------------------------------------------+
```
全部整数为**小端**——ESP32-S3(Xtensa)和所有桌面架构都原生如此,编解码就是
`to_le_bytes` / `from_le_bytes`,没有字节序转换步骤。
- `magic` 固定值,用于把同端口上的无关 UDP 包挡掉。
- `total_len` 是整张 JPEG 的长度,接收端据此预分配缓冲并判断帧是否完整。
- `width`/`height` 每个分片都重复携带(不只是第 0 个),这样每个数据报都能独立
解析,中途加入的接收端也能立刻知道分辨率。
## 为什么是 UDP 而不是 TCP
这是本 crate 存在的原因,也是实测数据:
| | TCP(HTTP MJPEG) | UDP |
|---|---|---|
| 传感器实际产出 | 22 fps | 22 fps |
| 客户端实际收到 | **1.9 fps** | **20.8 fps** |
| 实际吞吐 | 0.28 Mbit/s | 1.72 Mbit/s |
关键在于**链路是空的**:TCP 那一栏只用了链路能力的 1% 左右(ESP32-S3 在良好的
2.4GHz 链路上,TCP 实测上限约 20–30 Mbit/s),帧却堵在队头阻塞后面等一个实时画面
等不起的重传计时器。
UDP 没有任何重传与确认:缺了分片的帧被整个丢弃,下一帧顶上。这换来低延迟,也把
本来就有的带宽真正用起来了——但即便如此也只用掉 6–9%,说明瓶颈从头到尾都不在
带宽上。
## 接收端的取舍
`FrameAssembler` 的策略就是全部要点:**只有分片全部到齐才显示该帧,且最新的帧永远
优先。**
UDP 不保证跨数据报的顺序或到达,所以试图"补洞"的接收端做的功比"等下一张完整帧"
更多——而且补的过程中会显示撕裂的画面。
另外有一个有界超时,用来堵住另一种失败模式:发送方在帧中途死掉时,半截缓冲会
永远留在这里。
| 字段 | 含义 |
|---|---|
| `frag_count == 0` | 非法,立即拒绝 |
| `frag_index >= frag_count` | 自相矛盾,立即拒绝 |
| `payload_len > total_len` | 损坏,立即拒绝(在造成大额分配之前就挡住) |
## 发送端
`fragment_frame()` 从 JPEG 长度推出分片数,因此调用方不可能产出与自身矛盾的
分片集合。
`Fragment::encode_into()` 追加写入而不分配:发送端对每帧的每个分片复用同一个缓冲,
在 22fps 下这是每秒约 250 次分配与不分配的区别——在 WiFi 栈也要抢内部 RAM 的芯片
上,这个差别很实在。
## 不变量
- 一个数据报不超过 `HEADER_LEN + MAX_FRAGMENT_PAYLOAD` = 1420 字节,低于典型以太网
的 1500 MTU。
这一点比看起来重要:超过 MTU 的数据报要么被 AP 丢掉,要么被 IP 分片,而两种情况
的丢包都会**记在应用账上**而不是网络账上——后者本来更容易排查。
- VGA 下 `MAX_FRAGMENT_PAYLOAD` 对应每帧 8–10 个数据报。
- `DEFAULT_PORT` = 5600,两端共用同一个常量。