将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。 - fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示 - test(client): 按文件名核对媒体类型,避免依赖时间戳排序 验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
535 lines
38 KiB
Markdown
535 lines
38 KiB
Markdown
# 比赛控制与直播提案
|
||
|
||
> 状态:待确认,尚未实现。保留自 2026-10-19 的界面重构草案;以下布局、状态、命令、快捷键与验收数字均为提议,不是当前能力或测试结果。
|
||
> 比赛判负、技术暂停、计时比分、直播页和双路同步回放仍需确认。当前客户端说明见[桌面端](../desktop/client.md),已实现的 USB 维护与固件包格式见[固件维护](../maintenance/firmware.md)。
|
||
> 原草案中的旧色板、字体方案、单文件 CSS 基线、烧录实现计划和排期已被当前实现取代,不再作为开发依据。
|
||
> 规则依据:[2026 校内赛参赛手册](https://docs.simba-robotics.com/zh-cn/competition/)、[对抗赛与奖项](https://docs.simba-robotics.com/zh-cn/competition/matches/)、[裁判系统](https://docs.simba-robotics.com/zh-cn/competition/referee-system/)。规则摘录沿用原稿,实施前需核对手册。
|
||
|
||
## 页面方案
|
||
|
||
### 应用外壳(新增 `AppShell`)
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ TopBar 44px cam-desktop · 裁判端 · 链路● │ 比赛条(裁判/直播) │ 工具 │
|
||
├──────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Stage:视频 + 画面内 HUD + 覆盖层(暂停/判定/校准) │
|
||
│ │
|
||
├──────────────────────────────────────────────────────────────────────┤
|
||
│ StatusBar 28px 链路 / 帧率 / 丢帧 / 提示 │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
三种外壳状态(`data-shell`):
|
||
|
||
| 状态 | 触发 | 行为 |
|
||
| --- | --- | --- |
|
||
| `normal` | 默认 | 全部 chrome 可见 |
|
||
| `immersive` | F11 | chrome 隐藏;指针移到上沿 8px 时 TopBar 150ms 淡入,3 s 后淡出;指针静止 2 s 隐藏光标 |
|
||
| `broadcast` | 视图切换 `Ctrl+Shift+B` | 只渲染 `BroadcastStage`;禁用设置/维护/发现列表;不渲染任何 IP/token |
|
||
|
||
结构落点:`App.tsx` 里现有的门禁分支(致命错误 / 等待快照 / 身份选择 / 设密码)不动,只在最后一段把 `TeamPage | RefereePage` 包进 `AppShell`,并新增 `BroadcastPage` 分支。
|
||
|
||
### 用户端 `TeamPage`(操作手视角)
|
||
|
||
使用场景:操作手位于布帘后,单手操作、不看键盘,单局 3 分钟。
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ 02:41 (计时) │ ← 仅比赛进行中出现
|
||
│ ┌────────────┐ ┌────────────┐│
|
||
│ │我方 ████ 76│ │100 ████ 对手││ ← 血条贴角,宽 36%
|
||
│ └────────────┘ └────────────┘│
|
||
│ │
|
||
│ + (准星)│
|
||
│ │
|
||
│ (命中时:上中"命中 -3" + 全屏淡红 620ms) │
|
||
└──────────────────────────────────────────────┘
|
||
│ 链路 · 28.6 fps · 丢 0.4% │ ← release 下只留"链路"
|
||
```
|
||
|
||
改动点:
|
||
|
||
1. 顶部加计时条(仅 `match.phase ∈ {running, paused}` 且本队已绑定);不显示比分细节,局间才显示"第 2 局 · 1:1"。
|
||
2. 底部状态行从"滚轮缩放 · 拖动平移…"改成首次提示:进入页面 5 s 后自动淡出,鼠标移动再出现(作为新交互提议,时长仍需确认)。
|
||
3. 新增 `PausedOverlay`:技术暂停时整屏 86% 遮罩 + "技术暂停 03:12 · 由裁判发起",血条/计时保留可见。
|
||
4. 新增 `JudgedOverlay`:判负/结束时显示"本局判负(设备故障)"或"胜利",与 `MatchBanner` 合并成一个组件(避免两套结论 UI 同时出现)。
|
||
5. 设置入口、录制入口不进主界面(现在 TopBar 里有"设置/固件维护",用户端误触风险高):改为 `Ctrl+K` 命令面板或长按品牌名 2 s。
|
||
6. 校准模式(现有 `calibrating`)保留,但提示从底部横幅改到画面内角标,避免遮挡。
|
||
|
||
### 裁判端 `RefereePage`
|
||
|
||
三区 + 一条比赛条:
|
||
|
||
```
|
||
┌ TopBar ────────────────────────────────────────────────────────────────┐
|
||
│ cam-desktop · 裁判端 · 链路● · 录制×2 [直播] [设置] │
|
||
├ MatchBar 52px ─────────────────────────────────────────────────────────┤
|
||
│ BO3 第2局 │ 02:41 │ 红 76 ╍╍╍╍ 100 蓝 │ ⏸ 技术暂停 │ ⚑ 判负 │ ⋯ 更多 │
|
||
├────────────────────────────────────────────────────────────────────────┤
|
||
│ ┌─ 红方(左)──────────────┐ ┌─ 蓝方(右)──────────────┐ │
|
||
│ │ video + HUD │ │ video + HUD │ │
|
||
│ │ [队名 可编辑] [fps] │ │ [队名 可编辑] [fps] │ │
|
||
│ └─────────────────────────┘ └──────────────────────────┘ │
|
||
├ Dock ─────────────────────────────────────────────────────────────────┤
|
||
│ [装甲板 8] [录制与回放 3] [事件 27] [连接诊断] [刷新] [⟳] │
|
||
│ …内容(表格/列表/日志)… │
|
||
└────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
改动点:
|
||
|
||
1. `MatchBar`(新,常驻,高 52px):局次、计时、比分、暂停、判负、更多菜单。这是裁判端最重要的新增组件;判定后整条切成"结果态"(红/蓝底 + 理由 + 撤销入口)。
|
||
2. `referee-stage` 保持 `flex: 1`,新增的 `MatchBar` 占固定高度;下方控制台采用可拖拽高度(提议默认 240px,最小 120,最大 45vh);加一行比赛条后,900px 高的窗口约剩 560px 用于画面。
|
||
3. `ArmorTable` 改为两队并列(左红右蓝),每行左侧加 5 格迷你血条(与 HUD 同源,便于核对);主装甲板加 `主` 徽标;离线行 `opacity .55`(保留);`reportsAllPlates=false` 时显示"仅报 2 块"——规则允许 2 或 4 块装甲板。
|
||
4. 新增 `EventPanel`(Dock 第 3 个页签):命中/暂停/判负/录制开始/结束的时间轴列表,可导出。
|
||
5. `MediaPanel` 保留全部功能,新增两个东西:按局分组(`matchId` 目录)与 "回放本局"(双路同步 + 事件定位)。
|
||
6. 队名编辑保留(现在是内联输入),但改成"点击铅笔 → 行内编辑 → Enter 保存 / Esc 取消",避免一直占一行高。
|
||
|
||
### 直播 `BroadcastPage`(新)
|
||
|
||
见下文直播方案。要点:一个 1920×1080 虚拟画布 + 4 个模板 + 安全区参考线 + 只读数据。
|
||
|
||
## 状态与交互
|
||
|
||
### 状态模型(前端)
|
||
|
||
```ts
|
||
interface AppStore {
|
||
snapshot: Snapshot | null;
|
||
connected: boolean;
|
||
lastHit: HitPulseRecord | null;
|
||
confirmQuit: number;
|
||
notes: string[];
|
||
channelError: string | null;
|
||
// 新增
|
||
ui: { shell: "normal" | "immersive" | "broadcast"; guides: boolean; idle: boolean };
|
||
events: MatchEventView[]; // 由 WireMsg::MatchEvent 累积(本地最多 500 条)
|
||
clockSkewMs: number; // serverNow - localNow,用于计时插值
|
||
}
|
||
```
|
||
|
||
事件传输:比赛事件走独立推送,与现有的 `hit` 同一条路(`server::WireMsg` 新增 `MatchEvent`),前端累积;`Snapshot.match` 只带最近 12 条用于重连后补齐。理由:快照 5 Hz × 200 条事件会让每份快照增加到几十 KB,事件单独推送可避免重复传输。
|
||
|
||
计时以后端为准:`Snapshot` 增加 `nowMs: u64`(后端 Unix 毫秒),前端用 `useServerClock` 每帧算 `serverNow = localNow + clockSkewMs`,再用 `match.remainingMs - (serverNow - snapshotAt)` 插值。`setInterval` 在窗口失焦时会被节流,不能作为比赛计时依据。
|
||
|
||
### 一键判负
|
||
|
||
入口:`MatchBar` 右侧 `⚑ 判负`,或 `Ctrl/Cmd+Shift+F`。只在裁判端、`phase ∈ {running, paused}` 时可用。
|
||
|
||
三步,不可合并
|
||
|
||
```
|
||
① 点按 → ForfeitDialog
|
||
┌─────────────────────────────────────────┐
|
||
│ 判定本局结果 │
|
||
│ 当前:红 76 · 蓝 100 | 剩余 02:41 | 第2局│
|
||
│ 败方: (●) 红方 ( ) 蓝方 │
|
||
│ 理由: [设备故障 ▾] │
|
||
│ 设备故障 / 违规 / 弃权 / 未到场 / 其他 │
|
||
│ 备注: [________________________] │
|
||
│ ⚠ 本操作只改变比赛判定与记录,不改变硬件血量 │
|
||
│ [取消] [长按 1 秒确认判负] │
|
||
└─────────────────────────────────────────┘
|
||
② 长按 1000ms(HoldButton,松手即取消,Esc 取消)
|
||
③ 5s 内 toast 提供「撤销」
|
||
```
|
||
|
||
执行语义
|
||
|
||
```rust
|
||
match_end { reason: Forfeit { loser: 0|1, kind: ForfeitKind, note: Option<String> } }
|
||
```
|
||
|
||
- 状态 → `Ended { by: Referee, reason, at, actor }`,比分按规则 +1 给胜方。
|
||
- 用户端若已同步(见 R2):显示 `JudgedOverlay`「本局判负」;未同步则只影响裁判端与直播。
|
||
- 不改硬件血量:`ArmorHealthStore` 只接收上报,没有任何写入口(见 R1)。界面必须显式写明这一点,否则现场会以为"判负之后对方血量归零了"。
|
||
- 审计:事件 `{ at, actor, action:"forfeit", loser, reason, note, hp:[76,100], remainingMs:161000 }`,追加到 `matches/<yyyy-mm-dd>/<matchId>/events.ndjson`,同时进 `snapshot.match.events`。
|
||
- 撤销:`match_undo { eventId }`,5 s 内可直接撤销;之后只能 `match_reopen_round { reason }`,在历史里留下"更正"两条记录(不删除原记录)。
|
||
|
||
与自动判定共存:`resolve_outcomes` 已经能在血量归零时给出 `Victory/Defeat/Draw`。两者必须用同一字段建模:
|
||
|
||
```ts
|
||
end: { by: "auto" | "referee"; reason: "knockout" | "timeout" | "forfeit" | "equipment" | "tiebreak"; loser?: 0|1 }
|
||
```
|
||
|
||
回放和统计据此区分结束原因。
|
||
|
||
### 技术暂停
|
||
|
||
规则对齐:手册写"任意一方…可向场边裁判申请发起时长为 5 分钟 的技术暂停并说明理由,待主裁判批准后技术暂停开始。技术暂停期间,双方均可在该段时间内检修机器人。"
|
||
|
||
四步流程
|
||
|
||
```
|
||
申请方(红/蓝) → 理由 → ✅ 主裁判已批准 → 开始计时(05:00) → 恢复
|
||
```
|
||
|
||
- 入口:`MatchBar` 的 `⏸ 技术暂停`。对话框必填:申请方、理由(枚举+备注)、"主裁判已批准"复选框(不勾不能开始——把规则写进交互里)。
|
||
- 开始后:`phase = Paused { kind: Tech, endsAt, applicant }`,比赛计时冻结,技术暂停独立倒计时 5:00。
|
||
- 用户端/直播:`PausedOverlay` 显示"技术暂停 · 剩余 04:12",不遮挡血条与计时。
|
||
- 结束:
|
||
- 手动「恢复比赛」→ 3-2-1 倒数 3 s(`Resuming` 过渡态)→ `Running`。
|
||
- 05:00 到点 → 状态 `PausedTimeout`,提示"技术暂停已超时",按钮变成「恢复」/「判负」二选一,不自动判负(规则没写超时即判负)。
|
||
- 录制不停(证据链需要),但写入 `bookmark` 事件;视频文件里保留暂停期间的画面。
|
||
- 暂停期间的命中:硬件照常上报,客户端如实显示并把事件标红(不隐藏、不改血量)。理由见 R1/Q3——客户端没有写硬件的能力,做"冻结基线"只会制造两份互相矛盾的血量。
|
||
|
||
### 计时 / 比分
|
||
|
||
两个不同的 3 分钟,界面必须分开写
|
||
|
||
| 阶段 | 时长 | 文案 |
|
||
| --- | --- | --- |
|
||
| 赛前准备 | 3:00 | `准备 02:41`(浅色,不计入比赛) |
|
||
| 单局比赛 | 3:00 | `比赛 02:41`(主色,进入最后 30 s 变红) |
|
||
| 技术暂停 | 5:00 | `技术暂停 04:12`(青色) |
|
||
|
||
状态机
|
||
|
||
```
|
||
Idle ──start_round──▶ Running ──pause──▶ Paused ──resume──▶ Running
|
||
▲ │ │
|
||
│ ├──hp=0 / timeout──▶ Ended(by=auto)
|
||
│ └──forfeit─────────▶ Ended(by=referee)
|
||
└──reset/next_round─────┴──────────────────┘
|
||
|
||
Ended(timeout 且血量相等) ──▶ AwaitingTiebreak ──set_tiebreak──▶ Ended(by=referee, reason=tiebreak)
|
||
```
|
||
|
||
到点判定(严格照手册四条):
|
||
|
||
1. 血量先减为 0 的一方落败 → 已有 `resolve_outcomes`,直接复用。
|
||
2. 3 分钟耗尽且无人归零 → 血量多者胜。
|
||
3. 双方血量均等 → 不以"平局"结束,进入 `AwaitingTiebreak`,由裁判选择"本局最快开始移动的一队"。
|
||
- 客户端没有运动遥测(协议里没有位置/速度),所以只能人工录入:`set_tiebreak { winner: 0|1 }`。
|
||
- 这条必须显眼提示裁判"需要人工判定",不能默认算平局,否则会静默改变比赛结果。
|
||
4. 两边同时归零 → `Draw`(现有 `MatchOutcome::Draw` 逻辑保留,仅用于显示)。
|
||
|
||
比分:`bestOf ∈ {1,3,5}`(对应手册 BO1/BO3/BO5),`score: [u8;2]`,`history: Vec<RoundResult>`。先到 `ceil(bestOf/2)` 局的一方赢下系列赛,界面显示 `2:0 · 系列赛结束`。
|
||
|
||
计时实现(后端为准):
|
||
|
||
```rust
|
||
pub struct MatchClock {
|
||
started: Option<Instant>,
|
||
frozen_elapsed: Duration, // 暂停累计
|
||
limit: Duration,
|
||
}
|
||
impl MatchClock {
|
||
fn remaining(&self) -> Duration;
|
||
fn pause(&mut self); fn resume(&mut self);
|
||
}
|
||
```
|
||
|
||
在 `AppState::tick()`(5 Hz)里推进;`Snapshot.match.remaining_ms` 为权威值;`Snapshot.now_ms` 用于前端插值。不允许前端 `setInterval` 自己减。
|
||
|
||
### 回放
|
||
|
||
入口:`Ended` 后结果卡 →「回放本局」;或 Dock 的「录制与回放 → 按局分组」。
|
||
|
||
布局
|
||
|
||
```
|
||
┌ 回放 · 2026-11-01 第2局 红 vs 蓝 ─────────────── [导出] [返回] ┐
|
||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||
│ │ 红方录屏 │ │ 蓝方录屏 │ │
|
||
│ └──────────────────────┘ └──────────────────────┘ │
|
||
│ ▶ ━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 01:12/03:00 │
|
||
│ ⚑ 00:00 开始 · ● 00:41 命中 · ⏸ 01:12 技术暂停 · ⚑ 02:33 判负 │
|
||
└────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- 两个 `<video>` 共用主时钟(以 A 为主,B 每秒校正一次,偏差 > 120 ms 才 `seek`,避免抖动)。
|
||
- 事件时间轴可点击跳转:`currentTime = (event.atMs - roundStartAtMs) / 1000`。
|
||
- 快捷键:`空格` 播放/暂停、`←/→` ±1 s、`Shift+←/→` ±5 s、`M` 打标记。
|
||
- 截图:现有 `api.screenshot(camera)` 只能截最新一帧,不能截回放帧。需要新增 `save_png { data: Vec<u8>, name: String }`(限制写入 `recordings_dir/shots`),前端用 `canvas.drawImage(video)` + `toBlob` 取帧。
|
||
- 录制文件命名建议:`<date>_<round>_<红队>_vs_<蓝队>_<cameraText>.mp4`;若比赛开始时自动开启录制,需要给 `Recorder::start` 增加一个 `label: Option<&str>`。
|
||
- 录制起停由 `match_start/end` 发出事件,是否自动则看用户配置(默认自动开、自动停;退出程序仍走现有 `stop_all` 收尾)。
|
||
|
||
---
|
||
|
||
## 直播 UI:安全区与包装层
|
||
|
||
### 基准画布与安全区
|
||
|
||
基准 1920×1080(16:9)。相机是 640×480(4:3),所以直播层必须自己决定是加信箱还是裁切,不能指望画面填满。
|
||
|
||
| 区域 | 距边距 | 1080p 像素 | 内容 |
|
||
| --- | --- | --- | --- |
|
||
| 画布 | — | 0 | 1920×1080 |
|
||
| Action safe(93%) | 3.5% | 左右 67px / 上下 38px | 视频、装饰边框 |
|
||
| Title safe(90%) | 5% | 左右 96px / 上下 54px | 比分、计时、队名、判定卡 |
|
||
| 可牺牲区(平台遮挡) | — | 底部 120px(y ≥ 960)、右侧 60px | 只放赞助条/次要字幕;比分、计时、判定卡一律不得进入 |
|
||
| 视频安全区(画面内 HUD) | 3.5% | 640×480 上约 22px 横 / 17px 纵 | 血条与命中提示条 |
|
||
|
||
> 若输出 720p,以上像素值 ×2/3;若必须 4:3 输出(1440×1080),安全区按宽高比各算一次,不要简单套 16:9 的数字。
|
||
|
||
### 分区与坐标(模板 A:双路对称,默认)
|
||
|
||
```
|
||
y=0 ┌──────────────────────────────────────────────────────────┐
|
||
│ [赛事名/校徽] ● LIVE BO3 第2局 │ 0–108 标题区
|
||
y=108 ├──────────────────────────────────────────────────────────┤
|
||
│ ┌────────────────┐ ┌────────────────┐ │
|
||
y=150 │ 红方 │ 视频 800×600 │ │ 视频 800×600 │ 蓝方 │ 150–750 媒体区
|
||
│ 队名 │ (640×480 放大)│ │ │ 队名 │ x:140–940 / 980–1780
|
||
y=750 ├──────────────────────────────────────────────────────────┤
|
||
│ 红 76 ━━━━━━ 02:41 ━━━━━━ 100 蓝 │ 770–930 比分/计时
|
||
y=930 ├──────────────────────────────────────────────────────────┤
|
||
│ [事件字幕] [赞助条] │ 940–1080 可牺牲区
|
||
y=960 │ - - - - - - - - - 平台遮挡区上沿(不放关键信息) - - - - - │
|
||
y=1080 └──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- 媒体区左右各留 140px:其中 100px 给队名,剩余给队伍色条(`--team-0/1` 4px 竖条)。
|
||
- 比分/计时带固定 y 770–930,底边 930 < 960,永远不进平台遮挡区;判定卡必须在 title safe 内(x 96–1824,y 54–1026)。
|
||
- 计时居中,字号 `--fs-clock`;比分 `--fs-score`。
|
||
- 4:3 → 800×600 是 1.25× 放大,VGA 源放大后仍可读;不要裁切(裁切会切掉装甲板或场地边线,判罚举证时没有依据)。
|
||
|
||
模板 B:主视角 + PIP(一方图传丢失时自动建议)
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ 标题条 │
|
||
│ ┌────────────────────────────────────────┐ │
|
||
│ │ 主画面 1600×900(居中偏上) │ │
|
||
│ └────────────────────────────────────────┘ │
|
||
│ ┌──────────┐ │
|
||
│ │ PIP 480×360│ ← 右下,避开平台遮挡区
|
||
│ 比分板 └──────────┘ │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
模板 C:待机/准备(`Idle/Ready`,或信号中断)
|
||
|
||
- 中央:赛事名 + 双方队名 + `准备 03:00`(大号)+ "信号恢复中"(画面缺失时显示)。
|
||
- 背景:`--bg-void` + 2% 噪点 + 细网格(`background-image: repeating-linear-gradient`,静态,不滚)。
|
||
- 永不显示纯黑屏(黑屏会被平台判为离线)。
|
||
|
||
模板 D:判定(`Ended`)
|
||
|
||
- 中央判定卡:`红方胜` / `本局判负`(`--fs-display`)+ 理由(设备故障/超出时间/血量归零)+ 比分。
|
||
- 卡背景 `rgba(8,8,12,.72)` + 1px 亮边 + 切角,不做全屏红闪。
|
||
|
||
### 包装层结构(z 序)
|
||
|
||
```
|
||
z0 BackgroundLayer 底色 + 静态网格 + 噪点
|
||
z1 MediaLayer 双路 VideoSurface(复用现有组件,crosshair=null)
|
||
z2 HudLayer 画面内血条(可选显示,直播默认显示)
|
||
z3 LowerLayer 比分板 / 计时 / 队名 / 事件字幕
|
||
z4 AlertLayer 暂停 / 判负 / 判定卡
|
||
z5 BrandLayer 赛事名、校徽、赞助、LIVE 徽标
|
||
z6 GuideLayer 安全区参考线(仅预览,输出时 display:none)
|
||
```
|
||
|
||
实现要点:
|
||
|
||
- `BroadcastStage` 用 1920×1080 的绝对坐标系 + `transform: scale(min(vw/1920, vh/1080))`,`transform-origin: top left`。这样所有设计稿坐标直接写进 CSS,无需换算为百分比。
|
||
- 参考线由 `GuideLayer` 画(`outline: 1px dashed`),受 `props.guides` 控制;全屏/输出时自动关闭。
|
||
- 视频源仍用 `VideoSurface`(`ViewTransform` 固定 `IDENTITY_VIEW`),只是容器换成 800×600 的固定框——不要为直播再写一套 canvas 绘制。
|
||
|
||
### 输出方式
|
||
|
||
| 方案 | 做法 | 评价 |
|
||
| --- | --- | --- |
|
||
| A(推荐) | 同一窗口切 `broadcast` 视图 + F11 全屏;OBS「窗口采集」+ 裁到 16:9 | 无需新增权限或进程,作为首选方案 |
|
||
| B | 第二个 `WebviewWindow`(`label: broadcast`,无边框、置顶) | 需要 `core:webview:allow-create-webview-window`;双窗口抢 GPU,留待后续实现 |
|
||
| C | NDI / 虚拟摄像头 | 明确不做(依赖重、跨平台成本高) |
|
||
|
||
实施时需检查:
|
||
|
||
1. WebView 背景:`transparent: true` 在 Windows WebView2 与 macOS WKWebView 行为不一致,直播场景下透明边缘会出黑边。结论:用不透明底色 `--bg-void` 铺满,不做透明。
|
||
2. 低帧画面 + 高帧包装:视频是 JPEG 序列,帧率取决于固件配置(`commands.rs` 的注释里记着"The sensor 实测 22 fps",`target/firmware/BUILD-INFO.json` 里 vga30 档实测 29.7 fps);无论哪个值都远低于包装层的 60 fps,动效会"飘"在画面上。所以包装层只做状态迁移动画,不做连续动画。
|
||
3. 纯黑编码:`#000` 在 H.264 里容易 banding,用 `#07080a` + 2% 噪点。
|
||
4. OBS 窗口采集会采到鼠标:全屏时 `cursor: none`(`immersive` 状态负责)。
|
||
5. 窗口标题:直播窗口标题里不要出现队名/端口等内部信息(`setTitle` 现在会写身份;broadcast 视图下改成 `cam-desktop — 直播`)。
|
||
6. 分辨率自适应:`transform: scale()` 后文字仍清晰(矢量),但如果 OBS 缩放窗口,字号会变成非整数像素 → 建议 OBS 采集时窗口保持 1920×1080 或 1280×720。
|
||
|
||
### 直播视图的安全边界(必须做)
|
||
|
||
- `broadcast` 视图不渲染:设置按钮、维护入口、`cameras[].addr`(IP)、`stream_info.url`(token)、密码相关 UI、装甲板 id 全文(只显示队名)。
|
||
- 所有管理命令在广播态禁用(不是隐藏,是 `disabled` + `aria-hidden`,防止误触与 Tab 聚焦)。
|
||
- 演示模式(`demo`)在直播里显示红色角标"演示画面",与现有 `tag--demo` 同一语义(现在只在 TopBar,直播态必须另有一处)。
|
||
- 断流时 `StandbyCard` 明确写"信号中断 · 正在重连",不显示上一帧冻结画面冒充直播(冻结画面会被观众当成"画面卡住")。
|
||
|
||
---
|
||
|
||
## 校内赛规则要点与判罚流程
|
||
|
||
### 规则事实(来自官方手册,不改写)
|
||
|
||
| 条目 | 规定 | 来源 |
|
||
| --- | --- | --- |
|
||
| 队伍 | 每队严格 3 人,每人限一队;16 队取得比赛资格 | [赛事概览](https://docs.simba-robotics.com/zh-cn/competition/) |
|
||
| 赛制 | BO1 / BO3 / BO5;16→8 双败 BO3,8→4 双败 BO3,总决赛 BO5 | [对抗赛与奖项](https://docs.simba-robotics.com/zh-cn/competition/matches/) |
|
||
| 红蓝方 | 每场编号小者为红方,编号大者为蓝方 | 同上 |
|
||
| 检录 | 尺寸 / 遥控运动 / 发射 / 裁判系统安装 / 裁判系统(含图传)运作;通过后不得再烧录或改硬件直至准备阶段 | 同上 |
|
||
| 赛前准备 | 3 分钟;双方把机器人放到与装甲板颜色相同的部署点;确认裁判系统与操作位图传客户端连接、遥控连接正常 | 同上 |
|
||
| 技术暂停 | 5 分钟;任意一方遇突发问题向场边裁判申请并说明理由,主裁判批准后开始;期间双方均可检修 | 同上 |
|
||
| 血量 | 初始 100;每发有效攻击扣 3 点 | 同上 |
|
||
| 单局时长 | 3 分钟 | 同上 |
|
||
| 胜负 | ①血量先归零者负 ②时间耗尽血量多者胜 ③双方血量均等则以本局最快开始移动的一队获胜 | 同上 |
|
||
| 装甲板 | 每车 2 或 4 块,其中一块为主装甲板;检测周期 100 ms;任一触发后全车 100 ms 静默期;供电 5 V ± 1 V;UART 可向主控发键鼠指令 | [裁判系统](https://docs.simba-robotics.com/zh-cn/competition/referee-system/) |
|
||
| 图传 | 1 个模块,回传第一人称画面到裁判系统客户端屏幕;供电 3.3 V(与装甲板 5 V 不同) | 同上 |
|
||
| 日程 | 10-25 适应性训练;11-01 决赛(9:00–11:00 / 14:00–18:00) | [赛事概览](https://docs.simba-robotics.com/zh-cn/competition/) |
|
||
|
||
### 对客户端的影响
|
||
|
||
1. 血量必须同时有数字和格子。100 点 / 5 格 = 20 点/格,而单发只扣 3 点——一格的粒度相当于 6.7 发。只给格子,操作手无法判断"再中两发就死"。→ 保留 `showNumbers` 默认开启,且数字 ≥ 18px。
|
||
2. 100 点 = 34 发(33 发后剩 1 点)。裁判端可以显示"已中 33 发"这类辅助读数(`100 - hp) / 3`),但不要把它当成权威值——静默期内的连击会被合并。
|
||
3. 100 ms 静默期决定命中动效下限:同一次攻击可能在 4 块板上各上报一次,`HealthTracker` 按血量差派生,已经能合并;UI 侧不要为每个 board 事件各弹一次伤害数字(否则一次命中会闪 4 下)。
|
||
4. 装甲板 2 块也要能显示:`ArmorBoardDto.reportsAllPlates` / `platesSeen` 已存在,界面在 `platesSeen < 4` 时显示"仅报 N 块",不要显示 4 个空槽让人以为板子坏了。
|
||
5. "最快开始移动"没有数据源。图传只给画面,协议里没有位移/速度;因此这条规则只能人工判定(见“计时 / 比分”)。不要试图从画面里推算。
|
||
6. 赛前 3 分钟是"确认客户端连上了"的窗口,所以用户端需要一个准备检查卡:图传在线、装甲板在线、帧率、最近一帧年龄、镜像是否正确。这张卡在 `Ready` 阶段自动出现,`Running` 后消失。
|
||
7. 红蓝配色固定:队号 0 = 红、1 = 蓝,与部署点/装甲板灯色一致(现有 `teamColor` 已是这个语义,只改色值)。
|
||
|
||
### 判罚流程(客户端可执行的部分)
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ 单局结束条件 │
|
||
└──────────────┬───────────────┘
|
||
┌───────────────────────────┼───────────────────────────┐
|
||
│ │ │
|
||
血量归零 时间耗尽 人工裁定
|
||
(自动,双方一致) (自动比血量) (裁判操作)
|
||
│ │ │
|
||
│ ┌──────┴──────┐ ┌─────────┼─────────┐
|
||
│ 血量不等 血量相等 设备故障 违规/弃权 平血仲裁
|
||
│ │ │ │ │ │
|
||
▼ ▼ ▼ ▼ ▼ ▼
|
||
Ended(by=auto, Ended(by=auto, AwaitingTiebreak ForfeitDialog(长按 1s + 理由)
|
||
reason=knockout) reason=timeout) → set_tiebreak → Ended(by=referee)
|
||
│
|
||
▼
|
||
Ended(by=referee, reason=tiebreak)
|
||
```
|
||
|
||
操作记录
|
||
|
||
```json
|
||
{
|
||
"id": "evt-0007",
|
||
"atMs": 1761998400000,
|
||
"actor": "referee",
|
||
"action": "forfeit",
|
||
"loser": 0,
|
||
"reason": "equipment",
|
||
"note": "图传模块断电",
|
||
"hp": [76, 100],
|
||
"remainingMs": 161000,
|
||
"round": 2,
|
||
"score": [1, 0]
|
||
}
|
||
```
|
||
|
||
- 落盘:`<app_data>/matches/<yyyy-mm-dd>/<matchId>/events.ndjson`(追加写,不覆盖),另有 `match.json` 存最终结果。
|
||
- 导出:`match_export` 把 `result.csv` 与 `events.ndjson` 放到录制目录,供组委会归档。
|
||
- 更正:`match_undo`(5 s 内)或 `match_reopen_round`(带理由);任何更正都新增记录,不删除原记录。
|
||
|
||
规则缺口(手册未定义,客户端不臆造)
|
||
|
||
| 缺口 | 客户端默认行为 | 需要谁定 |
|
||
| --- | --- | --- |
|
||
| 警告/红黄牌累计 | 不实现累计逻辑,只有"理由 + 备注" | 组委会 |
|
||
| 判负后是否重赛 | 只记录 `reason`,不自动重开 | 主裁判 |
|
||
| 平血"最快开始移动"的判定依据 | 人工选择,必填备注 | 主裁判 |
|
||
| 设备故障(图传中断)导致无法比赛 | 人工裁定:技术暂停 / 判负 / 重赛 | 主裁判 |
|
||
| 技术暂停超时(>5 分钟) | 提示超时,不自动判负 | 主裁判 |
|
||
|
||
> 手册 1.0.0 的更新日志提到"添加了详细的扣血、判罚规则与单局时间",但当前公开页面只给出了 上方规则表列出的内容(扣血 3 点、技术暂停 5 分钟、四条胜负条件)。如果组委会另有一份判罚细则(红黄牌、犯规等级),需要提供原文,否则界面不写死任何罚则。
|
||
|
||
---
|
||
|
||
## 风险与待确认事项
|
||
|
||
### 风险
|
||
|
||
| # | 风险 | 概率/影响 | 缓解 |
|
||
| --- | --- | --- | --- |
|
||
| R1 | 客户端判负 ≠ 硬件血量:协议只有上行(`MsgType` 1–5),没有下行控制,判负后对方装甲板仍报 100 | 高 / 高 | 界面明确写"只改变判定与记录,不改变硬件血量";不伪造 HP;若要硬件生效必须另做固件+协议(Q2) |
|
||
| R2 | 跨端不同步:裁判端判负/暂停,操作位用户端看不到 | 高 / 高 | v1 只作用于裁判端与直播;v2 用 `5601` 广播新 `MsgType`(见下)并加共享码签名;现场先用对讲机兜底 |
|
||
| R6 | 动效拖慢低配裁判机(双路 canvas + CSS 动画) | 中 / 中 | 只用 `transform/opacity`;禁连续动画;低动效开关;实测 30 min 不掉帧 |
|
||
| R7 | 录制与比赛状态机耦合(自动起停误伤手动录制) | 中 / 中 | 状态机只发事件;"随比赛自动录制"做成开关,默认开 |
|
||
| R8 | 直播泄密(IP/token/密码入口出现在画面里) | 中 / 高 | broadcast 视图白名单渲染;不渲染 `addr`/`url`;只读态;截图核对清单 |
|
||
| R9 | 规则变更/规则缺口(平血、超时、罚则) | 中 / 中 | 规则参数可配置(`MatchConfig`);缺省行为全部"人工裁定 + 记录" |
|
||
| R10 | 中文排版回归(字距、字重、字号) | 中 / 低 | 沿用"中文不加字距";只换 token 不动排版规则;逐页截图对比 |
|
||
| R11 | 光敏/无障碍(红闪、脉动) | 低 / 中 | ≤ 3 Hz、面积 < 25%、`prefers-reduced-motion` 降级、低动效开关 |
|
||
|
||
R2 的可选技术方案(若用户要跨端同步)
|
||
|
||
```rust
|
||
// protocol/src/lib.rs 新增(不改动已有枚举值)
|
||
pub enum MsgType { Video = 1, Subscribe = 2, Hello = 3, Heartbeat = 4, ArmorHealth = 5, MatchControl = 6 }
|
||
|
||
// 载荷:version(1) op(1) team(1) seq(4) issued_unix_ms(8) mac(16)
|
||
// op: 1=pause 2=resume 3=forfeit 4=reset 5=state-sync
|
||
```
|
||
|
||
- 裁判端以 `REFEREE_ID` 广播到 `5601`;用户端在装甲板监听里增加一个 `MsgType::MatchControl` 分支(不改相机订阅逻辑)。
|
||
- 安全:局域网里任何人都能发 UDP,所以必须带共享码派生 MAC(检录时裁判端生成 6 位码,双方在客户端输入),否则队伍可以伪造"对方判负"。
|
||
- 固件兼容性待验证:装甲板固件只认 `MsgType::ArmorHealth`,未知类型按现有 `decode_header` 逻辑返回 `None` 被忽略;但上线前必须在实机上验证"未知 MsgType 不会让板子异常"。
|
||
|
||
### 需要用户确认的问题
|
||
|
||
比赛语义
|
||
|
||
1. Q1 客户端"一键判负"要不要让用户端(操作位)也看到?如果需要,是否接受"局域网广播 + 共享码"的方案(R2)?
|
||
2. Q2 判负/技术暂停是否需要改变装甲板硬件血量(例如判负后立即把对方 HP 打到 0)?这需要改固件 + 协议,属于另一个工程。
|
||
3. Q3 技术暂停期间的命中:如实显示并标红(当前建议),还是冻结/忽略?谁来确定"暂停开始"的时间点——客户端按钮,还是场边裁判口头通知后由裁判端按下?
|
||
4. Q4 平血时"本局最快开始移动的一队"由谁录入?是否接受"裁判端手动点选一方 + 必填备注"?
|
||
5. Q5 是否需要 BO3/BO5 的系列赛比分与"第 N 局"编排?队伍名/编号从哪来(手工输入 vs 导入名单)?
|
||
|
||
直播
|
||
|
||
6. Q6 输出分辨率与帧率(1080p30 / 720p60?)、推流平台(B 站 / 抖音 / 校内大屏 / 录播)?安全区按哪个标准?
|
||
7. Q7 是否用 OBS 窗口采集(推荐,方案 A)?还是需要独立输出窗口 / 透明背景(方案 B)?
|
||
8. Q8 包装层需要哪些固定素材:赛事名、校徽、赞助商 LOGO、选手照片/队徽?由谁提供、什么格式?
|
||
|
||
录制与部署
|
||
|
||
- 录制是否随比赛状态自动起停?文件名是否包含双方队名与局次?
|
||
- 现场优先支持哪个平台?当前默认 Rust target 为 `aarch64-apple-darwin`;Windows 构建需要显式指定宿主 target。
|
||
|
||
## 附录 A:热键总表
|
||
|
||
| 键 | 范围 | 动作 | 备注 |
|
||
| --- | --- | --- | --- |
|
||
| `F11` / `⌃⌘F` / `Alt+Enter` | 全局 | 全屏切换 | 400 ms 去抖 |
|
||
| `Esc` | 全局 | 关最上层(对话框 → 抽屉 → 全屏) | 逐层关闭 |
|
||
| `Ctrl/Cmd+K` | 全局 | 命令面板(设置/维护/直播/回放) | 替代主界面按钮 |
|
||
| `Ctrl/Cmd+Shift+B` | 全局 | 切换直播视图 | 仅裁判端 |
|
||
| `Ctrl/Cmd+Enter` | 裁判端 | 开始本局 | `phase ∈ {Idle, Ready}` |
|
||
| `Ctrl/Cmd+Shift+P` | 裁判端 | 技术暂停 | 弹对话框 |
|
||
| `Ctrl/Cmd+Shift+F` | 裁判端 | 判负 | 仍需长按确认 |
|
||
| `Ctrl/Cmd+1` / `+2` | 裁判端 | 选中红/蓝(对话框内) | — |
|
||
| `Space` | 仅回放 | 播放/暂停 | 比赛中禁止 |
|
||
| `←/→`、`Shift+←/→` | 仅回放 | ±1 s / ±5 s | — |
|
||
| `M` | 仅回放 | 打标记 | 写入事件 |
|
||
| `Tab`(按住) | 全局 | 显示当前视图快捷键 | 半透明面板 |
|
||
| `Ctrl/Cmd+R` | 全局 | 刷新快照(现有 `refresh`) | — |
|
||
|
||
## 附录 B:验收标准(提议)
|
||
|
||
| 项 | 标准 | 验证方法 |
|
||
| --- | --- | --- |
|
||
| 全屏 | F11 切换 < 350 ms;切 10 次不卡死、不丢键 | 手工 |
|
||
| 计时 | 3 分钟误差 < 200 ms;切窗口/最小化不失速 | 与手机秒表对比 3 次 |
|
||
| 判负 | 必须长按 ≥ 1000 ms;< 1000 ms 不生效;撤销窗口 5 s | 自动化 + 手工 |
|
||
| 技术暂停 | 申请表单不勾"已批准"无法开始;5:00 倒计时准确 | 手工 |
|
||
| 直播 | 安全区内无越界元素;不出现 IP/token/设置入口 | 截图核对清单 |
|
||
| 动效 | 30 min 双路运行,主线程不掉帧(≥ 25 fps) | 性能面板 |
|
||
| 无障碍 | 关键文字对比 ≥ 4.5:1;`prefers-reduced-motion` 下无位移动画 | 对比度工具 + 开关 |
|
||
| 回归 | 视频/血量/装甲板/录制/维护五项原功能全部可用 | 回归清单 |
|
||
|
||
## 附录 C:范围
|
||
|
||
1. 不做网络 OTA / 设备自升级(与[固件维护](../maintenance/firmware.md) 的边界一致)。
|
||
2. 不做修改固件源码;打包只组装已有产物。
|
||
3. 不做从画面推算"移动先后"、命中位置、弹道等规则数据。
|
||
4. 不做直播的透明背景/虚拟摄像头/NDI。
|
||
5. 不做多语言(中文界面为唯一语言,符合约束)。
|
||
6. 不做用户账号/云端同步。
|