docs: 按 humanizer 重写项目文档并同步 RMIC::2026 现状
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s

This commit is contained in:
2026-10-19 10:47:00 +08:00
parent d9a27b69f3
commit e3acb37d32
22 changed files with 3029 additions and 203 deletions
+21 -21
View File
@@ -3,9 +3,9 @@
摄像头 UDP 图传的线路格式,**固件与两端客户端共用**。
```
protocol/ 线路格式 + 帧重组 (13 个单元测试,无 ESP-IDF 依赖)
protocol/ 线路格式 + 帧重组 (23 个单元测试,无 ESP-IDF 依赖)
. 固件 -> ESP32-S3 (交叉编译,Xtensa)
client/ 桌面客户端 (Slint,两种身份)
desktop/ 桌面客户端 (RMIC::2026,Tauri v2 + React + Rust,两种身份)
```
这是本仓库里最该被测试的部分:帧重组和编码都极易出微妙错误,而这两件事都无法
@@ -17,23 +17,25 @@ client/ 桌面客户端 (Slint,两种身份)
cd protocol && cargo test
```
非 macOS / Apple Silicon 主机上,`protocol/.cargo/config.toml` 里钉住的 target 不适用,需按 [DEVELOPMENT.md](../DEVELOPMENT.md) 用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。本机实测:
```
13 passed
23 passed
```
## 拓扑
```text
┌──────────────────────────────┐
│ 裁判终端(PC,仅显示) │
│ 队伍一 + 队伍二 + 双方血量 │
│ RMIC::2026 桌面端(仅显示) │
│ 队伍一 + 队伍二 + 双方血量 │
└───────────────┬──────────────┘
│ 同一个局域网,广播可用
┌────────────────────┼────────────────────┐
│ │ │
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
│ 图传板 A │ │ 图传板 B │ │ 装甲板 ×4 │
│ (队伍一) │ │ (队伍二) │ │ (预留,未实现) │
│ (队伍一) │ │ (队伍二) │ │ (已实现) │
└───────────────┘ └───────────────┘ └───────────────┘
```
@@ -42,7 +44,7 @@ cd protocol && cargo test
## 线路格式
每个数据报 = **27 字节头** + 至多 **1400 字节**载荷:
每个数据报 = 27 字节头 + 至多 1400 字节载荷:
```text
0 1 2 3
@@ -72,12 +74,12 @@ cd protocol && cargo test
| 2 `Subscribe` | 客户端 → 板子 | "我是这个角色,请给我画面" |
| 3 `Hello` | 板子 → 客户端 | "我在线,我是谁" |
| 4 `Heartbeat` | 板子 → 客户端 | 同上,心跳形式 |
| 5 `ArmorHealth` | 装甲板 → 客户端 | 血量(**预留,当前无实现**) |
| 5 `ArmorHealth` | 装甲板 → 客户端 | 血量(已实现) |
### 三个设计决定
### 四个设计决定
**27 字节不凑整。** 头只占每个 1427 字节数据报的 1.9%,而为对齐在固件和客户端两侧
各写一段手写偏移常量,是引入静默错位的更划算的代价。
各写一段手写偏移常量,更容易引入静默错位。
**`sender_id` 是多路的前提。** 裁判终端同时收两块板子,靠它在同一端口上区分来源。
@@ -120,6 +122,7 @@ cd protocol && cargo test
烧录",而不是让人去猜。
这不削弱边界:视频、订阅、装甲板上报仍然走严格解析。
## 为什么视频走 UDP 而不是 TCP
实测(VGA 640x480,传感器产出 22 fps):
@@ -160,7 +163,7 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
它(`a_duplicate_cannot_complete_a_frame_with_a_hole`、
`a_gap_that_is_never_filled_never_yields_a_frame`)。
**不过要说清楚:在实测链路上一次重复投递都没出现过。** 一到两个订阅者、20 秒 2000+
**在实测链路上一次重复投递都没出现过。** 一到两个订阅者、20 秒 2000+
个分片,重复数为 0(固定端口与临时端口两种订阅方式都试过)。所以这层防护是为了正确
性,不是为了修一个正在发生的故障。
@@ -188,9 +191,8 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
### 发送端的复用
`Fragment::encode_into()` 追加写入而不分配:发送端对每帧的每个分片复用同一个缓冲,
在 22fps 下这是每秒约 250 次分配与不节省别的差别——在这块同时要抢内部 RAM 的芯片
上很实在。
`Fragment::encode_into()` 追加写入而不分配:发送端对每帧的每个分片复用同一个缓冲。
22 fps 下这是每秒约 250 次分配,省掉它们在这块同时要抢内部 RAM 的芯片上很实在。
`fragment_frame()` 从 JPEG 长度推出分片数,因此调用方不可能产出与自身矛盾的集合。
@@ -215,7 +217,7 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
丢包都**记在应用账上**而不是网络账上——后者本来更容易排查。
- VGA 下每帧约 17 个分片。
- `DEFAULT_PORT` = 5600,两端共用同一个常量。
- `ARMOR_PORT` = 5601,装甲板预留,当前无实现。
- `ARMOR_PORT` = 5601,装甲板血量上报。
## 身份
@@ -234,10 +236,8 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
例子也有——它的 `role` 字节曾多填 12 个零字节,固件按偏移严格读取因而拒收,表现为
"板子在线但 0 clients subscribed"。改协议时记得同步它。
## 装甲板(预留)
## 装甲板
`ArmorHealth` 与 `ARMOR_PORT` 已就位,`MsgType` 也留了 5 号。现在没有实现也没有
数据源,裁判界面显示占位符而不是 0——**0 看起来像真值,现场看到会以为对方已经没
血了**。
接上以后板子向 `ARMOR_PORT` 发这个结构,客户端监听即可,界面与其它代码都不用改。
`ArmorHealth` 占 `MsgType` 的 5 号。装甲板主板向 `ARMOR_PORT` 广播,`desktop/core/src/armor.rs`
里的 `ArmorHealthStore` 负责接收。收不到上报时界面显示占位符而不是 0——**0 看起来像真值,
现场看到会以为对方已经没血了**。