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
+19 -19
View File
@@ -29,7 +29,7 @@
| `immersive` | F11 | chrome 隐藏;指针移到上沿 8px 时 TopBar 150ms 淡入,3 s 后淡出;指针静止 2 s 隐藏光标 |
| `broadcast` | 视图切换 `Ctrl+Shift+B` | 只渲染 `BroadcastStage`;禁用设置/维护/发现列表;不渲染任何 IP/token |
结构落点:`App.tsx` 里现有的门禁分支(致命错误 / 等待快照 / 身份选择 / 设密码)不动,只在最后一段把 `TeamPage | RefereePage` 包进 `AppShell`,并新增 `BroadcastPage` 分支。
在 `App.tsx` 中保留现有门禁分支(致命错误 / 等待快照 / 身份选择 / 设密码),只在最后一段将 `TeamPage | RefereePage` 放入 `AppShell`,并新增 `BroadcastPage` 分支。
### 用户端 `TeamPage`(操作手视角)
@@ -60,7 +60,7 @@
### 裁判端 `RefereePage`
三区 + 一条比赛条:
裁判端分为三区,另设一条比赛条:
```
┌ TopBar ────────────────────────────────────────────────────────────────┐
@@ -84,7 +84,7 @@
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` 目录)与 "回放本局"(双路同步 + 事件定位)。
5. `MediaPanel` 保留全部功能,新增按局分组(`matchId` 目录)与“回放本局”(双路同步 + 事件定位)两项功能。
6. 队名编辑保留(现在是内联输入),但改成"点击铅笔 → 行内编辑 → Enter 保存 / Esc 取消",避免一直占一行高。
### 直播 `BroadcastPage`(新)
@@ -110,15 +110,15 @@ interface AppStore {
}
```
事件传输:比赛事件走独立推送,与现有的 `hit` 同一条路(`server::WireMsg` 新增 `MatchEvent`),前端累积;`Snapshot.match` 只带最近 12 条用于重连后补齐。理由:快照 5 Hz × 200 条事件会让每份快照增加到几十 KB,事件单独推送可避免重复传输。
比赛事件采用独立推送,与现有 `hit` 使用相同的传输方式:在 `server::WireMsg` 中新增 `MatchEvent`,由前端累积。`Snapshot.match` 仅携带最近 12 条,供重连后补齐。若按快照 5 Hz × 200 条事件传输,每份快照会增加到几十 KB;单独推送可避免重复传输。
计时以后端为准:`Snapshot` 增加 `nowMs: u64`(后端 Unix 毫秒),前端用 `useServerClock` 每帧算 `serverNow = localNow + clockSkewMs`,再用 `match.remainingMs - (serverNow - snapshotAt)` 插值。`setInterval` 在窗口失焦时会被节流,不能作为比赛计时依据。
计时以后端为准。在 `Snapshot` 中新增 `nowMs: u64`,表示后端 Unix 毫秒;前端通过 `useServerClock` 每帧计算 `serverNow = localNow + clockSkewMs`,再用 `match.remainingMs - (serverNow - snapshotAt)` 插值。`setInterval` 在窗口失焦时会被节流,不能作为比赛计时依据。
### 一键判负
入口:`MatchBar` 右侧 `⚑ 判负`,或 `Ctrl/Cmd+Shift+F`。只在裁判端、`phase ∈ {running, paused}` 时可用。
三步,不可合并
以下三步不可合并:
```
① 点按 → ForfeitDialog
@@ -144,7 +144,7 @@ match_end { reason: Forfeit { loser: 0|1, kind: ForfeitKind, note: Option<String
- 状态 → `Ended { by: Referee, reason, at, actor }`,比分按规则 +1 给胜方。
- 用户端若已同步(见 R2):显示 `JudgedOverlay`「本局判负」;未同步则只影响裁判端与直播。
- 不改硬件血量:`ArmorHealthStore` 只接收上报,没有任何写入口(见 R1)。界面必须显式写明这一点,否则现场会以为"判负之后对方血量归零了"。
- 不改硬件血量:`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 }`,在历史里留下"更正"两条记录(不删除原记录)。
@@ -177,7 +177,7 @@ end: { by: "auto" | "referee"; reason: "knockout" | "timeout" | "forfeit" | "equ
### 计时 / 比分
两个不同的 3 分钟,界面必须分开写
赛前准备与单局比赛各为 3 分钟,界面必须分别标注:
| 阶段 | 时长 | 文案 |
| --- | --- | --- |
@@ -253,7 +253,7 @@ impl MatchClock {
### 基准画布与安全区
基准 1920×1080(16:9)。相机是 640×480(4:3),所以直播层必须自己决定是加信箱还是裁切,不能指望画面填满。
基准画布为 1920×1080(16:9),相机画面为 640×480(4:3)。两者比例不同,直播层需明确选择留边或裁切,不能默认画面会填满画布。
| 区域 | 距边距 | 1080p 像素 | 内容 |
| --- | --- | --- | --- |
@@ -328,7 +328,7 @@ 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 绘制。
- 视频源继续使用 `VideoSurface`(`ViewTransform` 固定 `IDENTITY_VIEW`),将容器改为 800×600 的固定框,复用现有 canvas 绘制逻辑。
### 输出方式
@@ -341,7 +341,7 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
实施时需检查:
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,动效会"飘"在画面上。所以包装层只做状态迁移动画,不做连续动画。
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 — 直播`)。
@@ -350,9 +350,9 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
### 直播视图的安全边界(必须做)
- `broadcast` 视图不渲染:设置按钮、维护入口、`cameras[].addr`(IP)、`stream_info.url`(token)、密码相关 UI、装甲板 id 全文(只显示队名)。
- 所有管理命令在广播态禁用(不是隐藏,是 `disabled` + `aria-hidden`,防止误触与 Tab 聚焦)。
- 广播态下需禁用所有管理命令,使用 `disabled` + `aria-hidden` 防止误触与 Tab 聚焦,不能只隐藏入口。
- 演示模式(`demo`)在直播里显示红色角标"演示画面",与现有 `tag--demo` 同一语义(现在只在 TopBar,直播态必须另有一处)。
- 断流时 `StandbyCard` 明确写"信号中断 · 正在重连",不显示上一帧冻结画面冒充直播(冻结画面会被观众当成"画面卡住")。
- 断流时,`StandbyCard` 显示“信号中断 · 正在重连”,不显示上一帧冻结画面,以免观众将其误认为直播卡顿。
---
@@ -377,11 +377,11 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
### 对客户端的影响
1. 血量必须同时有数字和格子。100 点 / 5 格 = 20 点/格,而单发只扣 3 点——一格的粒度相当于 6.7 发。只给格子,操作手无法判断"再中两发就死"。→ 保留 `showNumbers` 默认开启,且数字 ≥ 18px。
2. 100 点 = 34 发(33 发后剩 1 点)。裁判端可以显示"已中 33 发"这类辅助读数(`100 - hp) / 3`),但不要把它当成权威值——静默期内的连击会被合并。
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. "最快开始移动"没有数据源。图传只给画面,协议里没有位移/速度;因此这条规则只能人工判定(见“计时 / 比分”)。不要试图从画面里推算。
5. 目前没有“最快开始移动”的数据源。图传只提供画面,协议不包含位移或速度,因此只能人工判定(见“计时 / 比分”),不从画面推算。
6. 赛前 3 分钟是"确认客户端连上了"的窗口,所以用户端需要一个准备检查卡:图传在线、装甲板在线、帧率、最近一帧年龄、镜像是否正确。这张卡在 `Ready` 阶段自动出现,`Running` 后消失。
7. 红蓝配色固定:队号 0 = 红、1 = 蓝,与部署点/装甲板灯色一致(现有 `teamColor` 已是这个语义,只改色值)。
@@ -439,7 +439,7 @@ z6 GuideLayer 安全区参考线(仅预览,输出时 display:none
| 设备故障(图传中断)导致无法比赛 | 人工裁定:技术暂停 / 判负 / 重赛 | 主裁判 |
| 技术暂停超时(>5 分钟) | 提示超时,不自动判负 | 主裁判 |
> 手册 1.0.0 的更新日志提到"添加了详细的扣血、判罚规则与单局时间",但当前公开页面只给出了 上方规则表列出的内容(扣血 3 点、技术暂停 5 分钟、四条胜负条件)。如果组委会另有一份判罚细则(红黄牌、犯规等级),需要提供原文,否则界面不写死任何罚则。
> 手册 1.0.0 的更新日志提到“添加了详细的扣血、判罚规则与单局时间”,但当前公开页面只提供上表所列内容(扣血 3 点、技术暂停 5 分钟、四条胜负条件)。若组委会另有红黄牌、犯规等级等判罚细则,需提供原文;在此之前,界面不写死罚则。
---
@@ -469,7 +469,7 @@ pub enum MsgType { Video = 1, Subscribe = 2, Hello = 3, Heartbeat = 4, ArmorHeal
```
- 裁判端以 `REFEREE_ID` 广播到 `5601`;用户端在装甲板监听里增加一个 `MsgType::MatchControl` 分支(不改相机订阅逻辑)。
- 安全:局域网里任何人都能发 UDP,所以必须带共享码派生 MAC(检录时裁判端生成 6 位码,双方在客户端输入),否则队伍可以伪造"对方判负"。
- 安全:局域网内任何人都能发送 UDP,因此消息必须携带共享码派生的 MAC。检录时由裁判端生成 6 位码,双方在客户端输入;否则队伍可以伪造对方判负消息。
- 固件兼容性待验证:装甲板固件只认 `MsgType::ArmorHealth`,未知类型按现有 `decode_header` 逻辑返回 `None` 被忽略;但上线前必须在实机上验证"未知 MsgType 不会让板子异常"。
### 需要用户确认的问题
@@ -527,7 +527,7 @@ pub enum MsgType { Video = 1, Subscribe = 2, Hello = 3, Heartbeat = 4, ArmorHeal
## 附录 C:范围
1. 不做网络 OTA / 设备自升级(与[固件维护](../maintenance/firmware.md) 的边界一致)。
2. 不做修改固件源码;打包只组装已有产物。
2. 不修改固件源码,打包只组装已有产物。
3. 不做从画面推算"移动先后"、命中位置、弹道等规则数据。
4. 不做直播的透明背景/虚拟摄像头/NDI。
5. 不做多语言(中文界面为唯一语言,符合约束)。
+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 解码和画面显示仍需浏览器预览及真机运行验证。
+9 -7
View File
@@ -28,6 +28,8 @@ espflash write-bin --chip esp32s3 --port COM5 0x0 target/firmware/esp32s3-merged
去掉导出命令的 `--merge --skip-padding` 可生成独立 App,它需写入 `partitions.csv` 声明的 Factory 偏移,并与板上 Bootloader 和分区表匹配。坏板重编可能改变该偏移;新板优先使用 ELF 或合并镜像。
需要交给桌面维护页的 ZIP 时,使用 `scripts/build.ps1 -Package`,或在构建后运行 `python scripts/package_firmware.py --firmware target/firmware`。Windows 仍需保留构建时的短目录 `CARGO_TARGET_DIR`。打包路径与旧镜像隔离的回归检查为 `python scripts/test-package-firmware.py`;它使用临时样例和模拟导出,不连接设备。
## 主机测试
协议层不依赖 ESP-IDF。进入 `protocol/`,以 Windows MSVC 为例:
@@ -40,7 +42,7 @@ Linux 常见 host 为 `x86_64-unknown-linux-gnu`,Apple Silicon 为 `aarch64-ap
装甲规则从 `armor/` 执行 `cargo +stable test -p armor-protocol`,桌面核心从 `desktop/core/` 执行 `cargo +stable test`,同样按需指定宿主 `--target` 和空的 `build-std`。
限帧、温度回差和 JPEG 压缩回压策略可以独立在主机运行,无需连接板卡。Windows 在根目录执行:
限帧、温度回差和 JPEG 压缩回压策略可在主机上独立测试,无需连接裁判系统。Windows 下在根目录执行:
```powershell
New-Item -ItemType Directory -Force target | Out-Null
@@ -64,8 +66,8 @@ cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys
## 发现与协议
- 图传板每 2 秒广播 Hello;客户端每 800 ms 广播 Subscribe,并向已发现的在线板卡 IP 单播续订,固定绑定 UDP 5600。
- 全局广播可能被 VPN / 虚拟网卡路由到其它接口。收到 Hello 后按设备 IP 定向续订,可以使用通往实际局域网的路由;队伍端仅向绑定的板卡增加单播续订。
- 图传板每 2 秒广播 Hello;客户端每 800 ms 广播 Subscribe,并向已发现的在线裁判系统 IP 单播续订,固定绑定 UDP 5600。
- 全局广播可能被 VPN / 虚拟网卡路由到其它接口。收到 Hello 后按设备 IP 定向续订,可以使用通往实际局域网的路由;队伍端仅向绑定的裁判系统增加单播续订。
- 固件使用订阅包的源地址作为发送目标,6 秒不续订即移除,最多保留 4 个对端。
- 新客户端为每个接收器生成随机会话 ID;同一源 IP 下,同一会话换端口会更新推流目标。旧版公共固定 ID `0x0C110001` 保持按地址区分,避免合并同机旧客户端。
- MAC 派生的 `sender_id` 不随 DHCP 地址变化,适合客户端持久绑定。
@@ -78,7 +80,7 @@ cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys
本次对照测试在保留 TUN 的环境中进行,最终参数与验证边界见 [性能排查](../testing/performance.md)。
下表为原 README 保存的**历史单次测试数据**,不是本次构建实测或性能保证。配置为 OV5640、VGA;原始日志、时长和完整网络条件未随这些数字保存。
下表保留原 README 中的历史单次测试数据,不代表本次构建的实测结果,也不构成性能保证。测试使用 OV5640、VGA;这些数字未附原始日志、时长和完整网络条件。
| 指标 | 早期 HTTP MJPEG / TCP | 后续 UDP |
| --- | --- | --- |
@@ -89,13 +91,13 @@ cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys
这些数据不足以单独证明性能差异的根因或无线链路容量。复测时记录板型、固件版本、分辨率、画质、信号、订阅者数量、时长和丢帧数。
FPS 应按**同一时间窗口的帧数增量 / 窗口秒数**计算。累计帧数与窗口时长混用会产生假数值,UI 轮询间隔也不代表相机出帧间隔。探测脚本的重组状态需按设备及帧隔离,避免测量错误。
FPS 应按同一时间窗口的帧数增量 / 窗口秒数计算。混用累计帧数与窗口时长会使结果失真,UI 轮询间隔也不代表相机出帧间隔。探测脚本需按设备和帧隔离重组状态,避免测量错误。
可重复测量:`python scripts/perf_probe.py <板卡IP> --seconds 30 --json perf.json`。默认使用独立的临时端口和 1 MiB 接收缓冲,每 800 ms 定向续订。脚本只依赖 Python 标准库;`--save-frame frame.jpg` 可保存首张完整 JPEG。公平比较应保持订阅者数量、分辨率、画质与网络条件一致。
可重复测量:`python scripts/perf_probe.py <裁判系统IP> --seconds 30 --json perf.json`。默认使用独立的临时端口和 1 MiB 接收缓冲,每 800 ms 定向续订。脚本只依赖 Python 标准库;`--save-frame frame.jpg` 可保存首张完整 JPEG。公平比较应保持订阅者数量、分辨率、画质与网络条件一致。
`frame_loss_percent` 包含不完整、超时及帧号缺口,不包含开始订阅前或测量结束后的帧。`assembly_p95_ms` 是首末分片到达的时间差,`inter_frame_p95_ms` 是完整帧到达间隔,均使用高精度计时;没有发送时间戳及两端时钟同步,不能称为端到端延迟。
固件日志将采集尝试与完整提交分开计数。`submitted` 只表示全部分片交给了网络,不代表无线确认或客户端显示;`send failures` 可能包含随后重试成功的调用,应结合 `peer frames dropped` 和接收端指标判断。
固件日志分别统计采集尝试和完整提交。`submitted` 只表示全部分片已交给网络,不代表收到无线确认或已在客户端显示。`send failures` 可能包含随后重试成功的调用,需结合 `peer frames dropped` 和接收端指标判断。
画面链路测量使用裁判端 `--latency-report <新文件>`,让相机拍摄校准页左侧标记,固定相机和窗口位置。标记经正常采集、网络与绘制路径回到客户端,源与回显共用主机单调时钟;不需要 ESP 时钟同步。测试期间只保留一个原生客户端,额外的本地 WebSocket 观察器不增加 UDP 订阅。方法、刷新估计边界及最新结果见 [延迟测量](../testing/latency.md)。
+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 引脚连接。升级驱动后需检查补丁并验证传感器探测和采集。
+4 -4
View File
@@ -1,6 +1,6 @@
# RMIC::2026 文档
从使用步骤进入,再按问题查阅硬件、实现和测试记录。代码命令默认从仓库根目录执行;需要切换目录的地方会注明。
首次使用可从入门文档开始,遇到问题时再查阅硬件、实现说明和测试记录。命令默认从仓库根目录执行;需要切换目录时会另行注明。
项目概览与架构图见 [README](../README.md),工具链、验证命令和仓库协作约定见 [AGENTS](../AGENTS.md)。
@@ -21,7 +21,7 @@
## 测试与排查记录
以下是特定配置、板卡和时间窗口的记录;不同轮次的数字不能直接视为同一配置的结果。
以下记录各自对应特定配置、裁判系统和观察时段,不能把不同轮次的数字当作同一配置下的结果。
- [最新帧与阻塞恢复](testing/realtime.md):发送期限、本地积压清理及五分钟观察。
- [功率、距离与拥塞](testing/power.md):15 dBm 回退及先前功率对照。
@@ -37,8 +37,8 @@
## 文档维护
本仓库参考 [Firefox 文档首页](https://github.com/mozilla-firefox/firefox/blob/main/docs/index.md)按入门、开发、源码、测试和发布分组,并用索引串联主题。这里统一使用 Markdown,不引入 Firefox 的整套文档构建工具。
本仓库参考 [Firefox 文档首页](https://github.com/mozilla-firefox/firefox/blob/main/docs/index.md),按入门、开发、源码、测试和发布分类,通过索引连接各主题。文档统一使用 Markdown,不引入 Firefox 的整套文档构建工具。
除 `README.md` 和 `AGENTS.md`(或 `Agents.md`)外,项目说明集中放在 `docs/`。根目录 README 介绍项目并链接到文档;子项目 README 保留简短入口。第三方组件的 README、许可证、构建文件、运行时配置和测试数据保留各自位置。
新增文档时先选择主题目录,再补充本页链接。迁移时同步修改相对链接、标题锚点与生成脚本。已被实现取代或已放弃的方案可以删除,历史仍可从 Git 查阅;仍需使用的实验记录保留条件、日期与验证范围,提案须明确标注状态。
新增文档时,先选择对应的主题目录,再补充本页链接。迁移文档时,同步修改相对链接、标题锚点与生成脚本。已被实现取代或已放弃的方案可以删除,历史仍可从 Git 查阅。保留实验记录时需写明条件、日期与验证范围,提案需标注状态。
+10 -13
View File
@@ -13,8 +13,7 @@
## 为什么坏块规避不需要重编译
App 镜像不包含自己的分区偏移;偏移只在 `0x8000` 的分区表里。所以同一份 App 可以
写到旧 App 之后的空闲槽位,只需要改分区表里的 factory 偏移,不需要重新构建。
App 镜像不包含自身的分区偏移,偏移保存在 `0x8000` 的分区表中。因此,同一份 App 可写入旧 App 之后的空闲槽位,只需修改分区表中的 factory 偏移,无需重新构建。
旧 `scripts/flash.sh` 之所以重编,是因为它把新偏移写回 `partitions.csv` 再重建。
客户端直接修改从设备读取的当前分区表;包内的 `partition-table.bin` 只作参考模板与校验:
@@ -50,8 +49,7 @@ App 镜像不包含自己的分区偏移;偏移只在 `0x8000` 的分区表里
- `0xFADE7`:bit 5–7 无法正确编程(已确认);
- `0x10863`:bit 6 读回为 1,尚未确认是否为永久故障,见[硬件说明](../hardware/camera.md#损坏-flash-模组的兼容处理)。
两个地址都在 `0x100000` 以下;`0x200000` 槽位天然避开它们。客户端仍会对写入的
App 整段回读,不依赖“已知坏点表”。
两个地址均低于 `0x100000`,因此 `0x200000` 槽位避开了它们。客户端仍会整段回读写入的 App,不依赖“已知坏点表”判断是否成功。
## 官方 .zip 固件包
@@ -104,7 +102,9 @@ BUILD-INFO.json # 可选;展示版本/构建信息
编译后打包:
```powershell
# 只打包(会先调用 make_bundle.py 刷新 bundle.json)
# Windows:使用与构建时相同的短目录。
$env:CARGO_TARGET_DIR = 'C:\s3vt'
# 只打包(从 ELF 导出镜像,再调用 make_bundle.py 刷新 bundle.json)
python scripts/package_firmware.py --firmware target/firmware
# 构建完成后自动打包
@@ -115,13 +115,13 @@ scripts/build.sh --package
产物:
```text
target/firmware/camfw-<chip>-<variant>-<rev>-<yyyymmdd>.zip
target/firmware/camfw-<chip>-v<version>-<build>.zip
target/firmware/camfw-...zip.sha256
```
zip 使用固定条目顺序、固定 mtime 与 deflate level 9,同一个输入应得到同一个整包
SHA256,便于核对分发的固件包是否一致。`package_firmware.py` 会复用
`make_bundle.py`,不维护第二套 manifest 逻辑。
zip 使用固定条目顺序、固定 mtime 和 deflate level 9。同一输入应生成相同的整包 SHA256,便于核对分发的固件包是否一致。`package_firmware.py` 复用 `make_bundle.py`,无需另维护一套 manifest 逻辑。
PowerShell 构建入口通过 Cargo 的产物报告传递 ELF 路径;单独运行 Python 打包时,通过 `cargo metadata` 读取输出目录,支持 `CARGO_TARGET_DIR` 和 Cargo 的 `build.target-dir`。也可用 `--elf <路径>` 显式指定 ELF。脚本从该 ELF 导出 App,复制同目录的 Bootloader 和分区表,并重写构建信息,避免沿用旧镜像或旧实机测试结论。缺少 ELF、配套镜像或导出工具时默认停止;仅在明确需要复用现有目录包时使用 `--allow-stale`。版本默认读取根目录 `version.json`。切回桌面端构建前执行 `Remove-Item Env:CARGO_TARGET_DIR`。
> 注意:bundle 里的 `partition-table.bin` 是参考模板;客户端实际写的是设备当前
> 分区表的 patch 版本,以保留设备的 NVS/PHY 布局。bundle 的分区表 SHA 仍会校验。
@@ -219,10 +219,7 @@ Secure Boot / Flash Encryption 均未启用)上验证:
- 离线单元测试:分区表解析、MD5 重算、候选槽位、尺寸解析、zip 路径白名单,共 7 个;
- 前端 `tsc --noEmit`、`vite build`,后端 `cargo check` 通过。
**尚未执行**完整实机切换(写 patch 后分区表 → 复位 → 启动日志)。App 写入/回读
已经在同一块板上验证;切换步骤的风险集中在 0x8000 的 3 KiB 分区表写入,写入后会
回读,失败会尝试恢复旧表。如果新 App 启动失败,页面会给出
备份目录和 `恢复旧分区表` 按钮。
完整实机切换(写 patch 后分区表 → 复位 → 启动日志)尚未执行;App 写入和回读已在同一块板上验证。切换风险集中在 0x8000 处的 3 KiB 分区表写入,写入后会回读校验,失败时尝试恢复旧表。若新 App 启动失败,页面会显示备份目录和 `恢复旧分区表` 按钮。
## 边界
+12 -24
View File
@@ -8,7 +8,7 @@ protocol/ 线路格式 + 帧重组 (23 个单元测试,无 ESP-IDF 依
desktop/ 桌面客户端 (RMIC::2026,Tauri v2 + React + Rust,两种身份)
```
编码与帧重组出错时,板上通常只表现为没有画面,难以定位。因此这里提供不依赖板卡的主机测试。
编码与帧重组出错时,板上通常只表现为没有画面,难以定位。因此这里提供不依赖裁判系统的主机测试。
在宿主架构上运行测试:
@@ -38,8 +38,7 @@ cd protocol && cargo test
└───────────────┘ └───────────────┘ └───────────────┘
```
两块板子不区分队伍——它们只知道自己是谁(MAC 派生的 id),"哪一边"由客户端决定。
烧同一份固件即可,配置在客户端一侧。
两块板子不区分队伍,只通过 MAC 派生的 id 标识自身,所属队伍由客户端配置决定。因此,两块板子可以烧录同一份固件。
## 线路格式
@@ -75,20 +74,18 @@ cd protocol && cargo test
### 四个设计决定
**27 字节不凑整。** 头只占每个 1427 字节数据报的 1.9%,而为对齐在固件和客户端两侧
各写一段手写偏移常量,更容易引入静默错位。
头长保留为 27 字节,不额外补齐。它只占每个 1427 字节数据报的 1.9%;为对齐而在固件和客户端分别维护手写偏移常量,反而更容易引入不易察觉的错位。
**`sender_id` 是多路的前提。** 裁判终端同时收两块板子,靠它在同一端口上区分来源。
**`width`/`height` 每个分片都重复携带**(不只是第 0 个),这样每个数据报都能独立
解析,中途加入的接收端也能立刻知道分辨率。
**`version` 不匹配就拒收**,而不是按错误布局继续解析——后者会表现为随机崩溃或画面
错乱,比拒绝连接难查得多。
`version` 不匹配时直接拒收。继续按错误布局解析会导致随机崩溃或画面错乱,比拒绝连接更难排查。
## 通告里的版本信息
`Hello` / `Heartbeat` 的负载是**向后兼容的一截**,现在 10 字节:
`Hello` / `Heartbeat` 的负载保留向后兼容的字段,目前共 10 字节:
```text
相对 HEADER_LEN 的偏移
@@ -102,15 +99,13 @@ cd protocol && cargo test
三个决定:
- **旧的 2 字节 `fw` 保留不动**。老客户端只读它,多余的字节不看,所以一次固件更新
不会把还在用旧客户端的机器踢下线。
- **新客户端接受只有 2 字节的老固件**:版本与构建号回到 0("未知"),协议号退回帧头里
的那个。界面显示"版本未知",而不是一个看起来像真值的 `0.0.0`。
- 保留旧的 2 字节 `fw` 字段。老客户端只读取该字段并忽略后续字节,因此更新固件不会使旧客户端因新增字段而断连。
- 新客户端接受仅含 2 字节负载的老固件通告:版本与构建号设为 0(“未知”),协议号使用帧头中的值。界面显示“版本未知”,避免将 `0.0.0` 当作实际版本。
- **通告用宽松的帧头解析,视频帧不用**。见下一条。
### 为什么通告允许协议不匹配
严格解析会丢弃 `version != PROTOCOL_VERSION` 的数据报,以避免按错误布局解析视频。若通告也被丢弃,客户端便无法发现协议不同的板卡,也无法解释没有画面的原因。
严格解析会丢弃 `version != PROTOCOL_VERSION` 的数据报,以避免按错误布局解析视频。若通告也被丢弃,客户端便无法发现协议不同的裁判系统,也无法解释没有画面的原因。
`Hello::decode` 使用 `decode_header_lenient`,保留魔数、长度和字段校验,允许版本不匹配。读出 `proto` 后,客户端可以显示双方协议号,并提示重新烧录。
@@ -138,18 +133,14 @@ UDP 不保证数据报的顺序和到达。接收端等待下一张完整帧,
### 按发送方分开
**每个发送方必须有自己的重组器。** 两块板子的 `frame_id` 各自独立计数,塞进同一个
重组器会让它们互相顶掉,两路都出不来。
每个发送方必须使用独立的重组器。两块板子的 `frame_id` 分别计数,若共用重组器,两路数据会互相覆盖,导致两路都无法交付画面。
### 重复分片必须去重
`FrameAssembler` 按分片下标记录是否收到,重复的直接跳过且不计数,并显式确认最后一个
分片确实到过。
用单一计数 `seen >= expected` 判断收齐不够健壮:一个丢失的分片若能被某个分片的副本
凑数,计数就达标了,而缓冲区里留着空洞,交出去一张尾部是 0 的 JPEG。两个测试专门覆盖
它(`a_duplicate_cannot_complete_a_frame_with_a_hole`、
`a_gap_that_is_never_filled_never_yields_a_frame`)。
仅用计数 `seen >= expected` 判断是否收齐并不可靠:重复分片可能补足计数,但丢失位置仍为空,最终交付的 JPEG 尾部会留下 0。两个测试覆盖这一情况(`a_duplicate_cannot_complete_a_frame_with_a_hole`、`a_gap_that_is_never_filled_never_yields_a_frame`)。
一次历史测试使用一到两个订阅者,20 秒收到 2000 多个分片;固定端口与临时端口两种方式均未观察到重复投递。去重用于保证重组正确性。重复片若被误计数,可能在帧率正常、没有报错时偶发地产生无法解码的 JPEG。
@@ -199,8 +190,7 @@ UDP 不保证数据报的顺序和到达。接收端等待下一张完整帧,
`id_from_mac()` 用固定长度输入的 FNV-1a 从 MAC 派生 `sender_id`。
**用 MAC 而不是 IP**,因为客户端要把绑定存进配置文件并在重启后继续有效——DHCP 换
地址是常态,MAC 不会变。用 MAC 也意味着烧同一份固件即可,不需要任何"我是谁"的配置。
使用 MAC 而非 IP,是为了让保存在配置文件中的绑定在重启后继续有效:DHCP 地址会变,MAC 不变。两块板子因此可以烧录同一份固件,无需另配设备身份。
返回值避开裁判终端保留值 0。测试检查结果稳定性与样例间是否碰撞,但不能证明哈希永不碰撞。
@@ -208,9 +198,7 @@ UDP 不保证数据报的顺序和到达。接收端等待下一张完整帧,
`scripts/udp_probe.py` 手写了这里的每一个常量,**不依赖本 crate**。
这正是它的价值:如果本 crate 的编码有错,脚本仍按文档解析,会立刻报出不一致。反面
例子也有——它的 `role` 字节曾多填 12 个零字节,固件按偏移严格读取因而拒收,表现为
"板子在线但 0 clients subscribed"。改协议时记得同步它。
如果本 crate 编码有误,脚本仍按文档独立解析,可立即报出不一致。探测脚本本身也曾出错:`role` 字节曾多填 12 个零字节,固件按严格偏移读取后拒收,表现为“板子在线但 0 clients subscribed”。修改协议时需同步核对脚本。
## 装甲板
+1 -1
View File
@@ -41,7 +41,7 @@
- 对临时 UDP 发送拥塞进行有时限的让出调度重试,统计实际提交帧、失败次数与丢弃帧。
- Windows UDP 接收缓冲请求增至 1 MiB;界面视频通道和解码队列保留最新待处理帧,避免慢端持续积压。
- 新客户端使用随机会话 ID;固件更新同一会话的源端口,避免 NAT / TUN 端口变化产生重复推流目标,旧固定 ID 客户端继续按地址区分。
- 新增可选编译参数 `WIFI_TX_POWER_DBM`(2–20 dBm);未设置时保留 SDK 默认上限,按实际板卡和网络测量调节。
- 新增可选编译参数 `WIFI_TX_POWER_DBM`(2–20 dBm);未设置时保留 SDK 默认上限,按实际裁判系统和网络测量调节。
- 新增 `scripts/perf_probe.py`,测量完整帧率、帧损失、接收间隔和分片组装耗时。
- 新增 `CAM_MAX_FPS`(默认 0,不限帧),可限制取帧和发送节拍,过载后不追赶补发。
- 新增 `CAM_GRAYSCALE`,通过传感器灰度效果保留硬件 JPEG;增加可配置的芯片温度减载和 5°C 恢复回差。
+45 -3
View File
@@ -3,13 +3,38 @@
"major": 0,
"minor": 2,
"patch": 2,
"build": 38,
"build": 39,
"protocol": 1,
"date": "2026-10-19",
"date": "2026-10-21",
"baseline": "v0.2.1",
"commits": 3,
"commits": 4,
"groups": {
"fix": [
{
"scope": "packaging",
"text": "修复 Windows 安装与固件打包并完善客户端文案",
"breaking": false
},
{
"scope": "installer",
"text": "修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签",
"breaking": false
},
{
"scope": "firmware",
"text": "从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序",
"breaking": false
},
{
"scope": "installer",
"text": "修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo",
"breaking": false
},
{
"scope": "config",
"text": "安装提示改用可选本地热点配置,排除 Wi-Fi 凭据",
"breaking": false
},
{
"scope": "client",
"text": "统一状态与操作文案,录制和截图成功后再显示完成提示",
@@ -17,6 +42,11 @@
}
],
"docs": [
{
"scope": null,
"text": "润色项目说明并统一裁判系统术语",
"breaking": false
},
{
"scope": null,
"text": "整理项目文档并统一界面表述",
@@ -34,11 +64,23 @@
}
],
"test": [
{
"scope": "packaging",
"text": "补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成",
"breaking": false
},
{
"scope": "client",
"text": "按文件名核对媒体类型,避免依赖时间戳排序",
"breaking": false
}
],
"style": [
{
"scope": "client",
"text": "应用图标增加圆角,统一“裁判系统客户端”命名",
"breaking": false
}
]
}
}
+13 -2
View File
@@ -4,24 +4,35 @@
> 提交采用 Conventional Commits,规则见[版本维护](../development/versioning.md)。
> 自动版本机制建立前的说明见[早期人工记录](changelog-legacy.md)。
## v0.2.2 — 2026-10-19
## v0.2.2 — 2026-10-21
构建号 **38** · 线路协议 **1** · 3 个提交(v0.2.1 → v0.2.2)
构建号 **39** · 线路协议 **1** · 4 个提交(v0.2.1 → v0.2.2)
### 修复
- **packaging** · 修复 Windows 安装与固件打包并完善客户端文案
- **installer** · 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- **firmware** · 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- **installer** · 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- **config** · 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- **client** · 统一状态与操作文案,录制和截图成功后再显示完成提示
### 文档
- 润色项目说明并统一裁判系统术语
- 整理项目文档并统一界面表述
- README 不写死构建号,改由 version.json 提供
- 按 humanizer 重写项目文档并同步 RMIC::2026 现状
### 测试
- **packaging** · 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成
- **client** · 按文件名核对媒体类型,避免依赖时间戳排序
### 样式
- **client** · 应用图标增加圆角,统一“裁判系统客户端”命名
## v0.2.1 — 2026-10-19
1 个提交
+5 -4
View File
@@ -12,7 +12,7 @@
| 网络 | 2.4 GHz Wi-Fi |
| 连接 | 支持数据传输的 USB 线、可识别的串口及稳定供电 |
默认采集 VGA(640 × 480)、彩色 JPEG quality 12,按传感器节拍发送。相机引脚、Flash 分区和 PSRAM 配置见 [图传板硬件](../hardware/camera.md)。换板型前必须核对接线,避免占用 Flash / PSRAM 引脚。
默认采集 VGA(640 × 480)彩色 JPEG,quality 为 12,按传感器节拍发送。相机引脚、Flash 分区和 PSRAM 配置见 [图传板硬件](../hardware/camera.md)。更换板型前必须核对接线,避免占用 Flash / PSRAM 引脚。
## 固件开发环境
@@ -151,6 +151,7 @@ WIFI_PASS='你的Wi-Fi密码'
# Windows PowerShell
$env:CARGO_TARGET_DIR = 'C:\s3vt'
.\scripts\build.ps1
# 同时导出并打包:.\scripts\build.ps1 -Package
```
```bash
@@ -158,9 +159,9 @@ $env:CARGO_TARGET_DIR = 'C:\s3vt'
./scripts/build.sh
```
脚本执行 `cargo build --release --locked`,目标为 `xtensa-esp32s3-espidf`。首次编译包含 Rust 标准库、ESP-IDF 和相机组件,耗时较长。构建完成后可用 `-Package` / `--package` 再打一个 `.zip` 固件包,供桌面端维护页使用。
脚本执行 `cargo build --release --locked`,目标为 `xtensa-esp32s3-espidf`。首次编译包含 Rust 标准库、ESP-IDF 和相机组件,耗时较长。构建完成后,可用 `-Package` / `--package` 生成 `.zip` 固件包,供桌面端维护页使用。
Windows 的 ESP-IDF 构建输出路径需不超过 88 字符,因此示例使用短目录 `C:\s3vt`(需可写,且不与其他项目共用)。`subst` 不能绕过该检查。下表的 `<TARGET_DIR>` 在 Windows 示例中为 `C:/s3vt`,Linux / macOS 默认是仓库的 `target`。每次新开 Windows 终端都要设置相同的 `CARGO_TARGET_DIR`。
Windows 的 ESP-IDF 构建输出路径需不超过 88 字符,因此示例使用短目录 `C:\s3vt`(需可写,且不与其他项目共用)。`subst` 不能绕过该检查。下表的 `<TARGET_DIR>` 在 Windows 示例中为 `C:/s3vt`,Linux / macOS 默认是仓库的 `target`。每次新开 Windows 终端都要设置相同的 `CARGO_TARGET_DIR`;切回桌面端构建前执行 `Remove-Item Env:CARGO_TARGET_DIR`。`-Package` 从 Cargo 本次报告的 ELF 路径导出 App,并复制同目录的 Bootloader 和分区表,ZIP 仍写入仓库的 `target/firmware/`。
| 产物 | 位置 |
| --- | --- |
@@ -207,4 +208,4 @@ espflash monitor --port COM5
`espflash save-image --merge` 可导出从 `0x0` 写入的合并镜像。完整命令、独立镜像偏移与测试方法见 [开发与验证](../development/firmware.md)。
保留的 `scripts/flash.sh` 针对特定坏 Flash 模组,依赖 macOS 环境,会修改分区并重编译。普通板卡使用上述标准流程;坏板限制见 [图传板硬件](../hardware/camera.md#损坏-flash-模组的兼容处理)。
保留的 `scripts/flash.sh` 针对特定坏 Flash 模组,依赖 macOS 环境,会修改分区并重编译。普通裁判系统使用上述标准流程;坏板限制见 [图传板硬件](../hardware/camera.md#损坏-flash-模组的兼容处理)。
+3 -3
View File
@@ -1,6 +1,6 @@
# 约 30 fps、Wi-Fi 重连与画面链路延迟验证(2026-10-18)
当前交付已增加阻塞恢复与新鲜度处理,见 [最新帧处理](realtime.md)。本页保留此前减光确认和画面延迟对照;所列 SHA 与测量结果均属于当时构建,不是当前固件的重测结果。
当前交付已增加阻塞恢复与新鲜度处理,见 [最新帧处理](realtime.md)。本页保留此前的亮度确认与画面延迟对照;所列 SHA 和测量结果均对应当时的构建,不代表当前固件重新测量后的结果。
## 先前三缓冲与小幅减光记录
@@ -31,7 +31,7 @@
| 标记 → 首次可解码画布回显 P95 | 230.7 ms | 193.4 ms |
| 回显估计 P99 / 最大值 | 285.3 / 443.2 ms | 248.9 / 461.5 ms |
这次窗口的 P50 降低 6.8 ms、P95 降低 37.3 ms。双缓冲减少潜在采集排队,同时保住接近 30 fps 的实际交付。两个窗口的网络回压、JPEG 自适应档位和时长不同,属于连续实机对照,不能把全部改善归因于缓冲数,也不是所有路由器或场景的保证。此前约 22 fps 的测试保留在 [持续运行记录](stability.md)。
这次对照窗口中,P50 降低 6.8 ms,P95 降低 37.3 ms。双缓冲减少了潜在的采集排队,实际交付仍接近 30 fps。不过,两个窗口的网络回压、JPEG 自适应档位和时长不同,只能作为连续实机对照,不能把全部改善归因于缓冲数,也不能据此保证所有路由器或场景的表现。此前约 22 fps 的测试见 [持续运行记录](stability.md)。
双缓冲试验五分钟全程在线,复位、采集错误、通道断开和计数重置均为 0 次。抽取 61 张 JPEG 均完整解码为 640×480 RGB;没有用灰度或降低色深换帧率。最小内部空闲堆 118771 B;实际 JPEG 档位 18–28,温度 58–60°C,未触发减载。
@@ -77,7 +77,7 @@ Windows 客户端使用裁判配置和 `--latency-report <新的NDJSON路径>`
源标记和回显均使用同一客户端 `performance.now()`,无需 ESP 时钟同步。绘制时刻以相邻 rAF 夹定,表中为上下界中点;此前双缓冲样本的区间宽度中位数为 11.4 ms。该范围包括相机取样等待、曝光、采集、无线传输、重组、本地 WebSocket、解码和画布绘制。它是**屏幕标记事件到首次可解码画布回显的估计**,没有测量源/输出面板扫描或像素响应;严格的物理光学端到端延迟需用高速相机复核。未识别帧不成为样本,首次可解码回显可能晚于较早的模糊回显,不能把有效样本分布当作每一帧的延迟。
此前双缓冲试验中,解码完成到画布绘制平均 2.72 ms;标记分析平均 2.02 ms。该分析只在校准模式运行,正常客户端不做画布回读。字段 `decodeToDrawMs` 从解码完成开始,不代表 JPEG 解码耗时。接收间隔、分片组装耗时及发送函数耗时各有独立口径,不能替代本测量。
此前双缓冲试验中,从解码完成到画布绘制平均耗时 2.72 ms,标记分析平均耗时 2.02 ms。标记分析仅在校准模式运行,正常客户端不回读画布。字段 `decodeToDrawMs` 从解码完成时开始计时,不代表 JPEG 解码耗时。接收间隔、分片组装耗时和发送函数耗时各有独立口径,均不能替代本测量。
`scripts/analyze_latency.py` 可按半开区间行号复算 NDJSON。完整结果、参数、行号范围和边界见制品 `LATENCY-RESULT.json`,分布图见 `LATENCY.png`。原始校准记录保存在本机 `target/latency/`,不把画面、私有配置或令牌日志打进制品。
+7 -5
View File
@@ -1,6 +1,6 @@
# 图传性能排查与验证(2026-10-18)
本文保留早期短时对照;当前交付已回退到 15 dBm,功率、距离与发送拥塞分析见 [功率与拥塞记录](power.md),阻塞处理见 [最新帧处理](realtime.md),此前约 30 fps 与回显延迟数据见 [延迟测量](latency.md),更早的验证见 [持续运行记录](stability.md)。下列数字对应当时的传感器时序和网络状态。
本文保留早期短时对照,数字对应当时的传感器时序和网络状态。当前交付已回退到 15 dBm,功率、距离与发送拥塞分析见 [功率与拥塞记录](power.md),阻塞处理见 [最新帧处理](realtime.md)。此前约 30 fps 与回显延迟数据见 [延迟测量](latency.md),更早的验证见 [持续运行记录](stability.md)。
## 环境与结论
@@ -21,7 +21,9 @@
表中 fps = 完整帧数 / 整个测量时长。帧损失包括被替换的不完整帧、超时及帧号缺口;末尾仍在接收的帧不计损失。丢帧率与旧客户端的口径不同,旧版会低估实际丢帧。
无线链路有明显时变,早期其它窗口也出现过约 5~15 fps。表中的数字不构成长期保证,这些配置也不是所有板卡的通用最优参数。前两行使用 CPU0,后两行使用 CPU1;核绑定对照未消除发送拥塞,最终保留显式 CPU1 配置。关闭 AMPDU 的试验几乎无法得到完整帧,已撤回;忙等重试也已撤回。另一次 40 ms 重试预算试验期间,有新客户端加入导致两个订阅者,且信号和画面复杂度变化;该轮约 12.6 fps,不能用于证明重试预算的单项影响,未作为最终配置依据。
无线链路随时间变化明显,早期其他窗口也出现过约 5~15 fps。表中数字不构成长期保证,这些配置也不是所有裁判系统的通用最优参数。前两行使用 CPU0,后两行使用 CPU1;核绑定对照未消除发送拥塞,最终保留显式 CPU1 配置。
关闭 AMPDU 的试验几乎无法得到完整帧,已撤回;忙等重试也已撤回。另一次 40 ms 重试预算试验中,新客户端加入后订阅者增至两个,信号和画面复杂度也有变化。该轮约 12.6 fps,无法证明重试预算的单项影响,因此未作为最终配置依据。
## 最终参数与改动
@@ -36,16 +38,16 @@
约 30 秒窗口中,后端完整帧增量 624、丢帧增量 38,窗口帧损失约 5.74%。界面连接存在,独立观察连接收到 619 张完整 JPEG,通道帧率 20.62 fps,帧间隔 P95 为 103.0 ms,无损坏 JPEG。
这项验证覆盖真实板卡、UDP 接收与重组、GUI 的 WebSocket 连接及二进制视频通道。当时另有 20 项协议测试、83 项客户端核心测试、9 项后端视频通道测试、5 项前端解码队列测试;Windows 类型检查和生产打包也通过。未将整个后端测试集或录制功能记为已验证。
这项验证覆盖真实裁判系统、UDP 接收与重组、GUI 的 WebSocket 连接及二进制视频通道。当时另有 20 项协议测试、83 项客户端核心测试、9 项后端视频通道测试、5 项前端解码队列测试;Windows 类型检查和生产打包也通过。未将整个后端测试集或录制功能记为已验证。
**时间边界:**组装耗时是首末分片到达的差值,帧间隔是相邻完整帧到达的差值,均不是摄像头到屏幕的端到端延迟。未做两端时钟同步或实际屏幕的光学延迟测量,也未自动验证 WebView 最终绘制像素。
组装耗时是首末分片到达的时间差,帧间隔是相邻完整帧到达的时间差,两者均不是摄像头到屏幕的端到端延迟。本次未进行两端时钟同步或实际屏幕的光学延迟测量,也未自动验证 WebView 最终绘制的像素。
## 复测与原始记录
先退出其它图传接收进程,避免增加订阅负载:
```text
python scripts/perf_probe.py <板卡IP> --seconds 60 --json perf.json --save-frame frame.jpg
python scripts/perf_probe.py <裁判系统IP> --seconds 60 --json perf.json --save-frame frame.jpg
```
本机原始 JSON、抽帧和串口日志在 `target/camera-diagnostics/`,随构建缓存忽略;主要文件为 `fps-core0-raw.json`、`fps-tx14-raw.json`、`fps-quality12-raw.json`、`fps-final-raw.json`、`fps-final-client-result.json`、`session-migration.json`。固件与 Windows 制品的 `BUILD-INFO.json` 记录最终测量及验证范围,ZIP 不包含用户运行配置。
+10 -8
View File
@@ -2,7 +2,7 @@
当前交付请求 `WIFI_TX_POWER_DBM=15`,实板驱动回读 **15.00 dBm**。根据用户的距离与拥塞反馈,从 17 dBm 回退;启动和 Wi-Fi 重连后应用同一设置,压缩与温控控制器不自动降低发射功率。三缓冲、VGA 彩色、AE 目标 45、约 20 ms 曝光上限和 PLL 时钟保持不变。
这里的数值是驱动配置的发射功率上限。API 回读不能代替功率计,也不能据此认为所有 PHY 速率下、每个数据包的天线端输出都恰好相同。
这里的数值表示驱动配置的发射功率上限。API 回读不能代替功率计测量,也不能证明所有 PHY 速率下、每个数据包的天线端输出都相同。
## 先前 17 dBm 记录说明了什么
@@ -20,8 +20,8 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲
## 为什么距离改善不明显
1. **功率差只有 3 dB。** 14→17 dBm 的配置上限约为两倍功率,不是两倍距离。按简化路径损耗模型 `距离倍率 = 10^(3/(10n))`,自由空间 `n=2` 时约 1.41 倍,假设室内 `n=3–4` 时约 1.19–1.26 倍。这只是模型估算,不是本次测得的距离。
2. **收发链路必须同时成立。** 增强板卡发射不能增强板卡接收;AP 信标、应答、订阅和数据回程仍依赖另一方向。ESP-IDF 的断连说明包含持续丢失信标等原因。当前记录没有最远点的断连 reason、AP 侧接收强度或重传统计,不能确定距离边界由哪一方向限制。来源:[ESP-IDF 断连事件说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected)。
3. **回读是上限,RSSI 有方向。** 板端日志里的 RSSI 是板卡接收到 AP 的信号,不是 AP 接收到板卡的信号。17 dBm 第二轮 RSSI 为 −25 至 −14 dBm,属于近距离记录,不能拿来证明远距离的上行链路已经改善。
2. **收发链路必须同时成立。** 增强裁判系统发射不能增强裁判系统接收;AP 信标、应答、订阅和数据回程仍依赖另一方向。ESP-IDF 的断连说明包含持续丢失信标等原因。当前记录没有最远点的断连 reason、AP 侧接收强度或重传统计,不能确定距离边界由哪一方向限制。来源:[ESP-IDF 断连事件说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected)。
3. **回读是上限,RSSI 有方向。** 板端日志里的 RSSI 是裁判系统接收到 AP 的信号,不是 AP 接收到裁判系统的信号。17 dBm 第二轮 RSSI 为 −25 至 −14 dBm,属于近距离记录,不能拿来证明远距离的上行链路已经改善。
## 为什么拥塞仍会加重
@@ -29,13 +29,13 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲
提高发射上限不会增加信道带宽或路由器的排队能力。若空口竞争、速率回退或重传使驱动排空变慢,突发提交仍会填满 TX 缓冲。这是与日志一致的机制推断;本次没有 CCA 忙占比、MAC 重传、实际发送 MCS 或 AP 队列数据,尚不能确认其中任何一项是功率变化造成的根因。来源:[ESP-IDF TX 数据路径与缓冲说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#how-to-improve-wi-fi-performance)。
更高功率改变干扰覆盖、射频线性度或供电瞬态,也可能间接影响链路,但这里没有相应测量,不将“接收端过载”“功放饱和”或“电压跌落”写成已确认原因。无 Brownout 记录也不能证明射频供电完全没有瞬态变化。
提高功率可能通过改变干扰覆盖、射频线性度或供电瞬态,间接影响链路,但本次没有相应测量,尚不能确认“接收端过载”“功放饱和”或“电压跌落”。没有 Brownout 记录,也不能证明射频供电完全没有瞬态变化。
电脑当前通过 **5 GHz / 信道 161 / 802.11ac** 连接,摄像头使用 2.4 GHz。因此不能直接声称视频在同一个 2.4 GHz 信道被路由器重复发送了两次;摄像头上行、AP 转发、电脑接收与 TUN 处理仍可能分别排队,TUN 保持原样。
## 还需区分本地跳帧
17 dBm 第二轮视频区间中,原生接收了 838 张完整 JPEG,而辅助本地 WebSocket 观察器收到 582 张。两者不是同一个指标:后端采用最新帧覆盖和帧龄期限,慢消费者可能主动跳过已完整收到的帧;少收到的本地消息不能全部算成无线丢帧,更不能直接等同于真实屏幕绘制帧率。
17 dBm 第二轮视频区间中,原生端接收了 838 张完整 JPEG,辅助本地 WebSocket 观察器收到 582 张。两者口径不同:后端采用最新帧覆盖和帧龄期限,慢消费者可能主动跳过已完整收到的帧。因此,本地消息少收到的部分不能全部算作无线丢帧,本地接收帧率也不能直接等同于实际屏幕绘制帧率。
## 当前回退验证与后续定位
@@ -43,7 +43,7 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲
本轮只把功率上限改到 15 dBm,其他图传参数与单 UDP 订阅保持不变,做五分钟收流观察;结果由交付包 `POWER-RESULT.json` 记录。之前的 17 dBm 记录保留为 `POWER-HISTORY-tx17.json`,14 dBm 的阻塞处理记录保留在 [最新帧处理](realtime.md)。本轮不重新测量最远断连距离或物理屏幕端到端延迟。
下一项值得单独对照的是**分片突发整形**:固定 15 dBm、曝光和 JPEG 策略,按发送回压逐步比较小批次提交与短暂让出调度,并同时看原生完整帧率、网络丢帧、主动跳帧和帧龄。不能仅通过增大缓冲或延长重试,把丢帧转化为持续排队延迟。若要确认距离瓶颈,需要同步记录远点断连原因、AP 侧板卡 RSSI、MAC 重传和信道忙占比。
下一项值得单独对照的是**分片突发整形**:固定 15 dBm、曝光和 JPEG 策略,按发送回压逐步比较小批次提交与短暂让出调度,并同时看原生完整帧率、网络丢帧、主动跳帧和帧龄。不能仅通过增大缓冲或延长重试,把丢帧转化为持续排队延迟。若要确认距离瓶颈,需要同步记录远点断连原因、AP 侧裁判系统 RSSI、MAC 重传和信道忙占比。
## 蛇形 PCB 天线的握持改善
@@ -54,11 +54,13 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲
1. **先改善握持和摆位。** 用塑料支架或绝缘延长握柄固定,让蛇形天线端伸出、远离手掌;握住远离天线的一端。可先尝试让手与天线相距约 3–5 cm,再按实测增加距离,这不是保证性能的固定阈值。
2. **留出天线净空。** Espressif 建议成品外壳中天线各方向至少 15 mm 净空;这不是“距离手 15 mm 就一定够”的保证。天线附近避开电池、金属支架、屏蔽片和紧贴的 USB 线或排线,外壳在天线区域使用非金属材料。来源:[ESP32-S3 PCB 天线布局指南](https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32s3/pcb-layout-design.html#general-principles-of-pcb-layout-for-modules-positioning-a-module-on-a-base-board)。
3. **固定朝向再比较。** 在同一位置和画面下,保持开发板朝向固定,比较“不接触 → 握远端 → 手靠近天线 → 移开手”,每段 30–60 秒,同时记录完整帧损失和板端 RSSI。单看 RSSI 不能覆盖上行质量,但可帮助区分朝向、遮挡与队列负载变化。
4. **需要长期手持时,优先考虑外置天线版本。** `ESP32-S3-WROOM-1` 为 PCB 天线,`WROOM-1U` 为外置天线接口版本。选兼容摄像头接线、Flash/PSRAM 配置的板卡,将合适的 2.4 GHz 天线安放在握持区域之外;具体封装、接口和射频连接须按实际板卡核对。外置天线也应远离手和金属。来源:[Espressif 模组数据手册](https://documentation.espressif.com/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf)。
4. **需要长期手持时,优先考虑外置天线版本。** `ESP32-S3-WROOM-1` 为 PCB 天线,`WROOM-1U` 为外置天线接口版本。选兼容摄像头接线、Flash/PSRAM 配置的裁判系统,将合适的 2.4 GHz 天线安放在握持区域之外;具体封装、接口和射频连接须按实际裁判系统核对。外置天线也应远离手和金属。来源:[Espressif 模组数据手册](https://documentation.espressif.com/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf)。
5. **必须贴近人体使用时,按最终结构调谐。** 外壳、支架、馈线和手持姿态都参与最终匹配,需要 VNA/射频测量后调整匹配网络。直接延长蛇形走线、随意焊线或并接两根天线不能代替调谐;在现板上先做可逆的机械隔离更容易验证。
## 15 dBm 本轮结果
当前 15 dBm 交付 App SHA-256:`a6092e4b0cf42819c04424e066651f285b8961312a73822d667532f800e46767`,应用分区 `0x70000`。COM8 烧录与三段独立校验通过;355.1 秒串口的 71 次功率回读均为 15.00 dBm。五分钟观察:原生完整接收 **29.68 fps**,丢帧 **1.045%**,本地 JPEG 通道 **28.23 fps**,31 张抽帧全部完整解码为 640×480 RGB,无无效 JPEG、设备离线、通道断开、复位或采集错误;仍有最长 1.646 秒到帧间隔,不能将此轮称为无卡顿。距离与拥塞分析见本页,测试边界见制品 `POWER-RESULT.json`。
当前 15 dBm 交付 App 的 SHA-256 为 `a6092e4b0cf42819c04424e066651f285b8961312a73822d667532f800e46767`,应用分区为 `0x70000`。COM8 烧录与三段独立校验通过;355.1 秒串口记录中的 71 次功率回读均为 15.00 dBm。
五分钟内,原生完整接收帧率为 **29.68 fps**,丢帧率为 **1.045%**,本地 JPEG 通道帧率为 **28.23 fps**。31 张抽帧均完整解码为 640×480 RGB;未发现无效 JPEG,也未出现设备离线、通道断开、复位或采集错误。但最长到帧间隔仍有 1.646 秒,不能将此轮称为无卡顿。距离与拥塞分析见本页,测试边界见制品 `POWER-RESULT.json`。
按时间近似对齐的板端五分钟区间有 731 次发送失败、653 次应用重试、78 次目标帧放弃;温度 56–58°C,RSSI -35 至 -11 dBm。板端区间由五秒采样的结束时间选择,边界并非逐包精确对齐。与此前短测时长、现场无线状态和 JPEG 内容不同,不能将差值归因于功率这一项。
+6 -4
View File
@@ -2,11 +2,13 @@
## 当前交付
当前 15 dBm 交付 App SHA-256:`a6092e4b0cf42819c04424e066651f285b8961312a73822d667532f800e46767`,应用分区 `0x70000`。COM8 烧录与三段独立校验通过;355.1 秒串口的 71 次功率回读均为 15.00 dBm。五分钟观察:原生完整接收 **29.68 fps**,丢帧 **1.045%**,本地 JPEG 通道 **28.23 fps**,31 张抽帧全部完整解码为 640×480 RGB,无无效 JPEG、设备离线、通道断开、复位或采集错误;仍有最长 1.646 秒的到帧间隔,不能称为无卡顿。距离与拥塞分析、测试边界见 [功率与拥塞记录](power.md) 和制品 `POWER-RESULT.json`。
当前 15 dBm 交付 App 的 SHA-256 为 `a6092e4b0cf42819c04424e066651f285b8961312a73822d667532f800e46767`,应用分区为 `0x70000`。COM8 烧录与三段独立校验通过;355.1 秒串口记录中的 71 次功率回读均为 15.00 dBm。
五分钟内,原生完整接收帧率为 **29.68 fps**,丢帧率为 **1.045%**,本地 JPEG 通道帧率为 **28.23 fps**。31 张抽帧均完整解码为 640×480 RGB;未发现无效 JPEG,也未出现设备离线、通道断开、复位或采集错误。但最长到帧间隔仍有 1.646 秒,不能称为无卡顿。距离与拥塞分析、测试边界见 [功率与拥塞记录](power.md) 和制品 `POWER-RESULT.json`。
沿用已确认可用的三块 PSRAM 采集缓冲、OV5640 VGA 彩色、约 30 fps PLL 时钟、AE 目标 45/55、约 20 ms 曝光上限及自动增益;Wi-Fi 自动重连和 TUN 兼容订阅保持不变。没有增加待发送 JPEG FIFO,也没有为了消除队列改成单块 DMA 缓冲。
发送程序只持有当前一张完整 JPEG;阻塞恢复后通过有限次数的零等待取帧选择最新有效帧,旧帧立即归还驱动。三块采集内存用于捕获余量,与必须按序播放的三帧队列不同。已有相机队列仍然有限,应用交接会跳过其旧帧。
发送程序一次只持有一张完整 JPEG。阻塞恢复后,通过有限次数的零等待取帧选出最新有效帧,并立即将被替换的旧帧归还驱动。三块采集内存提供捕获余量,并非必须按序播放的三帧队列。相机队列仍有容量上限,应用交接时会跳过其中的旧帧。
完整 JPEG 取得后按 1400 字节载荷分片立即提交,复用一个数据报缓冲,直接读取 PSRAM。当前传感器驱动在 JPEG 结束后交出完整帧,本次没有改成在 DMA 尚未完成时就读取其内容;不完整 JPEG 由接收端整帧丢弃,不进入绘制。
@@ -35,9 +37,9 @@ WIFI_TX_POWER_DBM=15
若要在发送回压时立即放弃本帧,可将 `UDP_RETRY_BUDGET_US=0` 后重编烧录。该模式仍保留三块采集内存和完整 JPEG 校验;这里的“零”指不让出调度重试,不是删除驱动和路由器中的所有缓冲。零重试策略已经主机测试,本次五分钟实机使用默认 8000 μs。
分片间隔也计入 25 ms 检查期限,不再额外延长帧的发送寿命;设置过大的间隔可能使整帧无法在期限内提交。分片在 SDK socket 中已经接受后无法由本策略撤回,底层调度和网络排队也无法由应用层强制抢占。以上为应用层新鲜度策略,不能保证每帧、无线链路或物理屏幕具有硬实时上限。
分片间隔计入 25 ms 检查期限,不额外延长帧的发送寿命;间隔过大可能使整帧无法按期提交。SDK socket 已接受的分片无法由本策略撤回,应用层也无法强制抢占底层调度和网络排队。因此,这些新鲜度策略只能约束应用层,不能为每帧、无线链路或物理屏幕保证硬实时上限。
本地 JPEG 二进制头继续为 16 字节,旧消费者可忽略新增文本消息。新前端连接旧后端时仍可用,但没有原生发布时间,只能按 JS 收到时刻判断本地队列年龄。两端本机发布时间使用同机系统时间;这不是 ESP 与电脑的时钟同步,也不能据此计算无线单向延迟。JS 内部排队同时使用单调时钟检查。
本地 JPEG 二进制头仍为 16 字节,旧消费者可忽略新增的文本消息。新前端也可连接旧后端,但由于没有原生发布时间,只能按 JS 收到消息的时刻判断本地队列年龄。本地通道两端使用同机系统时间记录发布时间;这不涉及 ESP 与电脑的时钟同步,也不能用于计算无线单向延迟。JS 内部排队同时使用单调时钟检查。
## 此前 14 dBm 版本的实机五分钟检查
+4 -4
View File
@@ -4,9 +4,9 @@
## 测试目标与配置
使用 ESP32-S3-N16R8 / OV5640、VGA、8 MB PSRAM、CPU 240 MHz、Windows 原生客户端,保留 Mihomo TUN。每轮只运行一个 UDP 订阅者,串口记录器与本地 WebSocket 观察器不会新增 UDP 推流目标。Windows 到板卡的实际路由走 WLAN;电脑连接 5 GHz、板卡连接 2.4 GHz。
使用 ESP32-S3-N16R8 / OV5640、VGA、8 MB PSRAM、CPU 240 MHz、Windows 原生客户端,保留 Mihomo TUN。每轮只运行一个 UDP 订阅者,串口记录器与本地 WebSocket 观察器不会新增 UDP 推流目标。Windows 到裁判系统的实际路由走 WLAN;电脑连接 5 GHz、裁判系统连接 2.4 GHz。
优化目标为彩色、内容可辨、较短曝光和尽可能高的完整接收帧率,每轮有效测量为 5 分钟。下文保留了亮度与时序调整过程;不同轮次的场景、曝光、压缩和网络状态有变化,接收数字是描述性对照,不能作为严格单因素实验。
优化目标是在保持彩色、内容可辨和较短曝光的同时,尽可能提高完整接收帧率;每轮有效测量为 5 分钟。下文保留亮度与时序调整过程。各轮场景、曝光、压缩和网络状态有所变化,因此接收数据仅供描述性对照,不属于严格的单因素实验。
```dotenv
CAM_RES=vga
@@ -56,7 +56,7 @@ OV5640 驱动的 AE 档位不是 EV 数值:名义零档目标为 55,档位
测量窗口内曝光为 295–295 行;JPEG 档位为 18–22,实际增益 11.00–18.00×。内部堆最小 129455 字节,平均提交耗时 2.60 ms/帧,温度 54–59°C,未触发温度减载。
这次完整接收帧率接近当前可靠彩色时序的约 22.2 fps 上限。亮度、画面内容、JPEG 大小和网络负载在各轮之间变化,不能把全部差异归因于单一控制参数,也不能将五分钟结果作为长期或所有路由器的保证。帧间隔不是摄像头到屏幕的光学延迟。
本轮完整接收帧率接近当前可靠彩色时序的约 22.2 fps 上限。各轮亮度、画面内容、JPEG 大小和网络负载不同,不能把全部差异归因于单一控制参数,也不能用五分钟结果保证长期运行或所有路由器下的表现。帧间隔不是摄像头到屏幕的光学延迟。
本轮固件 SHA-256 为 `e297069e190dcccefbae6d95591703c6848f4e523f910e2c0cb44af91f698936`,当时与交付及板上镜像一致;当前交付以 [项目说明](../../README.md) 和交付包的 `BUILD-INFO.json` 为准。配套 Bootloader、分区表及 App 均已独立读回校验。曲线和完整数据位于制品包内的 `STABILITY.png` / `SCHEDULING-RESULT.json`,不包含相机抽帧或令牌日志。
@@ -113,7 +113,7 @@ USB 恢复后,HTS 1500 的板上采集探针达到 30.49 fps,但白光环境
原计划 30 分钟的灰度 / 15 fps 长测按用户要求提前停止,保留了 **18 个完整分钟窗口**,合计 1080.15 秒:完整帧 15307、丢帧 806,平均 14.17 fps,帧损失 5.00%。板端记录温度约 48–58°C,无复位和采集错误。
早期丢帧上升时温度没有同步上升,温度下降期间也出现过明显发送失败。内存紧张后能够恢复;发送失败与队列回压更符合当前证据。这一轮不计为完成 30 分钟验证,且配置不同,不能与彩色不限帧试验直接作单因素对照。
早期丢帧上升时,温度没有同步上升;温度下降期间也出现过明显发送失败,内存紧张后则能够恢复。因此,现有证据更支持发送失败与队列回压的解释。本轮不能计作完成 30 分钟验证,且配置不同,不能与彩色不限帧试验直接作单因素对照。
温度控制仍可选:达到 75°C 时将已设置的帧率减半(向上取整),原来不限帧时降至 8 fps;降到 70°C 后恢复。阈值可设 40–95°C,`0` 禁用。75°C 是主动减载策略,不是芯片损坏温度规格。本轮未触发该策略。ESP 内部温度不能代表 OV5640 或环境温度:[ESP-IDF v5.5.5 温度传感器说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-reference/peripherals/temp_sensor.html)。
+6 -4
View File
@@ -8,9 +8,9 @@
| --- | --- |
| 提示缺少 `esp-1.97.0.0` | 按上述命令安装指定名称的工具链,重新打开终端后检查版本 |
| `espup` 查询 GitHub 版本返回 403 | 对上述固定版本安装命令追加 `--skip-version-parse`,或稍后重试 |
| 找不到 `ldproxy` / `libclang` | 安装链接工具;Unix 加载 `export-esp.sh`,Windows 重开终端 |
| 找不到 `ldproxy` / `libclang` | 安装链接工具;Unix 加载 `export-esp.sh`;Windows 使用 `scripts/build.ps1`,由脚本将 esp-clang 及其 DLL 依赖目录放到 PATH 前面 |
| ESP-IDF 路径或版本错误 | 清除旧 `IDF_PATH` 或指向 v5.5.5 SDK |
| Windows 路径过长 | 缩短仓库和工具链路径,见开发说明 |
| Windows 报 `Too long output directory` | 构建前设置 `$env:CARGO_TARGET_DIR = 'C:\s3vt'`,再运行 `scripts/build.ps1 -Package`;完整输出路径不得超过 88 字符,见[固件入门](../setup/firmware.md#编译固件) |
| Wi-Fi 连不上 | 核对 2.4 GHz、凭据和认证方式,再重新编译烧录 |
| 相机无输出 / 初始化失败 | 核对板型接线、供电与 Octal PSRAM |
| `NO-EOI`、几帧后停止 | 确认 `CONFIG_CAMERA_PSRAM_DMA=n` |
@@ -20,9 +20,11 @@
| 改 C 组件后无变化 | 清理 `esp-idf-sys` 构建缓存后重编,见开发说明 |
| 主机项目被编为 Xtensa | 显式指定宿主 target,见各子项目说明 |
用 `python scripts/udp_probe.py 10` 可独立检查视频协议;装甲板用 `python scripts/armor_probe.py 15`。这些脚本需要板卡在线,构建通过不代表已完成硬件验证。
`libclang.dll` 文件存在却报 `LoadLibraryExW failed` 时,还需检查它的依赖是否来自同一套 LLVM。仅将目录追加到 PATH 末尾不够;若前面已有其他 LLVM,可能加载到不兼容的同名 DLL。PowerShell 构建入口会将本项目工具链的 esp-clang 目录移到 PATH 前面。
测量帧率和丢帧可运行以下命令,将 IP 换成板卡实际地址。对照测试时先退出桌面客户端,避免额外订阅者增加无线负载:
用 `python scripts/udp_probe.py 10` 可独立检查视频协议;装甲板用 `python scripts/armor_probe.py 15`。这些脚本需要裁判系统在线,构建通过不代表已完成硬件验证。
测量帧率和丢帧时,运行以下命令,并将 IP 替换为裁判系统的实际地址。对照测试前先退出桌面客户端,避免额外订阅者增加无线负载:
```text
python scripts/perf_probe.py 192.168.1.139 --seconds 30 --json perf.json