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
+66 -68
View File
@@ -1,13 +1,13 @@
# RMIC::2026
黑龙江大学 602 实验室 · Simba Robotics 的视野链路桌面端。同一进程提供用户端与裁判端两种界面。
黑龙江大学 602 实验室 · Simba Robotics 的裁判系统客户端。同一进程提供用户端与裁判端两种界面。
| 身份 | 画面 | 血量 | 装甲板 | 绑定 |
|---|---|---|---|---|
| 用户端(`team`) | 一路 | **双方**(左上角自己 / 右上角对手) | 自己那块 | **要,改动需密码** |
| 裁判端(`referee`) | 两路并排 | 双方(各自贴画面角上,**队名可改**) | 发现列表 + 槽位 | 不需要 |
前端使用 WebView 中的 React,以 canvas 绘制画面;后端使用 Rust。网络接收、重组、发现、配置、密码和录制均已重写为可单测的模块。
前端在 WebView 中运行 React,以 canvas 绘制画面;后端使用 Rust。网络接收、重组、发现、配置、密码和录制均已重写为独立模块,可分别运行单元测试。
> 显示名是 `RMIC::2026`;安装包和可执行文件使用 `RMIC-2026`,因为 Windows 文件名不允许 `:`。配置、录制与日志沿用 `cam-desktop` 目录,以保留已有密码哈希与绑定,见“配置”。
@@ -40,7 +40,7 @@ desktop/
红色用于需要注意的状态。开启的开关为白色,滑杆填充为钢灰色,分段控件以底部 2px 线标记选中,分区编号为灰色,顶栏不加装饰强调线。
队红刻意做得比强调红更暖更浅,否则两条血并列时会被误读成"系统在报警"。
队伍红色比强调红更暖、更浅,以免两条血量并列时看起来像系统在报警。
### 形状:矩形,唯一的斜边给装甲条
@@ -48,7 +48,7 @@ desktop/
### 动效:严格照 Material 3 的曲线与时长
`styles/tokens.css` 里直接写着 M3 的七条缓动与 short/medium/long 四组时长:
`styles/tokens.css` 定义了 M3 的七条缓动曲线与 short/medium/long 四组时长:
| 令牌 | 曲线 | 语义 |
|---|---|---|
@@ -58,9 +58,7 @@ desktop/
| `--m3-standard` / `-decelerate` / `-accelerate` | `(0.2,0,0,1)` / `(0,0,0,1)` / `(0.3,0,1,1)` | 常规位移 |
| `--m3-linear` | `(0,0,1,1)` | 恒速(仅用于跟踪与循环) |
旧名 `--e-out` / `--e-in` / `--d-1..5` 保留为**指向 M3 的别名**,所以没有一条规则落在自造
的贝塞尔上。两个例外是刻意的:`steps()` 用于录制红点与终端光标(离散闪烁),`linear` 用于
"没信号"那圈恒速转动的虚线环——M3 的曲线描述的是有始有终的转场,不是无限循环。
旧名 `--e-out` / `--e-in` / `--d-1..5` 保留为指向 M3 的别名,相关规则均引用这些曲线,没有自定义贝塞尔曲线。两种循环效果另行处理:`steps()` 用于录制红点与终端光标的离散闪烁,`linear` 用于“没信号”状态下恒速转动的虚线环。M3 曲线描述有起止的转场,不适用于这些无限循环。
### 动效:每一页只有一个签名动作
@@ -85,35 +83,32 @@ desktop/
| 中文 | **HarmonyOS Sans SC**(400 / 500 / 700) | 叙述、标签、按钮文字。字面方正,11px 仍然立得住 |
| 拉丁与数字 | **JetBrains Mono**(400 / 500 / 700) | 全部读数:帧率、血量、地址、倍率、时间码。等宽,列里的数字跳动时不左右抖 |
两者都以 woff2 自托管在 `public/fonts/`,随 `frontendDist` 进安装包,**运行时不碰网络**
(CSP 里 `default-src 'self'` 也不允许)。中文由 `cn-font-split` 切成 82 个 unicode-range
分片 × 3 个字重,浏览器按需加载,避免启动时加载完整的 14 MB 字体。
两种字体都以 woff2 格式保存在 `public/fonts/`,随 `frontendDist` 打入安装包,运行时无需联网加载,CSP 的 `default-src 'self'` 也不允许从外部加载。中文字体由 `cn-font-split` 切成 82 个 unicode-range 分片 × 3 个字重,浏览器按需加载,避免启动时读取完整的 14 MB 字体。
合到一句话里靠**逐字回退**,不做语言判断:`--font-ui` 把 JetBrains Mono 写在前面,它里面
没有汉字,浏览器就逐字往下找,落到 HarmonyOS。于是"血量 87"里数字是等宽的、汉字是
HarmonyOS 的,一条 `font-family` 同时满足两个要求。
混排依靠逐字回退,无需判断语言:`--font-ui` 将 JetBrains Mono 放在前面,遇到其中没有的汉字时,浏览器回退到 HarmonyOS。因此,“血量 87”中的数字使用等宽字体,汉字使用 HarmonyOS,同一条 `font-family` 即可处理两者。
启动页等待 `document.fonts.ready`,避免先显示系统字体、两百毫秒后切换到 HarmonyOS 引起布局重排。
### 队标
应用图标与启动页的标记是队标(`https://simba-robotics.com/logo.svg`),由它生成整套
`src-tauri/icons/`。原图是 1024×1024 的画布、标志只占中间一条,所以脚本把 viewBox 收到本体
上再居中;**小尺寸用更大的缩放比**(光学尺寸:256px 以上 82%、64–128px 88%、48px 以下 97%),
否则 16/24/32 上那些 1px 笔画会糊成一团。`icon.ico` 是手工拼的多尺寸容器——Pillow 的写入器
只能从一张主图缩出所有档位,而 Windows 任务栏恰恰只取最小的那几档。
应用图标和启动页标记均来自队标(`https://simba-robotics.com/logo.svg`),整套 `src-tauri/icons/` 由此生成。原图画布为 1024×1024,标志只占中间一条,因此脚本先将 viewBox 收到图形本体,再居中放置。深色底板采用圆角,四角透明。小尺寸使用更大的缩放比(光学尺寸:256px 以上 82%、64–128px 88%、48px 以下 97%),避免 16/24/32 图标中的 1px 笔画模糊。`icon.ico` 手工封装各尺寸图像;Pillow 的写入器只能从一张主图缩出所有档位,而 Windows 任务栏只使用其中最小的几个尺寸。
笔画在 17px 以下就只剩 1px,所以顶栏只放字标 `RMIC::2026`(`::` 是界面里唯一被染红的两个
字符),完整队标留给启动页的 30px。
图标小于 17px 时,笔画只剩 1px,因此顶栏只放字标 `RMIC::2026`(其中 `::` 是界面里唯一标红的两个字符),完整队标在启动页以 30px 显示。
图标的 PNG、ICO 与 ICNS,以及 Windows 安装程序的侧图和页眉图,由同一脚本生成。先在 `desktop/` 安装前端依赖,再准备 Python 3 与 Pillow:
```sh
python -m pip install Pillow
python scripts/generate-brand-assets.py
```
以上命令在 `desktop/` 执行。脚本使用已有 Tauri CLI 渲染 SVG,不新增应用依赖。图标源文件为 `src-tauri/icons/icon.svg`;安装图源文件为 `src-tauri/installer/header.svg` 和 `sidebar.svg`,采用 132×57 和 164×338 的布局,导出两倍分辨率的 24 位 BMP(264×114、328×676),另保留原尺寸 PNG 便于预览。侧栏使用 `installer/logo.svg` 中不带底板的原始队标;生成脚本按图片引用读取对应 SVG,不把它替换为应用图标。安装图采用 `AspectFitHeight` 等比缩放,侧栏比例适配简体中文字体的对话框单位,避免 Logo 被水平压缩。页眉画布收窄,为标题区域保留间隔,防止图片等比放大后遮住标题首字。安装图片沿用墨黑蓝底、白色队标和红色强调色。默认使用 Windows 系统字体;其他平台可用 `--sans-font` 指定支持中文的字体文件、`--mono-font` 指定等宽字体。
### 每页最顺手的那个操作
- 用户端通过滚轮缩放、拖动平移、双击复位,无需额外点击按钮。六秒没有输入或受击时,角落标签与代号淡出;出现输入或受击后恢复。
- **裁判端让控制台跟着鼠标走。** 按下左格,下方列表就只显示那一边的装甲板(再按一次回到
全部,键盘 `1` / `2` / `0` 同样)。中间那条竖线可以拖——哪一路更重要就把它拉宽,双击回到
等宽。画面与下方控制台之间的横线也可拖动,用来调整回放区域的高度。
- **设置是一块带预览的控制台。** 左侧竖向导航,右侧常驻一块实时预览;预览用的是画面里
同一个绘制函数(`lib/reticle.ts`),使预览与实际画面的绘制逻辑一致。预览血量滑杆用于查看低血阈值与脉动效果。
- 裁判端点击左侧画面后,下方列表只显示该侧的装甲板;再次点击恢复显示全部,键盘 `1` / `2` / `0` 也可切换。拖动中间的竖线可调整两路画面的宽度,双击恢复等宽。拖动画面与控制台之间的横线可调整回放区域高度。
- 设置页左侧为竖向导航,右侧常驻实时预览。预览与实际画面共用绘制函数(`lib/reticle.ts`),保持绘制逻辑一致;预览血量滑杆用于查看低血阈值与脉动效果。
## 版本与改动日志:算出来的,不是写出来的
@@ -153,9 +148,32 @@ pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --ci -- --locked
- 安装包:同一 release 目录下的 `bundle/nsis/*-setup.exe`。
- 核心测试:进入 `core/` 后运行 `cargo +stable test --locked --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]'`。
Windows 打包需从普通终端或中完整性及以上的 CI runner 启动。`tauri.windows.conf.json` 在构建开始和打包前运行 `scripts/prepare-windows-bundle.ps1`,将输出目录、Cargo 宿主构建脚本、主程序及已有 NSIS 安装包的完整性标签设为 Medium。低完整性进程会被明确拒绝;CI 上传前还会只读检查产物标签。
如果已有安装包启动时报 `Error writing temporary file. Make sure your temp folder is valid.`,可先在普通 PowerShell 中检查标签:
```powershell
# 在 desktop/ 中执行。低完整性标签的 SID 为 S-1-16-4096。
icacls .\src-tauri\target\x86_64-pc-windows-msvc\release\bundle\nsis\*-setup.exe
# 修复本机已有主程序、安装包与打包输出目录,无需重新编译。
.\scripts\prepare-windows-bundle.ps1
# 只读复查,要求安装包存在。
.\scripts\prepare-windows-bundle.ps1 -CheckOnly
```
自定义 Cargo 输出路径时,脚本通过 `cargo metadata` 获取目录。单独修复其他目录可传 `-OutputDir <目标的 release 目录>`;调试产物则传相应的 `debug` 目录。
[Windows 创建进程时会取用户与 exe 文件完整性级别的较低值](https://learn.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control#process-creation),所以带 Low 标签的 exe 无法写入普通 `%TEMP%`。NSIS 在执行安装钩子前就会检查临时文件目录,仅重定向构建机的 `TEMP` 或放宽目录 DACL 不能解决分发后的问题。修复脚本只修改构建输出的完整性标签,不修改用户临时目录权限;安装包仍按当前用户安装。修复标签后若仍报错,再检查实际临时目录是否存在且可写。
权限回归检查:打包下载过 NSIS 工具后,在 `desktop/` 执行 `./scripts/test-windows-bundle.ps1`。它在独立样例目录验证 Low 进程拒绝、文件标签修复及重复执行,并编译一个仅写临时文件后退出的 NSIS 程序;不安装应用。工具不在默认缓存时可传 `-NsisCompiler <makensis.exe 路径>`。
NSIS 安装流程使用简体中文,包括欢迎、安装位置、安装完成、覆盖安装、卸载与 WebView2 错误提示。`src-tauri/installer/hooks.nsh` 定义页面提示和安装日志,`SimpChinese.nsh` 定义 Tauri 安装器的补充翻译;`tauri.windows.conf.json` 引用这些脚本和主题图片。沿用 Tauri 默认安装模板和当前用户安装方式;首次启动的模式选择与裁判系统绑定仍在客户端内完成。
默认完成页提示客户端与裁判系统连接同一网络,不内置 Wi-Fi 凭据。现场安装包如需显示热点信息,可将 `src-tauri/installer/network.example.nsh` 复制为同目录的 `network.local.nsh`,填写后重新打包。该本地文件已被 Git 忽略;不要提交它,分发安装包时也需确认其中的热点信息允许分享。
视频录制另需可从 `PATH` 找到的 `ffmpeg`,也可以在配置的 `[recording] ffmpeg` 中指定路径;基础图传显示不依赖它。
设备发现使用板卡的 Hello 广播;客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口;定向续订可避免"能发现并绑定,但始终等待画面"的情况。队伍端的绑定和画面过滤仍然生效。
设备发现使用裁判系统的 Hello 广播。客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口,定向续订可避免“能发现并绑定,却始终等待画面”的情况。队伍端的绑定和画面过滤仍然生效。
### 演示模式:没有板子也能把整条链路跑起来
@@ -188,8 +206,7 @@ CAM_CONFIG=<路径> 等价于 --config <路径>
使用 `tauri dev` 时,应用参数需要通过两个 `--` 分隔符传递,如 `tauri dev -- -- --demo`。也可通过上面的环境变量设置。
**未知参数只记一条 debug 日志,不报错。** macOS 从访达双击 .app 时系统会塞进来一个
`-psn_0_123456`;把它当错误处理的表现是"双击图标什么都没发生"。
未知参数只记录一条 debug 日志,不报错。macOS 从访达双击 .app 时,系统会附加 `-psn_0_123456`;若将它视为错误,用户看到的现象就是双击图标后程序没有启动。
## 为什么换成 Tauri,代价是什么
@@ -232,7 +249,7 @@ CAM_CONFIG=<路径> 等价于 --config <路径>
## 防作弊:两道闸门,不是一个
对抗赛里两队都用这套系统看自己小车的图传,要防止对手开第二个窗口连到另一队的板子。
对抗赛中,两队各用这套系统查看本车图传,接收限制用于防止对手另开窗口连接另一队的板子。
限制在接收端执行。Hello 广播已包含由 MAC 派生的 `sender_id` 与 IP;CSS 只能隐藏显示,无法阻止接收与解码。
@@ -241,14 +258,13 @@ CAM_CONFIG=<路径> 等价于 --config <路径>
| `only_sender` | 帧的重组与投递 | 用户端**始终**是绑定的那一块 |
| `discovery_all` | 是否记录**其它**板子的 Hello | 只在密码解锁后的设置界面里打开 |
旧客户端共用一个开关控制发现和接收,改绑需要手动修改配置文件。现在设置界面可以列出候选摄像头,同时保持接收限制:其他板卡即使向本机端口推流或进入发现列表,其视频帧也不会进入重组器。
旧客户端共用一个开关控制发现和接收,改绑需要手动修改配置文件。现在设置界面可以列出候选摄像头,同时保持接收限制:其他裁判系统即使向本机端口推流或进入发现列表,其视频帧也不会进入重组器。
`only_sender` 只在两种情况下放开:裁判端(中立方),以及用户端还没绑定摄像头。debug 构建不会放开它。
### 遥测按编译模式门控
`TEAM_DIAGNOSTICS = cfg!(debug_assertions)`。用户端 release 下**不产出**帧率/丢帧这些
数字,`CameraStatsDto::build` 将相应字段置零或置空。门控由编译模式决定,避免依赖现场命令行开关。
`TEAM_DIAGNOSTICS = cfg!(debug_assertions)`。用户端 release 构建不产出帧率和丢帧数据,`CameraStatsDto::build` 将相应字段置零或置空。是否提供这些数据由编译模式决定,不依赖现场命令行开关。
### 这挡不住什么
@@ -261,7 +277,7 @@ Hello 广播仍包含 IP。固件会向任何发送 `Subscribe` 的订阅者推
- 密码用于限制现场改绑和退出程序。配置只保存 Argon2id 哈希;修改配置中的绑定后,界面仍要求解锁。
- 拥有本机配置和源码的人可以删除哈希或修改客户端,因此密码无法约束这种操作。限制无线订阅仍需固件侧配对机制。
所以它的定位是**防误触与防随手改**,不是密码学意义上的对抗。
密码的作用是防止误触和现场随手修改,不能提供密码学意义上的对抗保护。
实现上:
@@ -274,10 +290,9 @@ Hello 广播仍包含 IP。固件会向任何发送 `Subscribe` 的订阅者推
Rust 侧的配置修改命令会调用 `AppState::require_unlocked`。即使从控制台调用 `invoke("bind_camera")`,也需要通过这项检查。
客户端移除了旧版的 `--camera-id` 命令行覆盖,避免绕过密码改绑。临时更换板卡时,在设置中解锁后修改,或用 `--config` 指定另一份配置文件。
客户端移除了旧版的 `--camera-id` 命令行覆盖,避免绕过密码改绑。临时更换裁判系统时,在设置中解锁后修改,或用 `--config` 指定另一份配置文件。
忘了密码:删掉配置文件里的 `password` 一行(或整个文件)。旧客户端的配置会在首次启动时
导入并备份成 `.bak`。
忘记密码时,可删除配置文件中的 `password` 一行,或删除整个文件。旧客户端配置会在首次启动时导入,并备份为 `.bak`。
## 用户端 HUD
@@ -295,8 +310,7 @@ Rust 侧的配置修改命令会调用 `AppState::require_unlocked`。即使从
### 胜负横幅
一方归零后,图传中央显示结果:获胜方为“胜利”,落败方为“落败”,双方同时归零为“平局”。用词带上对手的名字,裁判端两格因此也能直接
看出是哪一边赢了。
一方血量归零后,图传中央显示结果:获胜方为“胜利”,落败方为“落败”,双方同时归零为“平局”。提示带上对手名字,便于在裁判端两路画面中判断胜负。
- 结果由 Rust 侧计算(`snapshot::resolve_outcomes`,有单测)。自己归零即可判负,即使对手掉线;判胜则要求对手归零且自己仍有血,避免在本队从未上报时误判获胜。
- 横幅持续显示到血量重置(长按 BOOT / 下一局开始),供截图、回放和现场核对。
@@ -306,7 +320,7 @@ Rust 侧的配置修改命令会调用 `AppState::require_unlocked`。即使从
瞄准器标记相机画面中的一个点,直接绘制在 canvas 上。图像、网格、瞄准器和伤害数字共用同一组变换,避免 DOM 与 canvas 分别计算位置而产生偏移。
尺寸与缩放的关系也是刻意分开的:
瞄准器的位置与大小采用不同的缩放规则:
| | 跟着缩放走吗 | 为什么 |
|---|---|---|
@@ -314,8 +328,7 @@ Rust 侧的配置修改命令会调用 `AppState::require_unlocked`。即使从
| 大小 | 否 | 像真瞄具一样,放大之后准星覆盖画面里更小的一块 |
| 网格 | 是 | 它标的是画面里的比例,用来估距离 |
**偏移校准存的是画面比例,不是像素。** 摄像头与枪管不同轴时要把瞄点挪到弹着点上,而像素
偏移在分辨率或窗口大小一变就全错了。校准可以拖着画面调,也可以直接填数值。
偏移校准保存画面比例,不保存像素偏移。摄像头与枪管不同轴时,需要将瞄点调整到弹着点;若保存像素偏移,分辨率或窗口尺寸变化后就会失准。校准既可通过拖动画面完成,也可直接输入数值。
### 命中反馈
@@ -325,11 +338,9 @@ Rust 侧的配置修改命令会调用 `AppState::require_unlocked`。即使从
- 血量增加 ⇒ 不是命中,是长按 BOOT 的重置;
- 第一次见到一块板子不产生命中——没有基线,此时 `hp = 0` 只是"已经没血了"。
按**板子**记基线而不是按 `(板子, index)`:主板一次广播四条、`hp` 相同,按四条各记一份
基线会让一次掉血被报成四次命中。
基线按板子记录,而非按 `(板子, index)` 记录。主板一次广播四条上报,`hp` 相同;若为每条分别记录基线,一次掉血就会产生四次命中反馈。
已知精度上限:两次读数之间发生两次伤害时只能看到净差。心跳 200 ms,所以这段窗口里的两次
打击会被合并成一次——对"闪一下红"足够,对判定不够,判定看的是界面上的绝对血量。
命中反馈只能反映两次读数之间的血量净差。心跳间隔为 200 ms,这段时间内的两次打击会合并为一次反馈,足以触发红色提示,但不足以单独用于判定;判定需看界面上的绝对血量。
反馈包括画面四周泛红和准星旁的伤害数字,中央画面保持原样。早期版本的数字曾遮住瞄点,因此移到了准星旁。
@@ -337,25 +348,19 @@ Rust 侧的配置修改命令会调用 `AppState::require_unlocked`。即使从
早期版本会在命中时晃动画面和瞄准器。该效果会干扰观察,也容易被误认为车体晃动,因此已移除。
`HitFeedbackConfig::shake` 这个字段留着但不再生效:配置文件是 `deny_unknown_fields` 的,
删字段会让所有写过 `shake = true` 的配置**解析失败**,而配置解析失败等于程序起不来。设置
面板里也没有这个开关了。
`HitFeedbackConfig::shake` 字段保留,但已不再生效。配置解析使用 `deny_unknown_fields`,删除该字段会让所有写有 `shake = true` 的配置解析失败,导致程序无法启动。设置面板也已移除这个开关。
## 裁判端
### 队名可以直接改
两格画面各自下面有一个队名输入框(`TeamNameEditor`),回车或点到别处生效,Esc 取消。
名字是**这一场比赛**的属性,所以它放在裁判盯着的地方,而不是设置面板深处的某一节;改完
立刻反映到血条、装甲板表与"等待图传"的占位文字上。
两路画面下方各有一个队名输入框(`TeamNameEditor`),按回车或点击其他位置生效,Esc 取消。队名随比赛变化,因此直接放在裁判查看画面的区域。修改会立即同步到血条、装甲板表和“等待图传”的占位文字中。
Rust 侧统一处理队名(`config::sanitize_team_name`,有单测):合并空白,按字符截断到 12 个字,留空时使用「队伍一 / 队伍二」。输入框也限制长度,避免提交后才截断;按字符处理可避免截断中文的字节序列。
`set_team_name(team, name)` 每次只修改一队。若提交整个队名数组,最多 200 ms 前的旧快照可能覆盖刚修改的另一队名称。命令仍经过配置解锁检查,避免新增不受检查的写配置入口;裁判端的 `Role::needs_password` 为假,现场编辑无需密码。
名字在快照里以 `teamNames`(两条,下标就是队伍号)推给界面,两边各自的 `TeamHealthDto.name`
也是从这里来的:界面各处自己拼 `team === 0 ? "队伍一" : "队伍二"` 会在改名之后留下一半旧名字,
而且不会报错。
快照通过 `teamNames` 向界面传递两条队名,下标对应队伍号;两侧的 `TeamHealthDto.name` 也来自这里。若界面各处自行拼接 `team === 0 ? "队伍一" : "队伍二"`,改名后部分位置仍会保留旧名字,且不会报错。
### 两格等宽
@@ -372,16 +377,15 @@ Rust 侧统一处理队名(`config::sanitize_team_name`,有单测):合
3. 掉线的槽位保留它的 id(显示"离线"),这样它回来时还在原位;
4. 没有候选时保持槽位为空,避免两格重复显示同一块板。
旧实现只排除当前槽位原有的 id。一队掉线后,其槽位可能选中另一队仍在线的板卡,导致两格重复显示。规则 2 修复了这一问题,并有对应测试。
旧实现只排除当前槽位原有的 id。一队掉线后,其槽位可能选中另一队仍在线的裁判系统,导致两格重复显示。规则 2 修复了这一问题,并有对应测试。
图传板按在线候选自动分配,装甲板按上报的 `team` 分配。若画面对应关系不符,或装甲板固件的队伍号烧反,可在设置中手动指定槽位。
### 装甲板发现列表
列表显示板卡数量、自报队伍、血量、距上次广播的时间、四路上报情况和槽位。裁判端未显示血量时,可据此区分板卡未上线、队伍号错误和上报不完整。
列表显示裁判系统数量、自报队伍、血量、距上次广播的时间、四路上报情况和槽位。裁判端未显示血量时,可据此区分裁判系统未上线、队伍号错误和上报不完整。
**这不需要改固件**:主板一直用 `encode_from(sender_id)` 把自己的 MAC 派生 id 打进帧头,
只是旧的解码路径把它丢了。现在 `ArmorHealth::decode_from` 把它交出来。
该功能不需要修改固件:主板原本就通过 `encode_from(sender_id)` 将 MAC 派生 id 写入帧头,但旧解码路径丢弃了这一字段。现在由 `ArmorHealth::decode_from` 将它交给上层。
### 截图与录制
@@ -396,7 +400,7 @@ Rust 侧统一处理队名(`config::sanitize_team_name`,有单测):合
## 配置
`~/.config/cam-desktop/config.toml`,`--config` 可改。**目录名沿用旧的 `cam-desktop`**(原因见开头)。要干净重来就删掉这个目录。
默认配置文件为 `~/.config/cam-desktop/config.toml`,可用 `--config` 指定其他位置。目录名沿用旧的 `cam-desktop`,原因见开头;删除整个目录可重置配置。
```toml
role = "team"
@@ -488,15 +492,9 @@ RUST_LOG=debug ./target/aarch64-apple-darwin/debug/cam-desktop | grep 'stream:'
## 已知限制与没做的
- **同一队有两块装甲板在广播时**,`health(team)` 是"最后写入者优先",四个格子可能混着两块
板子的数。真实比赛一队只有一块,所以这只在开发(演示 + 真板子)或烧错固件时才会遇到;
日志里会警告一次,消歧办法是绑定具体的板子 id(`health_of_board`)。
- **录制帧率**是固定值,不是可变帧率。源帧率变化大时回放速度会有偏差;这个数会写进日志与
DTO,所以至少能解释清楚。
- **`csp` 是一条收紧过的白名单**,但它是"够用"而不是"最小":`style-src` 带
`'unsafe-inline'`(React 的内联 `style` 属性需要它),`connect-src` 放开了整个
`ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端",
见"测试"一节最后一条。
- 同一队有两块装甲板广播时,`health(team)` 采用“最后写入者优先”,四格数据可能来自不同板子。真实比赛每队只有一块,因此该情况只在开发(演示 + 真板子)或烧错固件时出现。日志会警告一次,可通过绑定具体板子 id(`health_of_board`)消除歧义。
- 录制使用固定帧率,不使用可变帧率。源帧率变化较大时,回放速度会有偏差;采用的帧率写入日志与 DTO,便于排查。
- `csp` 使用收紧后的白名单,但还不是最小范围:`style-src` 带有 `'unsafe-inline'`,供 React 内联 `style` 属性使用;`connect-src` 允许整个 `ws://127.0.0.1:*`,因为端口在启动时随机分配,无法写死。通过前后端连接检查验证它是否可用,方法见“测试”一节最后一条。
- **证书与公证**没做,`.dmg` 只在本机可用。
- 前端已有解码队列测试;布局、实际 WebView 解码和画面显示仍需浏览器预览及真机运行验证。