# 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` 是 `!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`。 |