两块 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/ 是上游的按芯片分目录,里面是 源码,必须保留。
158 lines
5.3 KiB
Markdown
158 lines
5.3 KiB
Markdown
# cam-client
|
||
|
||
[ESP32-S3 摄像头固件](../README.md) 的桌面查看器。接收 UDP 数据报形式的 JPEG 帧,
|
||
解码后显示实时画面,并给出帧率与丢帧统计。
|
||
|
||
Rust + [Slint](https://slint.dev),不需要额外的系统库。
|
||
|
||
## 环境要求
|
||
|
||
- Rust(普通工具链——**不要**用固件那个 `esp` 工具链)
|
||
- 已在运行本固件的摄像头,且能在 2.4GHz WiFi 下访问到
|
||
|
||
## 目标架构
|
||
|
||
**编译前先设好。** 本目录位于固件源码树内部,Cargo 否则会继承
|
||
`../.cargo/config.toml` 去交叉编译 `xtensa-esp32s3-espidf`——报错会出现在依赖
|
||
深处,而不是一眼可见的地方。
|
||
|
||
`client/.cargo/config.toml` 因此把目标钉成宿主架构:
|
||
|
||
```toml
|
||
[build]
|
||
target = "aarch64-apple-darwin" # Intel Mac:x86_64-apple-darwin
|
||
# Linux:rustc -vV | grep host 的输出
|
||
```
|
||
|
||
查看你的:
|
||
|
||
```sh
|
||
rustc -vV | grep host
|
||
```
|
||
|
||
## 编译
|
||
|
||
```sh
|
||
cargo build --release
|
||
```
|
||
|
||
因为上面的 target 覆盖,二进制落在带架构名的目录里:
|
||
|
||
```sh
|
||
./target/aarch64-apple-darwin/release/cam-client
|
||
```
|
||
|
||
## 运行
|
||
|
||
```sh
|
||
./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 时:
|
||
|
||
```sh
|
||
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。一个笃定的错误数字比没有数字更糟。
|
||
|
||
## 诊断
|
||
|
||
```sh
|
||
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`](../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`。 | |