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/ 是上游的按芯片分目录,里面是 源码,必须保留。
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# 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`。 |
|
||||
Reference in New Issue
Block a user