fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
构建客户端 / 检查(类型 + 单元测试) (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

- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- style(client): 应用图标增加圆角,统一“裁判系统客户端”命名
- fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- docs: 润色项目说明并统一裁判系统术语
- test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成

验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
This commit is contained in:
2026-10-21 09:25:17 +08:00
parent 6a0eb14b28
commit 054379dc64
52 changed files with 1070 additions and 280 deletions
+17 -44
View File
@@ -53,8 +53,7 @@ armor/
| 复位(新一局) | 长按 BOOT ≥500 ms → 满血、清计数 | `Button` + `Armor::reset` |
| 打印状态 | 短按 BOOT → 立刻打一行状态并重发上报 | `ButtonEvent::Click` |
**整车一条血量。** 板子算账、裁判端只显示:四条上报的 `hp` 相同,`index` 分别是 0..3,
界面按 `(team, index)` 收齐四条后显示同一个数值。
整车共用一条血量,由板子计算,裁判端只负责显示。四条上报的 `hp` 相同,`index` 分别为 0..3;界面按 `(team, index)` 收齐四条后显示同一个数值。
参考实现使用全局 50 ms 冷却,一路受击会抑制其余路在该窗口内的击打。当前四路分别冷却,由 `cooldown_is_per_plate_not_global` 测试验证。
@@ -66,28 +65,21 @@ armor/
magic "MVC1"(4) | type(1)=5 | version(2) | sender_id(4) | ... | team(1) | index(1) | hp(2)
```
- **血量一变立刻发**,没变就按 200 ms 心跳重发。裁判端靠心跳判断板子还在线,一次
丢包不该让界面退回"未接入"。
- `sender_id` 用 MAC 派生的板子 id(`protocol::id_from_mac`),不是 0 —— 0 是裁判
终端的保留 id,抓包时一块装甲板发的包不该长得像裁判端发的。
- 血量变化时立即发送,未变化时按 200 ms 心跳重发。裁判端通过心跳判断板子是否在线,避免一次丢包就将界面切回“未接入”。
- `sender_id` 使用 MAC 派生的板子 id(`protocol::id_from_mac`),不使用 0。0 是裁判终端的保留 id,需避免将装甲板的数据包误认为裁判端数据。
- 四条 `index` 的 `hp` 相同(整车一条血),裁判端按 `(team, index)` 收齐四条才显示。
- 广播而不是单播:桌面端电脑的 IP 是 DHCP 给的,会变。图传板用 Hello 广播绕开这件事,
装甲板沿用同一个约定。
- 上报使用广播而非单播,因为桌面电脑通过 DHCP 获取的 IP 可能变化。图传板使用 Hello 广播处理这一问题,装甲板沿用相同约定。
### 桌面端怎么收
`desktop/core/src/armor.rs` 里的 `ArmorHealthStore`(两种身份都启动):
- 绑 `0.0.0.0:5601` 收广播,按 `(team, index)` 存最后一次读数。
- **四条都在 1.5 s 内收到**才交给界面显示;否则回退占位符。心跳 200 ms,1.5 s 是连丢
7 个包,偶发丢包不会让读数来回跳。
- 四条上报都在 1.5 s 内收到时才交给界面显示,否则显示占位符。心跳间隔为 200 ms,1.5 s 对应连丢 7 个包;偶发丢包不会让读数反复切换。
- 用户端也要显示双方血条,所以两端都会占 5601。
- 血量变化会打日志(`armor: board 0x… team 0 hp 100 -> 97`):窗口是给别人看的,
抓日志的人也得能看见掉血。
- 血量变化会写入日志(`armor: board 0x… team 0 hp 100 -> 97`),便于不查看窗口时也能核对扣血。
**界面显示血量数字。** `TeamHealthDto` 带 `hp` 与 `critical`:数值就在队伍名旁边,低于
30 或归零时它与血条一起转红。归零**不写"已击毁"**——那一刻图传中央会给出
“胜利 / 落败”的结论(见 [桌面客户端](../desktop/client.md))。
界面在队伍名旁显示血量数字。`TeamHealthDto` 提供 `hp` 与 `critical`,血量低于 30 或归零时,数字与血条一起转红。归零时不显示“已击毁”,图传中央会显示“胜利 / 落败”(见 [桌面客户端](../desktop/client.md))。
## 构建
@@ -107,9 +99,7 @@ ARMOR_THRESHOLD=1200 scripts/build_armor.sh # 换受击阈值(看日志里
产物:`armor/target/riscv32imc-esp-espidf/release/armor`。
`ARMOR_TEAM` 是**编译期**注入的:换队伍重刷固件即可,不用先擦 NVS。NVS 里另外记着
`team` 与 `color`(命名空间 `armor`,与参考实现同名),两者不一致时以 `ARMOR_TEAM`
为准并覆盖,日志里会说一声。
`ARMOR_TEAM` 在编译期注入,更换队伍后重新烧录固件即可,无需先擦除 NVS。NVS 另存有 `team` 与 `color`,命名空间为 `armor`,与参考实现同名。两者不一致时,以 `ARMOR_TEAM` 为准覆盖 NVS,并记录日志。
本机已有的 ESP-IDF 与工具链通过 `armor/.cargo/config.toml` 指定(`IDF_PATH` /
`ESP_IDF_TOOLS_INSTALL_DIR`),不需要先 `activate` ESP-IDF。删掉那两行就交给
@@ -124,9 +114,7 @@ esp-idf-sys 自己下载它钉住的版本。
| v6.0.1(eim 装的) | `/Users/peng/.espressif/v6.0.1/esp-idf` | Xtensa + **RISC-V(`esp-15.2.0_20251204`)** |
| v5.5.5(图传板在用) | `/Users/peng/.espressif/esp-idf/v5.5.5` | 只有 Xtensa |
C3 是 **RISC-V**,而 v5.5.5 那套缺 `riscv32-esp-elf`(它要的是 `esp-14.2.0_20260121`,
本机没有,而 GitHub release 又下不动)。所以装甲板固件指向 v6.0.1 —— 它的工具链齐全,
且 `esp-idf-sys` 0.38.1 支持的版本范围是 4.4.4+、5.0–5.5、**6.0–6.2**。
C3 使用 RISC-V,而本机 v5.5.5 环境缺少 `riscv32-esp-elf`,所需版本为 `esp-14.2.0_20260121`;本机未安装,GitHub release 下载也未成功。因此装甲板固件使用工具链齐全的 v6.0.1。`esp-idf-sys` 0.38.1 支持的版本范围为 4.4.4+、5.0–5.5、6.0–6.2。
两套 IDF 共存没有冲突:各自 `.cargo/config.toml` 里的 `IDF_PATH` 是每套 crate 自己钉的。
@@ -143,10 +131,7 @@ cd armor/armor-protocol && cargo test # 22 个测试
cd protocol && cargo test # 线路格式往返(含 ArmorHealth)
```
(`armor-protocol/.cargo/config.toml` 把 target 覆盖回 host:它坐在 `armor/` 目录树
里,否则会继承 RISC-V 目标,链接必然失败。这与 `protocol/`、`desktop/` 的做法一致。
换到其他宿主平台时,配置里钉住的 `aarch64-apple-darwin` 不适用,按
[开发与验证](../development/firmware.md) 用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。)
`armor-protocol/.cargo/config.toml` 将 target 覆盖为 host,否则它会从上层 `armor/` 继承 RISC-V 目标,导致链接失败。这与 `protocol/`、`desktop/` 的做法一致。更换宿主平台时,固定的 `aarch64-apple-darwin` 不再适用,需按[开发与验证](../development/firmware.md)用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。
## 烧录与实测
@@ -159,7 +144,7 @@ scripts/flash_armor.sh --no-monitor # 只烧不抓
烧的是 ELF(`espflash` 自己从 ELF 里读分区表与 bootloader),芯片 `esp32c3`。
不打开裁判端界面,也可以单独确认板子在广播、血量对不对:
不打开裁判端界面,也可单独检查板子的广播和血量:
```bash
python3 scripts/armor_probe.py 15
@@ -212,8 +197,7 @@ W (306504) armor: BOOT long press: hp reset to 100 (new round)
### 1. ESP32-C3 的 RMT 只有 96 个 symbol 的通道内存
C3 的 RMT 只有一个 block,总计 96 个 symbol,按 48 为单位分给两个 TX 通道。`mem_block_symbols = 64`
会被向上取整到 96,也就是一个通道吃光全部内存,第二个 `rmt_new_tx_channel()` 就会失败:
C3 的 RMT 只有一个 block,共 96 个 symbol,按 48 为单位分配给两个 TX 通道。`mem_block_symbols = 64` 会向上取整到 96,使一个通道占满全部内存,第二个 `rmt_new_tx_channel()` 因此失败:
```text
E rmt: rmt_tx_register_to_group(168): no free tx channels
@@ -221,10 +205,7 @@ E rmt: rmt_tx_register_to_group(168): no free tx channels
C3 只有 2 个 TX 通道,四路灯带无法各占一个。因此只创建一个通道,使用全部 96 个 symbol,再通过 `rmt_tx_switch_gpio()` 依次切换四个 GPIO。每帧为 3 珠 × 24 bit + reset,共 73 个 symbol,每路发送约 90 µs。
**为什么不用 `esp-idf-hal` 的 `TxChannelDriver`。** 它把 `rmt_channel_handle_t` 藏在
结构体里(`as_ptr()` 是 `pub(crate)`),拿不到句柄就调不了 `rmt_tx_switch_gpio()`。
所以 `leds.rs` 直接用 sys 绑定:通道、官方 copy encoder、transmit 都是 IDF 原生的那
几个函数,用法与参考实现的 `led_strip_encoder.c` 一一对应。
未使用 `esp-idf-hal` 的 `TxChannelDriver`,是因为它未公开结构体中的 `rmt_channel_handle_t`(`as_ptr()` 为 `pub(crate)`),无法取得句柄调用 `rmt_tx_switch_gpio()`。因此 `leds.rs` 直接使用 sys 绑定,通道、官方 copy encoder 和 transmit 均调用 IDF 原生函数,用法与参考实现的 `led_strip_encoder.c` 一一对应。
参考实现每 10 ms 刷新四路同色灯带。这里四路颜色独立,只有内容变化时才执行 `disable → switch → enable → transmit`;每秒另做一次全量刷新,避免灯带因掉电、静电等丢帧后一直保持错误状态。
@@ -232,10 +213,7 @@ C3 只有 2 个 TX 通道,四路灯带无法各占一个。因此只创建一
一次击打的波形只有几十微秒宽,oneshot 轮询容易错过尖峰,造成轻敲漏判。采样方式与参考实现一致:四通道合计 40 kHz,每通道 10 kHz;每帧为 10 ms、共 400 个采样点,主循环按帧处理。
`esp-idf-hal` 0.47 的 `adc::continuous` 多通道入口 `EmptyAdcChannels::chain` 的约束是
`A: Adc` 且 `O: AdcChannels<AdcUnit = A>`:`ADC1` 满足前者、`ADCU1` 满足后者,同一个
类型参数凑不齐,四路 ADC1 通道拼不出来。所以 `piezo.rs` 同样直接用 sys 绑定,与参考
实现逐行对应。
`esp-idf-hal` 0.47 的 `adc::continuous` 多通道入口 `EmptyAdcChannels::chain` 要求同时满足 `A: Adc` 和 `O: AdcChannels<AdcUnit = A>`。`ADC1` 满足前者,`ADCU1` 满足后者,但同一个类型参数无法同时满足,因而不能组合四路 ADC1 通道。`piezo.rs` 因此也直接使用 sys 绑定,与参考实现逐行对应。
采样点的通道号由硬件结果自带(`type2.channel()`),映射不依赖"结果按 pattern 顺序
轮转"这个假设。
@@ -261,12 +239,9 @@ factory 划了 2 MB。
### 7. 判断"连上没有"要问 IDF,不要信 esp-idf-svc 的事件缓存
`BlockingWifi::is_connected()/is_up()/wait_netif_up()` 读的是 esp-idf-svc 自己维护的
WiFi 事件状态。实测在 **IDF v6.0.1 + esp-idf-svc 0.53** 上它会停在"已连接":日志说
一切正常、`get_ip_info()` 却是 `0.0.0.0`,而 IDF 的 `esp_wifi_sta_get_ap_info()` 明明
说没关联。后果是 `join()` 假装连上了、看门狗永远不会重试,板子静默失联。
`BlockingWifi::is_connected()/is_up()/wait_netif_up()` 读取 esp-idf-svc 维护的 WiFi 事件状态。实测在 IDF v6.0.1 + esp-idf-svc 0.53 上,缓存状态会停在“已连接”:日志显示正常,但 `get_ip_info()` 为 `0.0.0.0`,IDF 的 `esp_wifi_sta_get_ap_info()` 也显示未关联。这使 `join()` 误判连接成功,看门狗不再重试,板子失联却没有相应提示。
所以固件里一律用 IDF 的 API 判断:
因此,固件统一通过 IDF API 判断连接状态:
- 关联:`esp_wifi_sta_get_ap_info()`
- 地址:`esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")` + `esp_netif_get_ip_info()`
@@ -284,9 +259,7 @@ status: hp=0 hits=37 dead=true plates[front=37 rear=0 left=0 right=0]
ap=SR-602-2G4 rssi=-29 ch=6 ip=192.168.1.117 reports=1745 errs=0
```
- `peak[...]` 是**这一秒内每路 ADC 的峰值**,用来校准阈值:没敲时的底噪要明显低于
阈值,正常敲击要明显高于它(实测底噪 ~400–500,敲击 2000–4095)。只报"有没有过阈值"
没法判断阈值该调到哪。
- `peak[...]` 记录这一秒内每路 ADC 的峰值,用于校准阈值:未敲击时的底噪应明显低于阈值,正常敲击应明显高于阈值(实测底噪 ~400–500,敲击 2000–4095)。仅记录是否超过阈值,无法判断应如何调整。
- `thr=` 是当前阈值,`ARMOR_THRESHOLD=1200 scripts/build_armor.sh` 可以改。
- `ap=` / `ip=` / `reports=` 用于区分未联网和已发送但对端未显示的情况。
+4 -4
View File
@@ -18,9 +18,9 @@
| D0–D7 | 11, 9, 8, 10, 12, 18, 17, 16 |
| PWDN | 未连接(`-1`) |
当前 XCLK 为 20 MHz。不要直接套用 AI-Thinker、XIAO 等其他板卡的相机引脚表。
当前 XCLK 为 20 MHz。AI-Thinker、XIAO 等其他裁判系统的相机引脚表不能直接套用。
本模组需避开的资源包括 Flash / PSRAM 总线(GPIO 26–37)、USB(19 / 20)、串口(43 / 44)及模组内部引脚;具体以原理图为最终依据。占用 Flash / PSRAM 引脚可能造成传输期间 CPU 挂死、串口静默。
接线时需避开本模组的 Flash / PSRAM 总线(GPIO 26–37)、USB(19 / 20)、串口(43 / 44)及模组内部引脚,具体以原理图为准。占用 Flash / PSRAM 引脚可能导致传输期间 CPU 挂死、串口无输出。
## 内存与任务配置
@@ -64,13 +64,13 @@
`scripts/flash.sh` 保留的流程是:编译、导出 App、搜索 64 KiB 对齐的可写偏移、修改分区、重新构建和验证,再写入三段镜像。重新构建可能移动镜像内容,因此第二次验证不可省略。
脚本依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录及旧版工具参数,不适合作为通用烧录入口。坏板需核对工具版本及坏地址字节;普通板卡使用 README 流程。更换故障模组比长期依赖镜像位置更可靠。
脚本依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录及旧版工具参数,不适合作为通用烧录入口。坏板需核对工具版本及坏地址字节;普通裁判系统使用 README 流程。更换故障模组比长期依赖镜像位置更可靠。
## 本地相机组件
`components/esp32-camera/` 包含上游 2.1.7 及本地兼容补丁,`components/cam_wrapper/` 提供 Rust FFI 封装。
本地增加 `esp_camera_fb_get_latest` / `cam_take_latest`:等待第一张有效帧后,以有限次数零等待取出更新帧,并归还替换的缓冲。`cam_take(0)` 表示单次非阻塞尝试,不在空队列、坏 JPEG 或 DMA 复位哨兵上继续等待。升级驱动时需保留或移植这些补丁,并运行缓冲所有权测试;不能用“先检查有帧、再调用可能阻塞 4 秒的 get”替代。
本地新增 `esp_camera_fb_get_latest` / `cam_take_latest`:等待第一张有效帧后,以有限次数零等待尝试取出更新帧,并归还被替换的缓冲。`cam_take(0)` 表示单次非阻塞尝试,遇到空队列、坏 JPEG 或 DMA 复位哨兵时不再等待。升级驱动时需保留或移植这些补丁,并运行缓冲所有权测试;不能改成先检查有帧,再调用可能阻塞 4 秒的 get。
S3 的 `ll_cam.c` 中有 `CAM_IO_MUX_REG()` 本地补丁,用于 SCCB / I2C 引脚连接。升级驱动后需检查补丁并验证传感器探测和采集。