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
+27 -28
View File
@@ -1,8 +1,8 @@
# 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与裁判终端联动
# 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与 RMIC::2026 桌面端联动
一辆车四块装甲板(前/后/左/右),**只有主板一颗 MCU**:另外三块"从"板里没有单片机,
各自只有一路 WS2812 和一路压电片,直接接线到主板上。主板负责四路击打检测、四路灯带、
整车血量,并把血量广播给本项目裁判终端。
整车血量,并把血量广播给 RMIC::2026 桌面端。
固件是跑在 ESP-IDF 上的 Rust,目标芯片 **ESP32-C3(RISC-V)**。
@@ -28,10 +28,10 @@ armor/
| 功能 | GPIO | 说明 |
| --- | --- | --- |
| WS2812 前 | **IO10** | 每条灯带 3 颗灯珠,GRB、MSB first |
| WS2812 后 | **IO5** | |
| WS2812 左 | **IO6** | |
| WS2812 右 | **IO7** | |
| WS2812 前 | IO10 | 每条灯带 3 颗灯珠,GRB、MSB first |
| WS2812 后 | IO5 | |
| WS2812 左 | IO6 | |
| WS2812 右 | IO7 | |
| 压电片 前 | GPIO0(ADC1_CH0) | 0 dB 衰减、12 bit |
| 压电片 后 | GPIO4(ADC1_CH4) | |
| 压电片 左 | GPIO3(ADC1_CH3) | |
@@ -62,8 +62,8 @@ armor/
| 复位(新一局) | 长按 BOOT ≥500 ms → 满血、清计数 | `Button` + `Armor::reset` |
| 打印状态 | 短按 BOOT → 立刻打一行状态并重发上报 | `ButtonEvent::Click` |
**整车一条血量,四格同值。** 板子算账、裁判端只显示:每条上报的 `hp` 相同,
`index` 分别是 0..3,所以裁判端已经画好的四格血量界面不用改。
**整车一条血量。** 板子算账、裁判端只显示:四条上报的 `hp` 相同,`index` 分别是 0..3,
界面按 `(team, index)` 收齐四条后显示同一个数值。
**为什么要"每板独立冷却"。** 参考实现用全局 50 ms 冷却:一块板挨打后,别块板在这
50 ms 内的击打会被吃掉。四路各自独立才是"每面各自挨打"的正确语义,这里有单元测试
@@ -82,25 +82,23 @@ magic "MVC1"(4) | type(1)=5 | version(2) | sender_id(4) | ... | team(1) | index(
- `sender_id` 用 MAC 派生的板子 id(`protocol::id_from_mac`),不是 0 —— 0 是裁判
终端的保留 id,抓包时一块装甲板发的包不该长得像裁判端发的。
- 四条 `index` 的 `hp` 相同(整车一条血),裁判端按 `(team, index)` 收齐四条才显示。
- 广播而不是单播:裁判终端的 IP 是 DHCP 给的,会变。图传板用 Hello 广播绕开这件事,
- 广播而不是单播:桌面端电脑的 IP 是 DHCP 给的,会变。图传板用 Hello 广播绕开这件事,
装甲板沿用同一个约定。
### 裁判端怎么收
### 桌面端怎么收
`client/src/armor.rs` 里的 `ArmorHealthStore`(裁判端角色才启动):
`desktop/core/src/armor.rs` 里的 `ArmorHealthStore`(两种身份都启动):
- 绑 `0.0.0.0:5601` 收广播,按 `(team, index)` 存最后一次读数。
- **四条都在 1.5 s 内收到**才交给界面显示;否则回退占位符。心跳 200 ms,1.5 s 是连丢
7 个包,偶发丢包不会让四个格子各自跳。
- 队伍端不用它(界面不显示对手血量),所以那台机器不会去占 5601。
- 血量变化会打日志(`armor: team 0 hp 100 -> 97`):窗口是给别人看的,抓日志的人
也得能看见掉血。
7 个包,偶发丢包不会让读数来回跳。
- 用户端也要显示双方血条,所以两端都会占 5601。
- 血量变化会打日志(`armor: board 0x… team 0 hp 100 -> 97`):窗口是给别人看的,
抓日志的人也得能看见掉血。
**裁判端界面原本看不到血量。** 协议与 `ArmorSource` 早已就位,但 Slint 里那 4 格只是
绿/灰方块 —— `hps` 的数值从没被渲染,于是"板子被打到 0"在界面上完全看不出来(现场
表现就是"敲死了裁判系统没掉血")。现在 `TeamHealth` 多了一个 `hp-text` 与 `critical`:
队伍名旁边直接显示血量数字,低于 30 或归零时数字与血条转红。归零**不写"已击毁"**——那一刻
图传中央会给出「胜利 / 击败」的结论(见 [`desktop/README.md`](../desktop/README.md))。
**界面显示血量数字。** `TeamHealthDto` 带 `hp` 与 `critical`:数值就在队伍名旁边,低于
30 或归零时它与血条一起转红。归零**不写"已击毁"**——那一刻图传中央会给出
「胜利 / 击败」的结论(见 [`desktop/README.md`](../desktop/README.md))。
## 构建
@@ -157,7 +155,9 @@ cd protocol && cargo test # 线路格式往返(含 ArmorHealth
```
(`armor-protocol/.cargo/config.toml` 把 target 覆盖回 host:它坐在 `armor/` 目录树
里,否则会继承 RISC-V 目标,链接必然失败。这与 `protocol/`、`client/` 的做法一致。)
里,否则会继承 RISC-V 目标,链接必然失败。这与 `protocol/`、`desktop/` 的做法一致。
换到其他宿主平台时,配置里钉住的 `aarch64-apple-darwin` 不适用,按
[DEVELOPMENT.md](../DEVELOPMENT.md) 用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。)
## 烧录与实测
@@ -208,7 +208,7 @@ W (306504) armor: BOOT long press: hp reset to 100 (new round)
| 启动 | 无 panic/重启;状态行每秒一条 |
| WiFi | 关联 `SR-602-2G4`(rssi −36,ch 6),DHCP 拿到 `192.168.1.117` |
| 上报 | `scripts/armor_probe.py` 收到队伍一四条、约 5 包/秒、`sender_id 0xA820D321`、四格同值 |
| 裁判端 | `cam-client --role referee` 日志 `team 0 plate 0..3 is online (hp 100) via 192.168.1.117`,界面显示血量数字 |
| 裁判端 | 旧客户端 `cam-client --role referee` 日志 `team 0 plate 0..3 is online (hp 100) via 192.168.1.117`,界面显示血量数字 |
| 受击 | 敲前装甲板:`hit plate=front raw=2663..4095`,每次 −3(100→97→94…),只有 front 计数 |
| 变暗 | 敲击瞬间**只有前那一路**全黑 50 ms,其余不变(现场确认) |
| 死亡 | 34 击到 0;状态行 `dead=true strips[front=off rear=off left=off right=off]`,四路全黑 |
@@ -218,10 +218,9 @@ W (306504) armor: BOOT long press: hp reset to 100 (new round)
### 裁判端这边踩到的坑:界面根本不显示血量
协议、`ArmorSource`、`ArmorHealthStore` 都对,但改之前 Slint 里那 4 格只是绿/灰方块,
`hps` 的**数值从不渲染**——板子被打到 0,界面四格还是绿的,看起来像"没掉血"。现在
队伍名旁边直接显示数字(低于 30 转红,归零不写"已击毁",改由图传中央的胜负横幅表达),
并且客户端日志会打 `armor: team 0 hp 100 -> 97`。
协议、`ArmorSource`、`ArmorHealthStore` 都对,但改之前旧界面上那几格只是绿/灰方块,
`hps` 的**数值从不渲染**——板子被打到 0,格子还是绿的,看起来像"没掉血"。修好之后
队伍名旁边直接显示数字,客户端日志也会打 `armor: board 0x… team 0 hp 100 -> 97`。
## 实现要点(都是踩过的坑)
@@ -333,7 +332,7 @@ status: hp=0 hits=37 dead=true plates[front=37 rear=0 left=0 right=0]
| 受击变暗 | 四条灯带一起全黑 50 ms | 只暗被打的那一路 | 每面独立灯带的意义就在这里 |
| 冷却窗口 | 全局 50 ms | 每板独立 50 ms | 别块板的击打不该被吃掉 |
| 灯带刷新 | 每 10 ms 无条件刷四遍 | 只在内容变化时刷 + 每秒强制全刷 | 四路独立控制后,省掉四轮 RMT 开销 |
| 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端已画好的四格血量对齐 |
| 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端界面的血量显示对齐 |
| 颜色切换 | BOOT 短按切红/蓝,存 NVS | 颜色由 `ARMOR_TEAM` 决定(红/蓝),短按打状态 | 队别不该靠现场按键切错 |
| 按键 | 移植 OneButton 状态机(短按配对,长按切色) | 本地 FSM:长按复位血量、短按打状态 | 只有两种动作,且要能在主机上测 |
| 上报 sender_id | —(不走 UDP) | MAC 派生 id | 0 是裁判终端保留 id |
@@ -349,4 +348,4 @@ status: hp=0 hits=37 dead=true plates[front=37 rear=0 left=0 right=0]
`armor-protocol` 里已经预留了板位枚举与上报格式,加一个 `slave` bin 即可。
- **双击 / 多击**:`Button` 只实现短按与长按,参考实现里未使用的 OneButton 功能没有移植。
- **下发血量**:当前板子本地算账(裁判端只显示)。若以后要裁判端权威(下发血量/复位),
需要新增一条下行消息并在 `client` 里发。
需要新增一条下行消息并在 `desktop` 里发。