diff --git a/.gitignore b/.gitignore index ffe0403..eae59d7 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,5 @@ __pycache__/ # `docs/simba_docs`:工作区里另起的一个 Astro 文档站,与本仓库的目录结构无关, -# 也不属于本项目的产物。留着不跟踪,免得以后 `git add -A` 顺手把它带进来。 -/docs/ +# 也不属于本项目的产物。只忽略它本身;docs/ 下的项目文档要跟踪。 +/docs/simba_docs/ diff --git a/CHANGELOG.json b/CHANGELOG.json index 9e4cc83..e6ce006 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -3,31 +3,16 @@ "major": 0, "minor": 2, "patch": 1, - "build": 35, + "build": 36, "protocol": 1, "date": "2026-10-19", - "baseline": "v0.2.0", + "baseline": "v0.2.1", "commits": 1, "groups": { - "fix": [ + "docs": [ { - "scope": "ci", - "text": "版本校验在浅克隆下必然误报", - "breaking": false - }, - { - "scope": "ci", - "text": "checkout 改成 fetch-depth: 0——没有标签、提交数恒为 1 时,", - "breaking": false - }, - { - "scope": "version", - "text": "CHANGELOG 按标签区间重算全部历史,不再只渲染当前版本,", - "breaking": false - }, - { - "scope": "build", - "text": "固件包文件名用算出来的版本,不再用 BUILD-INFO.json 里那套旧编号", + "scope": null, + "text": "按 humanizer 重写项目文档并同步 RMIC::2026 现状", "breaking": false } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index aa04894..752ec47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,15 @@ ## v0.2.1 — 2026-10-19 -构建号 **35** · 线路协议 **1** · 1 个提交(v0.2.0 → v0.2.1) +构建号 **36** · 线路协议 **1** · 1 个提交(v0.2.1 → v0.2.1) + +### 文档 + +- 按 humanizer 重写项目文档并同步 RMIC::2026 现状 + +## v0.2.1 — 2026-10-19 + +1 个提交 ### 修复 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a8f544d..866d04a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,7 +4,7 @@ ## 镜像导出 -以下使用 espflash 4.4.0 与固定的 `esp-1.97.0.0` 工具链。完成构建后,使用 `esp-idf-sys` 复制到 `release/` 的 Bootloader。Windows PowerShell: +以下使用 espflash 4.4.0 与固定的 `esp-1.97.0.0` 工具链。构建完成后,Bootloader 由 `esp-idf-sys` 复制到 `release/`。Windows PowerShell: ```powershell $targetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' } @@ -36,9 +36,9 @@ espflash write-bin --chip esp32s3 --port COM5 0x0 target/firmware/esp32s3-merged cargo +stable test --locked --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]' ``` -Linux 常见 host 为 `x86_64-unknown-linux-gnu`,Apple Silicon 为 `aarch64-apple-darwin`,可用 `rustc +stable -vV` 查看。`--target` 覆盖仓库里的其他平台配置,空 `build-std` 避免主机测试继承嵌入式标准库重编译。 +Linux 常见 host 为 `x86_64-unknown-linux-gnu`,Apple Silicon 为 `aarch64-apple-darwin`,可用 `rustc +stable -vV` 查看。`--target` 覆盖仓库里的其他平台配置,空的 `build-std` 避免主机测试按嵌入式配置重编标准库。 -装甲规则从 `armor/` 执行 `cargo +stable test -p armor-protocol`,桌面核心从 `desktop/core/` 执行 `cargo +stable test`,同样按需指定宿主 `--target` 和空 `build-std`。 +装甲规则从 `armor/` 执行 `cargo +stable test -p armor-protocol`,桌面核心从 `desktop/core/` 执行 `cargo +stable test`,同样按需指定宿主 `--target` 和空的 `build-std`。 限帧、温度回差和 JPEG 压缩回压策略可以独立在主机运行,无需连接板卡。Windows 在根目录执行: @@ -52,7 +52,7 @@ rustc +stable --edition 2021 --test src/stream_control.rs -o target/stream-contr ## 清理构建缓存 -改动 C 组件或 `sdkconfig.defaults` 后,增量构建若没有反映变化,可从根目录执行: +改动 C 组件或 `sdkconfig.defaults` 后,如果增量构建没有反映这些改动,可从根目录执行: ```text cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys @@ -76,7 +76,7 @@ cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys ## 性能测量 -本次在保留 TUN 环境中的对照测试、最终参数与验证边界见 [PERFORMANCE.md](PERFORMANCE.md)。 +本次对照测试在保留 TUN 的环境中进行,最终参数与验证边界见 [PERFORMANCE.md](PERFORMANCE.md)。 下表为原 README 保存的**历史单次测试数据**,不是本次构建实测或性能保证。配置为 OV5640、VGA;原始日志、时长和完整网络条件未随这些数字保存。 diff --git a/FLASH-CLIENT-ARCH.md b/FLASH-CLIENT-ARCH.md index 1cee805..cddb6fe 100644 --- a/FLASH-CLIENT-ARCH.md +++ b/FLASH-CLIENT-ARCH.md @@ -1,6 +1,7 @@ # 烧录 / 维护客户端架构设计(ESP32-S3 图传板) > 状态:**提议中(Proposed)**,等待第 9 节的问题清单答复后冻结。 +> 落地情况:实际实现走的是 desktop 维护页 + `.zip` bundle,见 [docs/firmware-maintenance.md](docs/firmware-maintenance.md)。 > 输入约束:现场存在 SPI Flash 坏块;需要兼顾 OTA 与现场排障;**不改固件源码、不重新编译**; > 优先“模板固件 + 动态 patch”。 > 本文所有结论都在仓库里逐条对过代码/脚本/分区表/文档,引用位置见各节链接。 @@ -12,22 +13,22 @@ 1. **“不改固件源码”可以做到;但“真 OTA”做不到。** 当前固件没有 `ota_0/ota_1/otadata` 分区, 没有 `esp_ota` 调用,UDP 协议里也没有任何命令通道(只有 Hello/Subscribe/视频/血量)。 设备自己没有能力接收固件。所谓“兼顾 OTA”,在现约束下只能是**现场 USB 串口更新**, - 或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。**这是必须先决的问题**。 -2. **坏块规避其实根本不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回 + 或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。这是必须先决的问题。 +2. **坏块规避其实不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回 `partitions.csv`(构建输入)再触发重建;而 **App 镜像不包含自己的分区偏移**,偏移只存在于 `0x8000` 的 `partition-table.bin`(0xC00 字节 + 末尾 16 字节 MD5)。 → 对同一份已构建镜像,**搜索偏移 + 重写分区表 + 原样写 App**,全程零编译。 这是本方案最大的收益点,也是“模板 + patch”的第一块拼图。 3. **SSID/密码不是 NVS,是 `.rodata` 里的字符串字面量**(`env!` 编译期注入)。 - 二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch **App 镜像**, - 且必须连带处理**字符串长度常量**与**镜像 checksum/SHA256**。 + 二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch App 镜像, + 且必须连带处理字符串长度常量与镜像 checksum/SHA256。 长度常量能否可靠定位是这个方案真正的技术风险(见 7.2)。 4. **坏块信息不能“读”出来,只能“写进去再读回来”差分出来。** `flash_id` 只给芯片型号与容量, SFDP 只给参数,启动日志只给症状。仓库里 `0x10863` 的 bit6 挂在 1,就是整段回读比对发现的 (HARDWARE.md)。因此客户端必须自带「写入图案 → 回读 → 差分」与「写后整段校验差分」两条链路。 5. **推荐形态:纯逻辑核心库 `flash-core` + 优先 CLI + 桌面端加一页**。 把最危险的逻辑(镜像格式、分区表、偏移搜索、补丁)做成不碰 I/O 的纯函数, - 在主机上用**真实 1.1 MB 镜像做往返测试**——这与仓库已有的 `protocol/`、`stream_control`、 + 在主机上用真实 1.1 MB 镜像做往返测试——这与仓库已有的 `protocol/`、`stream_control`、 `link_control`「能在主机跑测试的策略代码」风格一致。 --- @@ -59,7 +60,7 @@ const PASSWORD: &str = env!("WIFI_PASS"); ``` - 每次启动都会用编译期值覆盖驱动配置:`wifi.set_configuration(&client_config(auth)?)` - ([src/main.rs:502](src/main.rs#L502))。所以 **NVS 里即使存着旧配置也不会被采用**。 + ([src/main.rs:502](src/main.rs#L502))。所以 NVS 里即使存着旧配置也不会被采用。 - `EspDefaultNvsPartition` 只是被交给 `EspWifi::new`([src/main.rs:178](src/main.rs#L178)), 用于驱动自身的存储,不参与凭据解析。 - 对比:[armor/armor-firmware/src/settings.rs](armor/armor-firmware/src/settings.rs) 的 C3 装甲板 @@ -90,13 +91,13 @@ const PASSWORD: &str = env!("WIFI_PASS"); 关键推论(对架构有决定性影响): - `0x10863` 位于**未分配空洞** `0x10000–0x70000`(`phy_init` 结束到 `factory` 开始之间, - 380 KB 全是空闲 flash)。→ 这块空洞就是天然的**坏块探测试验场**,写坏也不要紧。 + 380 KB 全是空闲 flash)。→ 这块空洞就是天然的坏块探测试验场,写坏也不要紧。 - `0xFADE7` 落在 App 镜像的**代码段**内(镜像布局见 1.5),所以“把那个字节改成别的安全值” - 会改坏指令;**唯一安全的动作是挪分区**。 + 会改坏指令;唯一安全的动作是挪分区。 - 现行偏移已经漂过至少三次:`0x10000` → `0x50000`([LATENCY.md:88](LATENCY.md#L88))→ `0x70000` (当前 [partitions.csv](partitions.csv))。每次重编都要重新核对,这正是要消灭的脆弱点。 -- 缺陷有两种**方向相反**的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF” - 的检查里是**隐形**的,必须写 0 才暴露。探测逻辑必须两种都打图案。 +- 缺陷有两种方向相反的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF” + 的检查里是隐形的,必须写 0 才暴露。探测逻辑必须两种都打图案。 ### 1.5 已实测出来的镜像布局(`fps-final-raw-serial.log` 逐段对齐算出) @@ -116,7 +117,7 @@ const PASSWORD: &str = env!("WIFI_PASS"); 几个直接可用的副产品: - `esp_app_desc_t` 在 **App 分区 + 0x20**,内含 `version` / `project_name` / `idf_ver` / 编译时间。 - **不需要让板子跑起来**,读 256 字节就能回答“这块板上到底是哪版固件”。 + 不需要让板子跑起来,读 256 字节就能回答“这块板上到底是哪版固件”。 - 分区表就是 0x8000 起的 0xC00 字节(`verify 0xc00 @ 0x8000` 佐证),末尾 16 字节 MD5。 改写偏移只要重算这 16 字节。 @@ -142,14 +143,14 @@ const PASSWORD: &str = env!("WIFI_PASS"); - ✅ 现场只有一个程序;已有 NSIS 打包、CI、配置与密码闸门可复用。 - ✅ 视频端本来就要连板子,烧完顺手进监控很自然。 - ❌ WebView 里跑长任务 + 进度 + 日志尾随,需要额外的事件/取消/背压设计。 -- ❌ 烧录是**系统级、不可中断、会变砖风险**的操作,和“看画面”共享一个进程的失败域。 +- ❌ 烧录是系统级、不可中断、有变砖风险的操作,和“看画面”共享一个进程的失败域。 - ❌ 无法在 CI / 无人值守 / 远程 SSH 场景复用。 ### 方案 B(推荐):纯逻辑核心库 + 双前端(CLI 优先,GUI 复用) `flash/core`(无 I/O,纯函数,主机可测)+ `flashctl` CLI + 桌面端后续加一页调用 Tauri 命令。 -- ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)**全部可在主机测**, +- ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)全部可在主机测, 且可以用仓库里真实的 1.1 MB 镜像做往返验收。 - ✅ CLI 先天可脚本化:CI 能跑、现场能远程跑、能出确定退出码与机器可读报告。 - ✅ GUI 后置,做完 CLI 后界面只是薄壳;失败域分开(GUI 崩了不影响烧录工具)。 @@ -160,7 +161,7 @@ const PASSWORD: &str = env!("WIFI_PASS"); - ✅ 落地最快,协议风险最低;`target/tools/espflash/espflash.exe` 已经在了。 - ✅ 与现有 `scripts/*.sh` 心智一致。 -- ❌ **拿不到回读差分**(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案 +- ❌ 拿不到回读差分(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案 坏块检测的核心,只能再调一次 `read-flash` 自己比。 - ❌ 解析人类可读输出、处理 `esptool.py` 的 Python 依赖,现场极易碎(现有 flash.sh 就是被 “macOS 串口名 + termios + 本地 IDF venv”绑死的)。 @@ -177,7 +178,7 @@ const PASSWORD: &str = env!("WIFI_PASS"); | 交付成本 | 10% | 6 | 6 | **9** | | 加权 | | 6.45 | **8.10** | 5.65 | -**推荐 B**,并明确:GUI 不是不做,而是**放到 P2**——先用 CLI 把危险逻辑做对、做实测。 +**推荐 B**。GUI 不是不做,而是放到 P2:先用 CLI 把危险逻辑做对、做实测。 --- @@ -284,8 +285,8 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─ | 串口日志诊断 | 症状:`invalid header` / checksum 失败 / rst 原因 | 秒级 | 板子已经在现场、只能看现象时 | 要点: -- 图案探测必须**双向**:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形—— - `0x10863` 就是整段回读才发现就是这个原因。 +- 图案探测必须双向:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形, + `0x10863` 就是这样在整段回读里才发现的。 - 探测区选空洞而不是 NVS:**不要为了体检去毁 NVS**。若怀疑 NVS 区有坏点,单独提供 “备份 NVS → 探测 → 还原”的危险模式,默认关闭。 - probe 结束后必须 `erase` 干净,并**复核空洞回到 0xFF**,避免留下疑似数据。 @@ -309,7 +310,7 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─ - **对齐放宽到 4 KiB**:App 分区只要求 4 KiB 对齐,`flash.sh` 用 64 KiB 是自我限制。 候选从 ~230 个变成 ~3700 个,命中概率高一个数量级(单坏点 1/8,双坏点也能稳过)。 - **分区大小 ≠ 镜像大小**:`0x120000` 的分区里镜像只占 `0x10E700`,尾部 71 KiB 是空的。 - 坏扇区落在尾部 slack 里**无害**,别把它算成阻塞。 + 坏扇区落在尾部 slack 里无害,别把它算成阻塞。 - **bootloader 与分区表固定偏移**:`0x0`(0x5160) 与 `0x8000`(0xC00) 挪不了。 这里只能做**字节级吸收**:例如坏点落在分区表某条 `label` 字段里,就换一个 label 让那一位合法 (label 是自由文本);落在 `magic/offset/size/MD5` 上则该板报废。这条路径要显式实现并明确其边界。 @@ -317,7 +318,7 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─ ### 4.3 执行(`flash-core::parttable` + `image`) - 改写 `partition-table.bin` 中 `factory` 的 offset(和必要时 size)→ 重算末尾 16 字节 MD5。 -- **App 镜像原样写入新偏移**——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。 +- App 镜像原样写入新偏移——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。 - 只有在“分区表/bootloader 必须字节吸收”这一层才动镜像,并且要重算 checksum/SHA256。 ### 4.4 验证与回滚(`session` + `report`) @@ -346,7 +347,7 @@ checksum = 0xEF XOR (所有段的所有数据字节) // 1 字节,位于 sha256 = SHA256(镜像中它之前的全部字节) // 32 字节,仅当 header.hash_appended=1 ``` -任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在**真实镜像上做往返测试** +任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在真实镜像上做往返测试 (重算结果与文件里已有的字节必须完全相同)——这是整个方案最便宜、最有效的验收手段, 在碰硬件之前就能证明实现是对的。 diff --git a/HARDWARE.md b/HARDWARE.md index b497cf5..4a4e95e 100644 --- a/HARDWARE.md +++ b/HARDWARE.md @@ -20,7 +20,7 @@ 当前 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 挂死、串口静默。 ## 内存与任务配置 @@ -38,7 +38,7 @@ | `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM` | `64` | 发送缓冲数量,使用内部 RAM | | `CONFIG_ESP_WIFI_TX_BA_WIN` | `32` | 保留 AMPDU 发送聚合 | -相机使用 3 个 PSRAM 帧缓冲及 `GRAB_LATEST`,优先取得较新的画面。JPEG 编码由传感器完成,S3 主要负责采集和发送。 +相机使用 3 个 PSRAM 帧缓冲及 `GRAB_LATEST`,优先取较新的帧。JPEG 编码由传感器完成,S3 主要负责采集和发送。 原测试板启用 PSRAM DMA 后曾出现 `NO-EOI`:JPEG 结束标记位于驱动末尾 DMA 搜索窗口之外,随后停止交付帧。因此默认关闭该配置;更换驱动后应单独验证持续采集。 @@ -54,7 +54,7 @@ 分区表位于 `0x8000`,S3 Bootloader 从 `0x0` 开始。App 写入分区表声明的偏移,不能直接套用 `0x10000`。修改分区或 Flash 模式后需重新构建,成套更新 Bootloader、分区表和 App。 -健康硬件可以评估恢复 `0x10000` App 偏移或更快 Flash 配置,但需重新验证。 +健康硬件可以考虑把 App 偏移恢复到 `0x10000`,或改用更快的 Flash 配置,但需重新验证。 ## 损坏 Flash 模组的兼容处理 @@ -62,7 +62,7 @@ 2026-10-18 的整段回读还发现 **`0x10863`** 的 bit 6 异常读回为 1:期望 `0x2e`,读回 `0x6e`,该轮 App 只有这一处字节不同,尚未单独擦写复核是否为永久故障。该模组的 App 偏移需至少为 `0x20000`,并继续检查 `0xFADE7`;烧录工具的校验失败必须先排除,不能当作网络或相机故障。 -`scripts/flash.sh` 保留的流程为:编译、导出 App、搜索 64 KiB 对齐的可写偏移、修改分区、重新构建和验证,再写入三段镜像。重新构建可能移动镜像内容,因此第二次验证不可省略。 +`scripts/flash.sh` 保留的流程是:编译、导出 App、搜索 64 KiB 对齐的可写偏移、修改分区、重新构建和验证,再写入三段镜像。重新构建可能移动镜像内容,因此第二次验证不可省略。 脚本依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录及旧版工具参数,不适合作为通用烧录入口。坏板需核对工具版本及坏地址字节;普通板卡使用 README 流程。更换故障模组比长期依赖镜像位置更可靠。 diff --git a/LATENCY.md b/LATENCY.md index 5ce0421..81f634f 100644 --- a/LATENCY.md +++ b/LATENCY.md @@ -1,14 +1,14 @@ # 约 30 fps、Wi-Fi 重连与画面链路延迟验证(2026-10-18) -当前交付已增加阻塞恢复与新鲜度处理,见 [REALTIME.md](REALTIME.md)。本页保留此前减光确认和画面延迟对照;本页 SHA 与测量结果均属于当时构建,不是当前固件的重测结果。 +当前交付已增加阻塞恢复与新鲜度处理,见 [REALTIME.md](REALTIME.md)。本页保留此前减光确认和画面延迟对照;所列 SHA 与测量结果均属于当时构建,不是当前固件的重测结果。 ## 先前三缓冲与小幅减光记录 -根据用户长程运行反馈,双缓冲在峰值负载下丢帧明显,当前交付恢复 **`CAM_FB_COUNT=3`**,保留突发发送期间的采集余量。约 30 fps 完整 PLL 配置、VGA 彩色、正常白平衡、Wi-Fi 自动重连和自适应 JPEG 均保留。双缓冲保留为试验选项,下面的短时延迟对照属于历史记录,不能证明长程峰值稳定性。 +根据用户长程运行反馈,双缓冲在峰值负载下丢帧明显,当前交付恢复 **`CAM_FB_COUNT=3`**,为突发发送留出采集余量。约 30 fps 完整 PLL 配置、VGA 彩色、正常白平衡、Wi-Fi 自动重连和自适应 JPEG 都没有改动。双缓冲作为试验选项保留,下面的短时延迟对照属于历史记录,不能证明长程峰值稳定性。 针对现场轻微过亮,增加 `CAM_AE_LEVEL=-1`,将 OV5640 AE 目标从 55 微调到 45,目标比值约 −0.29 EV;`CAM_EXPOSURE_EV=0`,自动曝光和增益仍联动,曝光上限保持 592 行(约 20 ms)。这会小幅降低亮度目标,不保证实际快门必然缩短。当前 50 Hz 抗频闪步长约 10 ms,直接小减曝光上限可能跨越一整档,因此使用 AE 目标细调。该目标比值不等于经过照度标定的实际 EV。 -本次已通过 COM8 烧录与 Bootloader、分区表、App 的独立校验。重新启动后串口观察 45.15 秒,复位和采集错误均为 0,订阅已恢复;末个窗口提交 29.7 fps。观察中仍有 UDP 发送回压,三缓冲不代表完全消除路由器拥塞。本次未重新测量原生完整接收损失率或画布延迟,也未重新进行长程峰值测试。 +本次烧录经 COM8 完成,Bootloader、分区表和 App 均通过独立校验。重新启动后串口观察 45.15 秒,复位和采集错误均为 0,订阅已恢复;末个窗口提交 29.7 fps。观察中仍有 UDP 发送回压,三缓冲不代表完全消除路由器拥塞。本次未重新测量原生完整接收损失率或画布延迟,也未重新进行长程峰值测试。 用户已确认当前三缓冲、小幅减光版本亮度合适、颜色稳定。 @@ -33,7 +33,7 @@ 这次窗口的 P50 降低 6.8 ms、P95 降低 37.3 ms。双缓冲减少潜在采集排队,同时保住接近 30 fps 的实际交付。两个窗口的网络回压、JPEG 自适应档位和时长不同,属于连续实机对照,不能把全部改善归因于缓冲数,也不是所有路由器或场景的保证。此前约 22 fps 的测试保留在 [STABILITY.md](STABILITY.md)。 -双缓冲试验五分钟全程在线,复位 0 次、采集错误 0 次,通道断开 0 次、计数重置 0 次。抽取 61 张 JPEG 均完整解码为 640×480 RGB;没有用灰度或降低色深换帧率。最小内部空闲堆 118771 B;实际 JPEG 档位 18–28,温度 58–60°C,未触发减载。 +双缓冲试验五分钟全程在线,复位、采集错误、通道断开和计数重置均为 0 次。抽取 61 张 JPEG 均完整解码为 640×480 RGB;没有用灰度或降低色深换帧率。最小内部空闲堆 118771 B;实际 JPEG 档位 18–28,温度 58–60°C,未触发减载。 ## 该测试阶段的配置与时钟 @@ -69,7 +69,7 @@ XCLK 保持 20 MHz,使用完整 PLL 分频组合:VCO 610 MHz、SYSCLK 61 MHz 实机诊断构建主动断开并保持 5 秒:检测断线后 9021 ms 恢复网络,其中重试发起到恢复日志为 4029 ms(含取址和恢复处理);无需重启,客户端随后自动续订恢复视频。该测试使用同一恢复逻辑、60 MHz SYSCLK 的预检构建;最终交付为 61 MHz,故障注入参数为 0。恢复到同一个 IP 已验证,路由器长时间关机、密码改变和 DHCP 换址尚未分别做硬件测试。启动阶段第一次联网仍使用原有扫描和认证模式探测逻辑。 -处理方式遵循 [ESP-IDF 断线事件说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected):有效 IP 恢复后再重建上层 socket。4 项恢复策略测试覆盖认证不被重启、关联/DHCP 超时、指数退避和新断线重置状态。 +处理方式遵循 [ESP-IDF 断线事件说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected):有效 IP 恢复后再重建上层 socket。4 项恢复策略测试覆盖认证阶段不被重启、关联/DHCP 超时、指数退避和新断线重置状态。 ## 延迟测量方法与边界 diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 05bac65..f267ef5 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -1,6 +1,6 @@ # 图传性能排查与验证(2026-10-18) -本文保留早期短时对照;当前交付已回退到 15 dBm,功率、距离与发送拥塞分析见 [POWER.md](POWER.md),阻塞处理见 [REALTIME.md](REALTIME.md)。此前约 30 fps 与回显延迟数据见 [LATENCY.md](LATENCY.md),更早验证见 [STABILITY.md](STABILITY.md)。下列数字对应当时的传感器时序和网络状态。 +本文保留早期短时对照;当前交付已回退到 15 dBm,功率、距离与发送拥塞分析见 [POWER.md](POWER.md),阻塞处理见 [REALTIME.md](REALTIME.md),此前约 30 fps 与回显延迟数据见 [LATENCY.md](LATENCY.md),更早的验证见 [STABILITY.md](STABILITY.md)。下列数字对应当时的传感器时序和网络状态。 ## 环境与结论 @@ -8,7 +8,7 @@ 传感器在 CPU0 和 CPU1 均可输出约 22.2 fps。低帧率期间持续出现 `sendto` 的 `ENOMEM`(errno 12),完整帧提交率低于采集率,客户端收到不完整分片。普通系统内存仍有约 100 KiB 以上可用,证据指向无线发送拥塞与驱动缓冲回压。核绑定和单独扩大 Windows 缓冲均未解决主要瓶颈。 -降低最大发射功率、使用视频队列,并降低单帧大小后有所改善。功率切换对照中,20 dBm 的窗口明显恶化,10 dBm 的两次窗口约 12 fps;14 dBm 的结果仍有波动。仅凭这些数据无法确定供电、天线、射频或 AP 的具体物理原因。 +降低最大发射功率、改用视频队列、减小单帧大小后有所改善。功率切换对照中,20 dBm 的窗口明显恶化,10 dBm 的两次窗口约 12 fps;14 dBm 的结果仍有波动。仅凭这些数据无法确定供电、天线、射频或 AP 的具体物理原因。 ## 接收端对照 @@ -21,7 +21,7 @@ 表中 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,7 +36,7 @@ 约 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 最终绘制像素。 diff --git a/POWER.md b/POWER.md index 2d5bb36..a4cc5e6 100644 --- a/POWER.md +++ b/POWER.md @@ -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 记录说明了什么 @@ -11,9 +11,9 @@ | 17 dBm 第一轮 | 视频 30 s / 串口 75 s | 29.78 fps | 0.667% | 90 / 80 / 10(整段串口) | | 17 dBm 第二轮,增加周期回读 | 视频 30 s / 串口 75 s | 27.92 fps | 6.889% | 498 / 446 / 52(整段串口) | -两轮的发射上限相同,曝光与缓冲参数相同,但无线状态、场景内容和时间并未锁定,且第二轮多了周期功率读数。因此这些记录不构成严格 A/B 对照,不能单凭它们断定“增加 3 dB 必然导致某个百分比的丢帧”。用户反馈是回退依据,日志用于定位失败发生在哪一层。 +两轮的发射上限、曝光与缓冲参数相同,但无线状态、场景内容和时间并未锁定,且第二轮多了周期功率读数。因此这些记录不构成严格 A/B 对照,不能单凭它们断定“增加 3 dB 必然导致某个百分比的丢帧”。用户反馈是回退依据,日志用于定位失败发生在哪一层。 -第二轮内部空闲堆最少 120039 字节、最大连续块最少 43008 字节,温度 57–58°C,无温控限帧、复位或采集错误,只有一个 UDP 订阅者。出现发送失败时伴有 `errno 12 / ENOMEM`,优先指向网络发送资源的瞬时耗尽。堆仍有余量不能排除受配额限制的 Wi-Fi TX 缓冲已满,也不能把这个错误直接当成内存泄漏。 +第二轮内部空闲堆最少 120039 字节、最大连续块最少 43008 字节,温度 57–58°C,无温控限帧、复位或采集错误,只有一个 UDP 订阅者。出现发送失败时伴有 `errno 12 / ENOMEM`,优先指向网络发送资源的瞬时耗尽。堆仍有余量,不能排除受配额限制的 Wi-Fi TX 缓冲已满,也不能把这个错误直接当成内存泄漏。 ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲全部占用而返回 `ENOMEM`。目前日志中的“446 次重试”是**应用调用 sendto 的重试**,不是已测得的 802.11 空口重传次数。来源:[ESP-IDF v5.5.5 lwIP 文档](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/lwip.html#limitations)。 @@ -27,7 +27,7 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲 已确认的应用行为是:每约 33 ms 取得一张 JPEG,再把它拆成多个数据报连续提交,分片间隔为 0;成功返回只表示本地网络栈接受,不能证明 AP 或电脑已经收到。每帧 8 ms 重试预算和 25 ms 持有期限限制应用等待,但不能撤回已经进入 SDK 的旧分片。 -提高发射上限不会增加信道带宽或路由器的排队能力。若空口竞争、速率回退或重传使驱动排空变慢,突发提交仍会填满 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)。 +提高发射上限不会增加信道带宽或路由器的排队能力。若空口竞争、速率回退或重传使驱动排空变慢,突发提交仍会填满 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 记录也不能证明射频供电完全没有瞬态变化。 @@ -39,9 +39,9 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲 ## 当前回退验证与后续定位 -测试期间用户补充观察:用手握持开发板时丢包明显增多。测试没有逐秒标记握持、位置和朝向,五分钟记录应视作现场连续性观察,不能当作固定摆位的功率 A/B 测试。人体靠近 PCB 天线导致的吸收、失谐或遮挡成为更值得优先排查的线索。 +测试期间用户补充观察:用手握持开发板时丢包明显增多。测试没有逐秒标记握持、位置和朝向,五分钟记录应视作现场连续性观察,不能当作固定摆位的功率 A/B 测试。人体靠近 PCB 天线造成的吸收、失谐或遮挡,是更值得优先排查的线索。 -本轮只改变功率上限到 15 dBm,保持其他图传参数与单 UDP 订阅,做五分钟收流观察;结果由交付包 `POWER-RESULT.json` 记录。之前的 17 dBm 记录保留为 `POWER-HISTORY-tx17.json`,14 dBm 的阻塞处理记录保留在 [REALTIME.md](REALTIME.md)。本轮不重新测量最远断连距离或物理屏幕端到端延迟。 +本轮只把功率上限改到 15 dBm,其他图传参数与单 UDP 订阅保持不变,做五分钟收流观察;结果由交付包 `POWER-RESULT.json` 记录。之前的 17 dBm 记录保留为 `POWER-HISTORY-tx17.json`,14 dBm 的阻塞处理记录保留在 [REALTIME.md](REALTIME.md)。本轮不重新测量最远断连距离或物理屏幕端到端延迟。 下一项值得单独对照的是**分片突发整形**:固定 15 dBm、曝光和 JPEG 策略,按发送回压逐步比较小批次提交与短暂让出调度,并同时看原生完整帧率、网络丢帧、主动跳帧和帧龄。不能仅通过增大缓冲或延长重试,把丢帧转化为持续排队延迟。若要确认距离瓶颈,需要同步记录远点断连原因、AP 侧板卡 RSSI、MAC 重传和信道忙占比。 @@ -59,6 +59,6 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲 ## 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.md](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-RESULT.json`。 按时间近似对齐的板端五分钟区间有 731 次发送失败、653 次应用重试、78 次目标帧放弃;温度 56–58°C,RSSI -35 至 -11 dBm。板端区间由五秒采样的结束时间选择,边界并非逐包精确对齐。与此前短测时长、现场无线状态和 JPEG 内容不同,不能将差值归因于功率这一项。 diff --git a/README.md b/README.md index e25fd2b..e45ded1 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,41 @@ -# ESP32-S3 摄像头图传系统 +# RMIC::2026 视野链路系统 -基于 **Rust + ESP-IDF** 的局域网实时图传系统。ESP32-S3 采集摄像头输出的 JPEG,通过 UDP 向桌面客户端推流;桌面端提供队伍端与裁判端界面,并接收 ESP32-C3 装甲板的血量广播。 +黑龙江大学 602 实验室 · Simba Robotics 的机器人对抗赛视野链路。ESP32-S3 采集 OV5640 输出的硬件 JPEG,经 UDP 推给局域网内的桌面端;桌面端 RMIC::2026 提供队伍端与裁判端两套界面,接收 ESP32-C3 装甲板的血量广播,并负责固件维护。 -## 功能与架构 - -- 摄像头自动探测、JPEG 采集、UDP 分片发送与完整帧重组。 -- 局域网广播发现与订阅,单块图传板最多向 4 个客户端发送画面。 -- 运行中 Wi-Fi 断开后自动重连,重新获得 IP 后重建 UDP socket 并恢复订阅。 -- 使用 MAC 派生的稳定设备 ID;两块图传板可烧录同一固件,在客户端绑定队伍。 -- Tauri v2 + React 桌面端:视频显示、双方血量、设备绑定与录制。 -- 独立 ESP32-C3 装甲板固件:四路受击检测、WS2812 灯带及血量上报。 +当前版本 **v0.2.1**(构建号 35,线路协议 1)。仓库已从 `ESP32S3_vt` 改名为 `simba-rmic-2026`,旧地址会重定向到新地址: ```text -ESP32-S3 图传板 A ─┐ ┌─ 队伍客户端 - ├─ 同一局域网 / UDP ─┼─ 裁判客户端 -ESP32-S3 图传板 B ─┤ └─ 队伍客户端 -ESP32-C3 装甲主板 ─┘ +git@git.simba-robotics.com:PRAM/simba-rmic-2026.git ``` -图传使用 UDP **5600**,装甲板血量使用 UDP **5601**。板卡与客户端需在可互相通信、允许广播的网络中;关闭 AP 客户端隔离,并允许客户端程序通过防火墙。 +## 组成与数据流 + +```text +ESP32-S3 图传板 A ─┐ ┌─ 队伍端(绑定本队板卡) + ├─ 同一局域网 / UDP ─┼─ 裁判端(双路画面 + 装甲板表) +ESP32-S3 图传板 B ─┤ └─ 队伍端 +ESP32-C3 装甲板 ───┘ +``` + +视频使用 UDP **5600**,装甲板血量使用 UDP **5601**。板卡与客户端要在可互相通信、允许广播的网络里;关闭 AP 客户端隔离,并允许客户端程序通过防火墙。 + +| 部分 | 作用 | 技术栈 | +| --- | --- | --- | +| 根目录固件 | 图传板:相机采集、JPEG 分片、UDP 发送、发现与订阅、Wi-Fi 重连 | Rust `std` + ESP-IDF | +| `protocol/` | 视频与血量的线路格式、帧重组 | 纯 Rust,主机可测 | +| `desktop/` | RMIC::2026:队伍端 / 裁判端、录制、固件维护 | Tauri v2 + React + Rust | +| `armor/` | 装甲板:四路受击检测、WS2812 灯带、血量上报 | Rust + ESP-IDF(ESP32-C3) | +| `components/` | 本地相机驱动与 C/Rust 接口封装 | C + Rust FFI | + +## 当前状态 + +图传板跑 VGA 640×480 彩色,完整接收约 **30 fps**,发射功率 **15 dBm**,三块 PSRAM 采集缓冲。发送端只保留当前一张完整 JPEG,拥塞时按有限预算重试,过期就转向新帧;原生视频通道和 WebView 都会拒绝滞留超过 100 ms 的旧帧。具体策略和五分钟实测见 [REALTIME.md](REALTIME.md)。 + +桌面端已经换成 Tauri v2 + React,不再是 Slint 客户端。队伍端有装甲条、瞄准器、命中反馈和密码闸门,裁判端有双路画面、可改队名、装甲板发现表、槽位分配和录制回放;两侧共用同一份快照。固件维护页支持选择编译后打出的 `.zip` 固件包,在坏块板上搜索可写槽位并回读校验,不需要重新编译。 + +装甲板是独立的 ESP32-C3 工程:主板负责四路压电片和四路灯带,整车 100 血,每次受击扣 3 点,每 200 ms 广播四条血量。 + +版本号和改动日志都从 git 历史算出。板子每 2 秒广播的 `Hello` 里带着语义化版本、构建号和编译进去的线路协议号;客户端会把它显示在绑定列表和裁判端每一格下面,协议不匹配时直接说明原因,而不是只表现为没有画面。 ## 项目结构 @@ -29,10 +46,11 @@ ESP32-C3 装甲主板 ─┘ | `protocol/` | 共享 UDP 协议与帧重组,支持主机测试 | | `armor/` | 独立 ESP32-C3 装甲板 workspace | | `desktop/` | Tauri + React 客户端及独立网络核心 `core/` | -| `scripts/` | 构建、烧录与探测脚本 | +| `scripts/` | 构建、烧录、打包与探测脚本 | | `reference/armor/` | 早期 C 版装甲板工程 | +| `docs/` | 固件维护、自动烧录评估与界面重构设计稿 | -各子项目的构建目标不同。以下编译、烧录流程针对**根目录的 ESP32-S3 图传固件**。 +各子项目的构建目标不同。下面的编译、烧录流程针对根目录的 ESP32-S3 图传固件。 ## 硬件要求 @@ -46,17 +64,17 @@ ESP32-C3 装甲主板 ─┘ 默认采集 VGA(640 × 480)、彩色 JPEG quality 12,按传感器节拍发送。相机引脚、Flash 分区和 PSRAM 配置见 [HARDWARE.md](HARDWARE.md)。换板型前必须核对接线,避免占用 Flash / PSRAM 引脚。 -## ESP Rust 工具链入门 +## 固件开发环境 -本项目使用 Rust `std` 和 ESP-IDF。ESP32-S3 属于 **Xtensa** 架构,需要通过 `espup` 安装 Espressif Rust 工具链;普通 `rustup target add` 不能替代这一步。根目录固定选择名为 `esp-1.97.0.0` 的工具链,避免浮动版本影响构建。 +本项目使用 Rust `std` 和 ESP-IDF。ESP32-S3 属于 Xtensa 架构,需要通过 `espup` 安装 Espressif Rust 工具链;普通 `rustup target add` 不能替代这一步。根目录固定使用名为 `esp-1.97.0.0` 的工具链,避免浮动版本影响构建。 ### 1. 安装基础依赖与 Rust 工具 先安装 [Rust / rustup](https://rustup.rs/)、[Git](https://git-scm.com/downloads)、[Python 3](https://www.python.org/downloads/)(建议 3.11)、[CMake](https://cmake.org/download/) 与 [Ninja](https://ninja-build.org/),确保命令在 `PATH` 中。 -- **Windows**:使用 PowerShell 7.5 或更新版本,安装 Visual Studio Build Tools 的“使用 C++ 的桌面开发”和 Windows SDK,使用 MSVC Rust 主机工具链。仓库和工具链尽量使用较短、无空格和中文的路径。 -- **Linux(Debian / Ubuntu)**:安装 `build-essential pkg-config libudev-dev libssl-dev python3 python3-venv cmake ninja-build git curl`。 -- **macOS**:安装 Xcode Command Line Tools(`xcode-select --install`),以及 Python、CMake 和 Ninja。 +- Windows:使用 PowerShell 7.5 或更新版本,安装 Visual Studio Build Tools 的“使用 C++ 的桌面开发”和 Windows SDK,使用 MSVC Rust 主机工具链。仓库和工具链尽量使用较短、无空格和中文的路径。 +- Linux(Debian / Ubuntu):安装 `build-essential pkg-config libudev-dev libssl-dev python3 python3-venv cmake ninja-build git curl`。 +- macOS:安装 Xcode Command Line Tools(`xcode-select --install`),以及 Python、CMake 和 Ninja。 以下命令显式使用 `+stable`,即使在仓库内执行,也不会要求事先已有 `esp`: @@ -89,7 +107,7 @@ espflash --version ### 2. 准备 ESP-IDF -默认使用 **ESP-IDF v5.5.5**。首次构建时,`esp-idf-sys` 下载 SDK、对应 C 工具链及 Python 依赖,默认缓存到用户目录的 `.espressif/`;需要网络、磁盘空间及缓存目录写入权限。 +默认使用 ESP-IDF v5.5.5。首次构建时,`esp-idf-sys` 下载 SDK、对应 C 工具链及 Python 依赖,默认缓存到用户目录的 `.espressif/`;需要网络、磁盘空间及缓存目录写入权限。 已有匹配版本 SDK 时可在终端设置 `IDF_PATH`,不要提交个人绝对路径: @@ -142,6 +160,8 @@ WIFI_PASS='你的Wi-Fi密码' #CAM_THERMAL_LIMIT_C=75 #CAM_VSYNC_INVERT=1 #WIFI_AUTH=wpa2 +#WIFI_TX_POWER_DBM=15 +#WIFI_TEST_DROP_AFTER_S=0 ``` | 变量 | 默认值 | 说明 | @@ -149,7 +169,7 @@ WIFI_PASS='你的Wi-Fi密码' | `WIFI_SSID` | 无,必填 | 2.4 GHz 网络名称,最多 32 字节 | | `WIFI_PASS` | 空字符串 | Wi-Fi 密码;开放网络可留空 | | `CAM_RES` | `vga` | `qqvga` / `qvga` / `cif` / `hvga` / `vga` / `svga` / `xga` / `sxga` / `uxga` | -| `CAM_QUALITY` | `12` | 0–63,越小通常画质越好、JPEG 越大 | +| `CAM_QUALITY` | `12` | 0–63,越小通常画质越好、JPEG 越大;当前交付使用 18 | | `CAM_QUALITY_MAX` | `0` | `0` 固定画质;设为 `CAM_QUALITY`–63 时按发送回压在两者之间调节压缩,保留彩色和分辨率。拥塞时每 5 秒增大最多 3 档,连续稳定 15 秒才恢复 1 档 | | `CAM_MAX_FPS` | `0` | 取帧与发送的最高节拍,1–60;`0` 不限帧。仅限制消费节拍,不修改传感器时钟 | | `CAM_CLOCK_PROFILE` | `default` | `default` 保留驱动时钟;`vga30` 使用 OV5640 VGA 约 30.09 fps PLL 配置,保持 HTS/VTS 和读出窗口,SYSCLK 61 MHz / PCLK 15.25 MHz。切换后需重新核对曝光行数 | @@ -165,8 +185,9 @@ WIFI_PASS='你的Wi-Fi密码' | `UDP_RETRY_BUDGET_US` | `8000` | 每帧、所有分片及订阅者共用的让出调度重试预算,0–25000 μs,须为 tick 整数倍;`0` 为非阻塞失败即弃。总发送检查期限为 25 ms,采集帧龄期限为 100 ms;分片间隔也计入期限,不会额外延长 | | `CAM_THERMAL_LIMIT_C` | `75` | 芯片内部温度达到该值时减半帧率(向上取整),下降 5°C 后恢复;40–95,`0` 禁用。原先不限帧时降至 8 fps | | `CAM_VSYNC_INVERT` | `1` | 驱动反相 VSYNC;硬件已反相时填 `0` | -| `WIFI_AUTH` | 自动选择 | `none` / `wep` / `wpa` / `wpa2` / `wpawpa2` / `wpa3` / `wpa2wpa3` / `wapi` | +| `WIFI_AUTH` | 自动选择 | `none` / `wep` / `wpa` / `wpa2` / `wpawpa2` / `wpa3` / `wpa2wpa3` / `wapi`;只在扫描看不到 AP(如隐藏 SSID)时才需要 | | `WIFI_TX_POWER_DBM` | SDK 默认上限 | 可选整数 2–20 dBm;当前交付请求 15 dBm,本板驱动回读 15.00 dBm;启动及重连时应用,拥塞和温控逻辑不降低此上限 | +| `WIFI_TEST_DROP_AFTER_S` | `0` | 仅诊断构建:运行指定秒数后主动断网一次并保持 5 秒,用来验证运行时恢复。正式构建保持 `0` | `.env` 使用简单的 `KEY=value`;值含空格、`#` 或 `$` 时用单引号包围,不使用变量展开或命令替换。脚本会加载 `.env`,直接 `cargo build` 不会自动读取它。上述参数在编译时写入固件,修改后需要重新编译和烧录。`.env` 已被 Git 忽略;固件也包含凭据,分享前应重新配置。 @@ -187,7 +208,7 @@ $env:CARGO_TARGET_DIR = 'C:\s3vt' ./scripts/build.sh ``` -脚本执行 `cargo build --release --locked`,目标为 `xtensa-esp32s3-espidf`。首次编译包含 Rust 标准库、ESP-IDF 和相机组件,耗时较长。 +脚本执行 `cargo build --release --locked`,目标为 `xtensa-esp32s3-espidf`。首次编译包含 Rust 标准库、ESP-IDF 和相机组件,耗时较长。构建完成后可用 `-Package` / `--package` 再打一个 `.zip` 固件包,供桌面端维护页使用。 Windows 的 ESP-IDF 构建输出路径需不超过 88 字符,因此示例使用短目录 `C:\s3vt`(需可写,且不与其他项目共用)。`subst` 不能绕过该检查。下表的 `` 在 Windows 示例中为 `C:/s3vt`,Linux / macOS 默认是仓库的 `target`。每次新开 Windows 终端都要设置相同的 `CARGO_TARGET_DIR`。 @@ -224,7 +245,7 @@ target_dir="${CARGO_TARGET_DIR:-target}" espflash flash --chip esp32s3 --port /dev/ttyACM0 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$target_dir/xtensa-esp32s3-espidf/release/bootloader.bin" --monitor "$target_dir/xtensa-esp32s3-espidf/release/esp32" ``` -连接失败时按住 **BOOT**,点按 **RESET / EN**,松开 BOOT 后重试。正常启动应看到相机识别、Wi-Fi 获取 IP 和 `ESP32 camera is live` 日志。没有客户端时,`0 clients subscribed` 属于正常状态。 +连接失败时按住 BOOT,点按 RESET / EN,松开 BOOT 后重试。正常启动应看到相机识别、Wi-Fi 获取 IP 和 `ESP32 camera is live` 日志。没有客户端时,`0 clients subscribed` 属于正常状态。 仅查看日志: @@ -236,11 +257,11 @@ espflash monitor --port COM5 `espflash save-image --merge` 可导出从 `0x0` 写入的合并镜像。完整命令、独立镜像偏移与测试方法见 [DEVELOPMENT.md](DEVELOPMENT.md)。 -保留的 `scripts/flash.sh` 针对**特定坏 Flash 模组**,依赖 macOS 环境,会修改分区并重编译。普通板卡使用上述标准流程;坏板限制见 [HARDWARE.md](HARDWARE.md#损坏-flash-模组的兼容处理)。 +保留的 `scripts/flash.sh` 针对特定坏 Flash 模组,依赖 macOS 环境,会修改分区并重编译。普通板卡使用上述标准流程;坏板限制见 [HARDWARE.md](HARDWARE.md#损坏-flash-模组的兼容处理)。 -## 客户端与装甲板 +## 桌面端 RMIC::2026 -桌面端入口如下。安装依赖、主机 target 设置与功能说明见 [desktop/README.md](desktop/README.md): +安装依赖并启动开发模式: ```text cd desktop @@ -248,7 +269,26 @@ pnpm install pnpm app ``` -装甲板是独立的 **ESP32-C3 / RISC-V** 工程,不能使用 S3 的 ELF。`ARMOR_TEAM=0/1` 在编译期决定红 / 蓝队伍。接线、规则与构建烧录见 [armor/README.md](armor/README.md)。 +`pnpm app` 等价于 `tauri dev`:起 Vite、编 Rust、开窗口。只构建前端用 `pnpm build`,打包用 `pnpm tauri build`。Windows x64 需要 Node.js 22.12+、pnpm、Rust stable MSVC、Visual Studio Build Tools 的 C++ 组件和 WebView2 Runtime;安装包用 NSIS,产物在 `src-tauri/target/x86_64-pc-windows-msvc/release/`。 + +没有板子也能跑演示模式: + +```text +cd desktop +CAM_DEMO=1 pnpm app +``` + +演示模式合成协议数据报,从真实的接收与重组路径进入界面,不是给界面塞假数据。界面配置、密码、绑定、录制、裁判端槽位、防作弊和固件维护的细节见 [desktop/README.md](desktop/README.md)。固件维护页的实现、`.zip` 包格式与实机验证范围见 [docs/firmware-maintenance.md](docs/firmware-maintenance.md)。 + +## 装甲板 + +装甲板是独立的 ESP32-C3 / RISC-V 工程,不能使用 S3 的 ELF。`ARMOR_TEAM=0/1` 在编译期决定红 / 蓝队伍。接线、规则与构建烧录见 [armor/README.md](armor/README.md)。 + +```bash +scripts/build_armor.sh # 编译 +scripts/flash_armor.sh # 烧录并看 20 秒日志 +python3 scripts/armor_probe.py 15 # 不打开界面时核对血量广播 +``` ## 常见问题 @@ -263,6 +303,7 @@ pnpm app | 相机无输出 / 初始化失败 | 核对板型接线、供电与 Octal PSRAM | | `NO-EOI`、几帧后停止 | 确认 `CONFIG_CAMERA_PSRAM_DMA=n` | | 在线但没有画面 | 检查客户端订阅、UDP 5600、防火墙及 AP 隔离 | +| 界面提示协议不匹配 | 板子与客户端的线路协议号不同;按提示重新烧录同版本的固件 | | 开启 VPN / 虚拟网卡后设备在线但无画面 | 使用支持按已发现设备 IP 定向续订的客户端;仅发送 `255.255.255.255` 广播可能走错网卡 | | 改 C 组件后无变化 | 清理 `esp-idf-sys` 构建缓存后重编,见开发说明 | | 主机项目被编为 Xtensa | 显式指定宿主 target,见各子项目说明 | @@ -277,7 +318,7 @@ python scripts/perf_probe.py 192.168.1.139 --seconds 30 --json perf.json 脚本定向续订,支持保留 VPN / TUN 的环境。输出包含完整帧率、帧损失率、接收间隔和组装耗时;这些时间不等于摄像头到屏幕的端到端延迟。 -运动图传保持 `CAM_GRAYSCALE=0`,优先使用自动曝光上限和自动增益协同控制拖影。室内强制 −1 EV / 固定短快门在本机过暗,已撤回;提高增益上限也会增加噪点。需要限流时可设置 `CAM_MAX_FPS=15`,追求帧率时使用 `0`;可启用有界 JPEG 压缩调节应对发送回压,分片间隔按网络实测选择。灰度效果仍保留硬件 JPEG,不是改为 RGB565 或降低 JPEG 采样位数。75°C 是主动减载阈值,温度来自 ESP 芯片内部,不能代表摄像头或环境温度,也不是芯片损坏温度规格。五分钟彩色对照与此前中止的灰度长测见 [STABILITY.md](STABILITY.md)。 +运动图传保持 `CAM_GRAYSCALE=0`,优先使用自动曝光上限和自动增益协同控制拖影。室内强制 −1 EV / 固定短快门在本机过暗,已撤回;提高增益上限也会增加噪点。需要限流时设 `CAM_MAX_FPS=15`,追求帧率时用 `0`;可启用有界 JPEG 压缩调节应对发送回压,分片间隔按网络实测选择。灰度效果仍保留硬件 JPEG,不是改为 RGB565 或降低 JPEG 采样位数。75°C 是主动减载阈值,温度来自 ESP 芯片内部,不能代表摄像头或环境温度,也不是芯片损坏温度规格。五分钟彩色对照与此前中止的灰度长测见 [STABILITY.md](STABILITY.md)。 ## 版本号与改动日志 @@ -288,29 +329,29 @@ node scripts/version.mjs --write # 计算并写回 version.json / CHANGELOG / node scripts/version.mjs --check # 只校验(CI 与发布前用) ``` -- 版本号由提交类型决定:`BREAKING CHANGE:` → major,`feat` → minor,`fix`/`perf` → patch。 +- 版本号由提交类型决定:`BREAKING CHANGE:` → major,`feat` → minor,`fix` / `perf` → patch。 - 构建号 = `git rev-list --count HEAD`,单调、不撞车。 -- 同一次计算还产出 `version.json`(固件 `build.rs` 读它)、`CHANGELOG.json`、 - `desktop/src/lib/version.ts`。 +- 同一次计算还产出 `version.json`(固件 `build.rs` 读它)、`CHANGELOG.json`、`desktop/src/lib/version.ts`。 -**固件与客户端的版本是联动过的**:板子每 2 秒广播的 `Hello` 里带着语义化版本、构建号与 -它编译进去的线路协议号;客户端读出来显示在设置与裁判端每一格下面,协议对不上时直接说明 -原因("板子是协议 2,客户端是协议 1,画面会被丢弃,请重新烧录"),而不是只表现为没有画面。 +固件与客户端的版本是联动的:板子每 2 秒广播的 `Hello` 里带着语义化版本、构建号与它编译进去的线路协议号;客户端读出来显示在设置与裁判端每一格下面,协议对不上时直接说明原因(“板子是协议 2,客户端是协议 1,画面会被丢弃,请重新烧录”),而不是只表现为没有画面。 + +细节见 [desktop/README.md](desktop/README.md#版本与改动日志算出来的不是写出来的) 与 [protocol/README.md](protocol/README.md#通告里的版本信息)。 -细节见 [`desktop/README.md`](desktop/README.md#版本与改动日志算出来的不是写出来的) 与 -[`protocol/README.md`](protocol/README.md#通告里的版本信息)。 ## 文档索引 -- [CHANGELOG.md](CHANGELOG.md):改动记录与历史修复。 +- [CHANGELOG.md](CHANGELOG.md):改动记录,由脚本从 git 历史生成。 - [HARDWARE.md](HARDWARE.md):接线、内存、Flash 与驱动限制。 - [DEVELOPMENT.md](DEVELOPMENT.md):镜像导出、测试、协议与性能测量。 - [PERFORMANCE.md](PERFORMANCE.md):保留 TUN 的图传排查、参数取舍与实际测试结果。 - [STABILITY.md](STABILITY.md):限帧、灰度与温度减载,长时间运行测试。 - [REALTIME.md](REALTIME.md):阻塞时取最新帧、超期丢弃和客户端积压处理。 - [POWER.md](POWER.md):15 dBm 回退、功率与距离的关系,以及发送拥塞证据。 -- [LATENCY.md](LATENCY.md):当前约 30 fps 配置、Wi-Fi 重连及实际画面链路延迟对照。 +- [LATENCY.md](LATENCY.md):约 30 fps 配置、Wi-Fi 重连及实际画面链路延迟对照。 +- [FLASH-CLIENT-ARCH.md](FLASH-CLIENT-ARCH.md):烧录 / 维护客户端的架构提议稿。 - [protocol/README.md](protocol/README.md):UDP 线路格式及帧重组。 - [armor/README.md](armor/README.md):装甲板固件与规则。 -- [desktop/README.md](desktop/README.md):桌面端配置、录制与排查。 -- [docs/firmware-provisioning-evaluation.md](docs/firmware-provisioning-evaluation.md):自动烧录、坏区规避与 SSID 配置 patch 的工具评估、MVE 与测试矩阵。 +- [desktop/README.md](desktop/README.md):RMIC::2026 的配置、录制与排查。 - [docs/firmware-maintenance.md](docs/firmware-maintenance.md):已实现的固件维护页、官方 bundle 格式、坏块规避流程与实机验证记录。 +- [docs/firmware-provisioning-evaluation.md](docs/firmware-provisioning-evaluation.md):自动烧录、坏区规避与 SSID 配置 patch 的工具评估、MVE 与测试矩阵。 +- [docs/desktop-ui-redesign.md](docs/desktop-ui-redesign.md):RMIC::2026 界面、比赛控制与直播包装的重构设计稿。 +- [docs/CHANGELOG-legacy.md](docs/CHANGELOG-legacy.md):建立自动版本机制之前的人工改动记录。 diff --git a/REALTIME.md b/REALTIME.md index 7ab0e50..2c85d73 100644 --- a/REALTIME.md +++ b/REALTIME.md @@ -2,13 +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.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.md) 和制品 `POWER-RESULT.json`。 -保留已确认可用的三块 PSRAM 采集缓冲、OV5640 VGA 彩色、约 30 fps PLL 时钟、AE 目标 45/55、约 20 ms 曝光上限及自动增益;Wi-Fi 自动重连和 TUN 兼容订阅继续保留。没有增加待发送 JPEG FIFO,也没有为了消除队列改成单块 DMA 缓冲。 +沿用已确认可用的三块 PSRAM 采集缓冲、OV5640 VGA 彩色、约 30 fps PLL 时钟、AE 目标 45/55、约 20 ms 曝光上限及自动增益;Wi-Fi 自动重连和 TUN 兼容订阅保持不变。没有增加待发送 JPEG FIFO,也没有为了消除队列改成单块 DMA 缓冲。 发送程序只持有当前一张完整 JPEG;阻塞恢复后通过有限次数的零等待取帧选择最新有效帧,旧帧立即归还驱动。三块采集内存用于捕获余量,与必须按序播放的三帧队列不同。已有相机队列仍然有限,应用交接会跳过其旧帧。 -完整 JPEG 取得后按 1400 字节载荷分片立即提交,复用一个数据报缓冲,直接读取 PSRAM。当前传感器驱动在 JPEG 结束后交出完整帧,本次没有修改为 DMA 尚未完成就读取其内容;不完整 JPEG 由接收端整帧丢弃,不进入绘制。 +完整 JPEG 取得后按 1400 字节载荷分片立即提交,复用一个数据报缓冲,直接读取 PSRAM。当前传感器驱动在 JPEG 结束后交出完整帧,本次没有改成在 DMA 尚未完成时就读取其内容;不完整 JPEG 由接收端整帧丢弃,不进入绘制。 ## 阻塞处理 @@ -31,9 +31,9 @@ UDP_RETRY_BUDGET_US=8000 WIFI_TX_POWER_DBM=15 ``` -当前交付根据用户反馈回退到 15 dBm,实板驱动回读 15.00 dBm。启动和重连应用同一功率上限,拥塞和温控逻辑不自动降功率;此前 17 dBm 的数据作为历史记录保留,见 [POWER.md](POWER.md)。 +当前交付根据用户反馈回退到 15 dBm。启动和重连应用同一功率上限,拥塞和温控逻辑不自动降功率;此前 17 dBm 的数据作为历史记录保留,见 [POWER.md](POWER.md)。 -若要在发送回压时立即放弃本帧,可将 `UDP_RETRY_BUDGET_US=0` 后重编烧录。该模式仍保留三块采集内存和完整 JPEG 校验;含义是零让出重试,不是删除驱动和路由器中的所有缓冲。零重试策略已经主机测试,本次五分钟实机使用默认 8000 μs。 +若要在发送回压时立即放弃本帧,可将 `UDP_RETRY_BUDGET_US=0` 后重编烧录。该模式仍保留三块采集内存和完整 JPEG 校验;这里的“零”指不让出调度重试,不是删除驱动和路由器中的所有缓冲。零重试策略已经主机测试,本次五分钟实机使用默认 8000 μs。 分片间隔也计入 25 ms 检查期限,不再额外延长帧的发送寿命;设置过大的间隔可能使整帧无法在期限内提交。分片在 SDK socket 中已经接受后无法由本策略撤回,底层调度和网络排队也无法由应用层强制抢占。以上为应用层新鲜度策略,不能保证每帧、无线链路或物理屏幕具有硬实时上限。 diff --git a/STABILITY.md b/STABILITY.md index 566977d..215a7cc 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -30,13 +30,13 @@ UDP_PACKET_GAP_US=0 OV5640 驱动的 AE 档位不是 EV 数值:名义零档目标为 55,档位 −3 的目标为 25,目标比值约为 −1.14 EV;档位 −4 的目标为 15,约为 −1.87 EV。固件的 `CAM_EXPOSURE_EV=-1/-2` 使用这两个近似档位;这不是经过照度标定的绝对 EV。 -早期仅降低 AE 目标后,实际曝光仍为 885 行;固定 220 行约为 10 ms,但用户反馈太暗,固定约 20 ms 也仍然太暗,均已撤回。其他时钟或分辨率下,行数对应的时间不同,不能直接套用行数。 +早期仅降低 AE 目标后,实际曝光仍为 885 行;固定 220 行约为 10 ms,用户反馈太暗;固定约 20 ms 仍然太暗,均已撤回。其他时钟或分辨率下,行数对应的时间不同,不能直接套用行数。 该阶段采用正常亮度目标、自动 AEC / AGC、440 行曝光上限,关闭夜景延帧,并按现有 50/60 Hz band step 同步约束最大 band 数。自动增益上限由原来的 15.5× 扩至 31.94×,给短曝光补偿空间;增益会放大噪点。预检实际曝光为 295 行,按 SYSCLK 45 MHz / HTS 2060 估算约 13.5 ms,约为早期 885 行曝光的三分之一。440 行上限约 20.1 ms,实际值由场景与频闪步长决定。 用户已确认新配置**亮度够用、颜色稳定**。它仍使用传感器硬件彩色 JPEG,不通过降低色深来提帧。 -彩色 JPEG 抽帧已解码为 640×480 RGB,仍使用传感器硬件 JPEG 编码。没有用灰度换取本轮帧率。曝光行数日志用于验证快门配置,未进行运动靶拖影或照度标定测量。驱动依据:[Espressif OV5640 源码](https://github.com/espressif/esp32-camera/blob/master/sensors/ov5640.c)。 +彩色 JPEG 抽帧已解码为 640×480 RGB。曝光行数日志用于验证快门配置,未进行运动靶拖影或照度标定测量。驱动依据:[Espressif OV5640 源码](https://github.com/espressif/esp32-camera/blob/master/sensors/ov5640.c)。 ## 最终配置的五分钟验证 @@ -58,7 +58,7 @@ OV5640 驱动的 AE 档位不是 EV 数值:名义零档目标为 55,档位 这次完整接收帧率接近当前可靠彩色时序的约 22.2 fps 上限。亮度、画面内容、JPEG 大小和网络负载在各轮之间变化,不能把全部差异归因于单一控制参数,也不能将五分钟结果作为长期或所有路由器的保证。帧间隔不是摄像头到屏幕的光学延迟。 -本轮固件 SHA-256 为 `e297069e190dcccefbae6d95591703c6848f4e523f910e2c0cb44af91f698936`,当时与交付及板上镜像一致;最新交付见 LATENCY.md。配套 Bootloader、分区表及 App 均已独立读回校验。曲线和完整数据位于制品包内的 `STABILITY.png` / `SCHEDULING-RESULT.json`,不包含相机抽帧或令牌日志。 +本轮固件 SHA-256 为 `e297069e190dcccefbae6d95591703c6848f4e523f910e2c0cb44af91f698936`,当时与交付及板上镜像一致;当前交付以 [README.md](README.md) 和交付包的 `BUILD-INFO.json` 为准。配套 Bootloader、分区表及 App 均已独立读回校验。曲线和完整数据位于制品包内的 `STABILITY.png` / `SCHEDULING-RESULT.json`,不包含相机抽帧或令牌日志。 ## 较早的五分钟彩色基线 @@ -81,7 +81,7 @@ OV5640 驱动的 AE 档位不是 EV 数值:名义零档目标为 55,档位 1000 μs 最小分片间隔轮次在设备运行约 235 秒时出现串口 I/O 错误,随后系统不再枚举开发板串口,摄像头同时离线。观察器提前停止,未完成有效的五分钟对照,断连尾段不能归因于调度。 -断连前 3 个完整在线窗口合计 181.192 秒,接收 21.58 fps、帧损失 1.96%,本地通道 17.25 fps,没有显示出逐包间隔的收益。它可能影响 AMPDU 聚合效率,但未抓取空口包,不能确认唯一原因。保留突发发送作为已验证基线,不采用这轮的完整汇总作性能结论。 +断连前 3 个完整在线窗口合计 181.192 秒,接收 21.58 fps、帧损失 1.96%,本地通道 17.25 fps,没有显示出逐包间隔的收益。这个间隔可能影响 AMPDU 聚合效率,但未抓取空口包,不能确认唯一原因。保留突发发送作为已验证基线,不采用这轮的完整汇总作性能结论。 ## 已撤回的行时序提帧试验 @@ -93,7 +93,7 @@ USB 恢复后,HTS 1500 的板上采集探针达到 30.49 fps,但白光环境 ## 室内亮度回归 -用户确认 −1 EV / 固定 10 ms,以及恢复名义 0 EV / 固定约 20 ms 后仍然太暗。因此最终方案不再固定短快门,而是让 AEC / AGC 自动联动,并以曝光上限控制拖影。手册说明 AGC 通常在 AEC 到达上限后继续增加;仅保留 AGC 开关不能当作亮度已经补足的证据。需要结合实际曝光、增益和画面验证。 +用户确认,−1 EV / 固定 10 ms,以及恢复名义 0 EV 后固定约 20 ms,都仍然太暗。因此最终方案不再固定短快门,而是让 AEC / AGC 自动联动,并以曝光上限控制拖影。手册说明 AGC 通常在 AEC 到达上限后继续增加;仅保留 AGC 开关不能当作亮度已经补足的证据。需要结合实际曝光、增益和画面验证。 固定 20 ms 的完整五分钟轮次接收 5220 帧、丢帧 1430(21.50%),接收 17.40 fps,本地 JPEG 通道 13.45 fps,接收间隔 P95 277.473 ms、最大 1163.792 ms;全程在线、无采集错误。它保留为拥塞对照,未被选为最终配置。 diff --git a/armor/README.md b/armor/README.md index 3b1beaa..4efc01a 100644 --- a/armor/README.md +++ b/armor/README.md @@ -1,8 +1,8 @@ -# 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与裁判终端联动 +# 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与 RMIC::2026 桌面端联动 一辆车四块装甲板(前/后/左/右),**只有主板一颗 MCU**:另外三块"从"板里没有单片机, 各自只有一路 WS2812 和一路压电片,直接接线到主板上。主板负责四路击打检测、四路灯带、 -整车血量,并把血量广播给本项目裁判终端。 +整车血量,并把血量广播给 RMIC::2026 桌面端。 固件是跑在 ESP-IDF 上的 Rust,目标芯片 **ESP32-C3(RISC-V)**。 @@ -28,10 +28,10 @@ armor/ | 功能 | GPIO | 说明 | | --- | --- | --- | -| WS2812 前 | **IO10** | 每条灯带 3 颗灯珠,GRB、MSB first | -| WS2812 后 | **IO5** | | -| WS2812 左 | **IO6** | | -| WS2812 右 | **IO7** | | +| WS2812 前 | IO10 | 每条灯带 3 颗灯珠,GRB、MSB first | +| WS2812 后 | IO5 | | +| WS2812 左 | IO6 | | +| WS2812 右 | IO7 | | | 压电片 前 | GPIO0(ADC1_CH0) | 0 dB 衰减、12 bit | | 压电片 后 | GPIO4(ADC1_CH4) | | | 压电片 左 | GPIO3(ADC1_CH3) | | @@ -62,8 +62,8 @@ armor/ | 复位(新一局) | 长按 BOOT ≥500 ms → 满血、清计数 | `Button` + `Armor::reset` | | 打印状态 | 短按 BOOT → 立刻打一行状态并重发上报 | `ButtonEvent::Click` | -**整车一条血量,四格同值。** 板子算账、裁判端只显示:每条上报的 `hp` 相同, -`index` 分别是 0..3,所以裁判端已经画好的四格血量界面不用改。 +**整车一条血量。** 板子算账、裁判端只显示:四条上报的 `hp` 相同,`index` 分别是 0..3, +界面按 `(team, index)` 收齐四条后显示同一个数值。 **为什么要"每板独立冷却"。** 参考实现用全局 50 ms 冷却:一块板挨打后,别块板在这 50 ms 内的击打会被吃掉。四路各自独立才是"每面各自挨打"的正确语义,这里有单元测试 @@ -82,25 +82,23 @@ magic "MVC1"(4) | type(1)=5 | version(2) | sender_id(4) | ... | team(1) | index( - `sender_id` 用 MAC 派生的板子 id(`protocol::id_from_mac`),不是 0 —— 0 是裁判 终端的保留 id,抓包时一块装甲板发的包不该长得像裁判端发的。 - 四条 `index` 的 `hp` 相同(整车一条血),裁判端按 `(team, index)` 收齐四条才显示。 -- 广播而不是单播:裁判终端的 IP 是 DHCP 给的,会变。图传板用 Hello 广播绕开这件事, +- 广播而不是单播:桌面端电脑的 IP 是 DHCP 给的,会变。图传板用 Hello 广播绕开这件事, 装甲板沿用同一个约定。 -### 裁判端怎么收 +### 桌面端怎么收 -`client/src/armor.rs` 里的 `ArmorHealthStore`(裁判端角色才启动): +`desktop/core/src/armor.rs` 里的 `ArmorHealthStore`(两种身份都启动): - 绑 `0.0.0.0:5601` 收广播,按 `(team, index)` 存最后一次读数。 - **四条都在 1.5 s 内收到**才交给界面显示;否则回退占位符。心跳 200 ms,1.5 s 是连丢 - 7 个包,偶发丢包不会让四个格子各自跳。 -- 队伍端不用它(界面不显示对手血量),所以那台机器不会去占 5601。 -- 血量变化会打日志(`armor: team 0 hp 100 -> 97`):窗口是给别人看的,抓日志的人 - 也得能看见掉血。 + 7 个包,偶发丢包不会让读数来回跳。 +- 用户端也要显示双方血条,所以两端都会占 5601。 +- 血量变化会打日志(`armor: board 0x… team 0 hp 100 -> 97`):窗口是给别人看的, + 抓日志的人也得能看见掉血。 -**裁判端界面原本看不到血量。** 协议与 `ArmorSource` 早已就位,但 Slint 里那 4 格只是 -绿/灰方块 —— `hps` 的数值从没被渲染,于是"板子被打到 0"在界面上完全看不出来(现场 -表现就是"敲死了裁判系统没掉血")。现在 `TeamHealth` 多了一个 `hp-text` 与 `critical`: -队伍名旁边直接显示血量数字,低于 30 或归零时数字与血条转红。归零**不写"已击毁"**——那一刻 -图传中央会给出「胜利 / 击败」的结论(见 [`desktop/README.md`](../desktop/README.md))。 +**界面显示血量数字。** `TeamHealthDto` 带 `hp` 与 `critical`:数值就在队伍名旁边,低于 +30 或归零时它与血条一起转红。归零**不写"已击毁"**——那一刻图传中央会给出 +「胜利 / 击败」的结论(见 [`desktop/README.md`](../desktop/README.md))。 ## 构建 @@ -157,7 +155,9 @@ cd protocol && cargo test # 线路格式往返(含 ArmorHealth ``` (`armor-protocol/.cargo/config.toml` 把 target 覆盖回 host:它坐在 `armor/` 目录树 -里,否则会继承 RISC-V 目标,链接必然失败。这与 `protocol/`、`client/` 的做法一致。) +里,否则会继承 RISC-V 目标,链接必然失败。这与 `protocol/`、`desktop/` 的做法一致。 +换到其他宿主平台时,配置里钉住的 `aarch64-apple-darwin` 不适用,按 +[DEVELOPMENT.md](../DEVELOPMENT.md) 用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。) ## 烧录与实测 @@ -208,7 +208,7 @@ W (306504) armor: BOOT long press: hp reset to 100 (new round) | 启动 | 无 panic/重启;状态行每秒一条 | | WiFi | 关联 `SR-602-2G4`(rssi −36,ch 6),DHCP 拿到 `192.168.1.117` | | 上报 | `scripts/armor_probe.py` 收到队伍一四条、约 5 包/秒、`sender_id 0xA820D321`、四格同值 | -| 裁判端 | `cam-client --role referee` 日志 `team 0 plate 0..3 is online (hp 100) via 192.168.1.117`,界面显示血量数字 | +| 裁判端 | 旧客户端 `cam-client --role referee` 日志 `team 0 plate 0..3 is online (hp 100) via 192.168.1.117`,界面显示血量数字 | | 受击 | 敲前装甲板:`hit plate=front raw=2663..4095`,每次 −3(100→97→94…),只有 front 计数 | | 变暗 | 敲击瞬间**只有前那一路**全黑 50 ms,其余不变(现场确认) | | 死亡 | 34 击到 0;状态行 `dead=true strips[front=off rear=off left=off right=off]`,四路全黑 | @@ -218,10 +218,9 @@ W (306504) armor: BOOT long press: hp reset to 100 (new round) ### 裁判端这边踩到的坑:界面根本不显示血量 -协议、`ArmorSource`、`ArmorHealthStore` 都对,但改之前 Slint 里那 4 格只是绿/灰方块, -`hps` 的**数值从不渲染**——板子被打到 0,界面四格还是绿的,看起来像"没掉血"。现在 -队伍名旁边直接显示数字(低于 30 转红,归零不写"已击毁",改由图传中央的胜负横幅表达), -并且客户端日志会打 `armor: team 0 hp 100 -> 97`。 +协议、`ArmorSource`、`ArmorHealthStore` 都对,但改之前旧界面上那几格只是绿/灰方块, +`hps` 的**数值从不渲染**——板子被打到 0,格子还是绿的,看起来像"没掉血"。修好之后 +队伍名旁边直接显示数字,客户端日志也会打 `armor: board 0x… team 0 hp 100 -> 97`。 ## 实现要点(都是踩过的坑) @@ -333,7 +332,7 @@ status: hp=0 hits=37 dead=true plates[front=37 rear=0 left=0 right=0] | 受击变暗 | 四条灯带一起全黑 50 ms | 只暗被打的那一路 | 每面独立灯带的意义就在这里 | | 冷却窗口 | 全局 50 ms | 每板独立 50 ms | 别块板的击打不该被吃掉 | | 灯带刷新 | 每 10 ms 无条件刷四遍 | 只在内容变化时刷 + 每秒强制全刷 | 四路独立控制后,省掉四轮 RMT 开销 | -| 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端已画好的四格血量对齐 | +| 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端界面的血量显示对齐 | | 颜色切换 | BOOT 短按切红/蓝,存 NVS | 颜色由 `ARMOR_TEAM` 决定(红/蓝),短按打状态 | 队别不该靠现场按键切错 | | 按键 | 移植 OneButton 状态机(短按配对,长按切色) | 本地 FSM:长按复位血量、短按打状态 | 只有两种动作,且要能在主机上测 | | 上报 sender_id | —(不走 UDP) | MAC 派生 id | 0 是裁判终端保留 id | @@ -349,4 +348,4 @@ status: hp=0 hits=37 dead=true plates[front=37 rear=0 left=0 right=0] `armor-protocol` 里已经预留了板位枚举与上报格式,加一个 `slave` bin 即可。 - **双击 / 多击**:`Button` 只实现短按与长按,参考实现里未使用的 OneButton 功能没有移植。 - **下发血量**:当前板子本地算账(裁判端只显示)。若以后要裁判端权威(下发血量/复位), - 需要新增一条下行消息并在 `client` 里发。 + 需要新增一条下行消息并在 `desktop` 里发。 diff --git a/desktop/README.md b/desktop/README.md index 8ff9056..2ee8591 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -1,15 +1,15 @@ # 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` 目录下——这是**故意的**, @@ -18,7 +18,7 @@ ```text desktop/ core/ cam-core:不含任何界面与 Tauri 的东西(83 个测试) - src-tauri/ Tauri 后端:推流、命令、录制(32 个测试) + src-tauri/ Tauri 后端:推流、命令、录制(40 个测试) src/ React 前端:两套页面 + HUD src/lib/ IPC 封装、WebSocket 客户端、状态存储、格式化、准星绘制、界面小节拍 src/components/ 取景器、装甲条、命中、结算、装甲板表、队名、录制、设置抽屉、启动序列、门、提示 @@ -30,8 +30,6 @@ desktop/ ## 界面:一套终端语言 -这一栏不是装饰说明,而是"改界面之前该知道的几件事"。 - ### 配色:墨黑蓝打底,只有一支红 | 角色 | 值 | 用在哪 | @@ -43,14 +41,14 @@ desktop/ | 队伍 | `--team-0 #ff6b5e` / `--team-1 #4a9bff` | **硬件约束**:装甲板 LED 就是红与蓝 | 一条硬规则:**红只给必须被看见的东西**。开关合上是白的、滑杆填充是钢灰、分段控件的选中 -记号是底沿一条 2px 线、分区编号是灰的、顶栏没有任何强调线。当红色出现时,它后面一定有一 -件真事——这就是"只高亮真正重要的信息"落到代码里的样子。 +记号是底沿一条 2px 线、分区编号是灰的、顶栏没有任何强调线。红色出现的地方,后面都有一 +件真事。 队红刻意做得比强调红更暖更浅,否则两条血并列时会被误读成"系统在报警"。 ### 形状:矩形,唯一的斜边给装甲条 -全局是矩形:没有圆角、没有切角、没有药丸、没有阴影卡片。**唯一的斜边是装甲条那排平行四边形** +全局是矩形:没有圆角、切角、药丸,也没有阴影卡片。**唯一的斜边是装甲条那排平行四边形** ——它必须在一屏矩形里被一眼认成"这是血,不是别的东西"。别的元素一旦也开始斜切,那个形状 就不再携带信息了。 @@ -166,7 +164,7 @@ node scripts/version.mjs --print # 把算出来的版本打到 stdout ### 固件与客户端怎么对上 -同一条链贯穿到底:`git → version.json → 固件二进制 → Hello 报文 → 客户端界面`。 +同一条链:`git → version.json → 固件二进制 → Hello 报文 → 客户端界面`。 板子每 2 秒广播的 `Hello` 里带着**语义化版本、构建号、以及它编译进去的线路协议号** (布局见 `protocol/README.md`)。客户端读出来之后: @@ -178,6 +176,7 @@ node scripts/version.mjs --print # 把算出来的版本打到 stdout **为什么值得占这 10 个字节**:一块烧错固件的板子,原来只表现为"没画面",现场会去查网线、 AP、供电。现在同一个现象会直接指向"重新烧固件"。 + ## 快速开始 ```sh @@ -192,7 +191,7 @@ pnpm tauri build # 默认打包 macOS 的 .app 与 .dmg;Windows 见下文 ```sh cd desktop/core && cargo test # 83 -cd desktop/src-tauri && cargo test # 32,其中几条真的会起 ffmpeg +cd desktop/src-tauri && cargo test # 40,其中几条真的会起 ffmpeg ``` ### Windows x64 构建 @@ -214,7 +213,7 @@ pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --ci -- --locked 视频录制另需可从 `PATH` 找到的 `ffmpeg`,也可以在配置的 `[recording] ffmpeg` 中指定路径;基础图传显示不依赖它。 -设备发现使用板卡的 Hello 广播;客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口;定向续订可避免“能发现并绑定,但始终等待画面”的情况。队伍端的绑定和画面过滤仍然生效。 +设备发现使用板卡的 Hello 广播;客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口;定向续订可避免"能发现并绑定,但始终等待画面"的情况。队伍端的绑定和画面过滤仍然生效。 ### 演示模式:没有板子也能把整条链路跑起来 @@ -223,8 +222,8 @@ cd desktop CAM_DEMO=1 pnpm app ``` -它**不伪造数据给界面**,而是合成**协议数据报**,从 `Receiver::datagram` 与 -`ArmorHealthStore::datagram` 进去。也就是说演示模式跑的是同一条真实路径: +它不伪造数据给界面,而是合成协议数据报,从 `Receiver::datagram` 与 +`ArmorHealthStore::datagram` 进去。演示模式跑的是同一条真实路径: ```text 合成 JPEG ──分片──▶ Receiver::datagram ──重组──▶ 帧 sink ──▶ WebSocket ──▶ canvas @@ -273,7 +272,7 @@ CAM_CONFIG=<路径> 等价于 --config <路径> ## 视频怎么进 WebView -**一个只监听回环地址的 WebSocket,推二进制 JPEG。** 界面从 `stream_info` 命令拿到 +视频走一个只监听回环地址的 WebSocket,推二进制 JPEG。界面从 `stream_info` 命令拿到 `ws://127.0.0.1:<随机端口>/?token=...`。 ### 为什么不是 Tauri 的 IPC @@ -307,8 +306,8 @@ CAM_CONFIG=<路径> 等价于 --config <路径> 对抗赛里两队都用这套系统看自己小车的图传,要防止对手开第二个窗口连到另一队的板子。 -**关键是这件事做在接收端,不是界面里。** 广播的 Hello 里就有 MAC 派生的 `sender_id` 与 -IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/CSS 只控制显示。 +关键是这件事做在接收端,不是界面里。广播的 Hello 里就有 MAC 派生的 `sender_id` 与 +IP,藏起来毫无意义;数据也可能已经收下并解码了,CSS 只控制显示。 | 闸门 | 管什么 | 什么时候开 | |---|---|---| @@ -316,10 +315,10 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/ | `discovery_all` | 是否记录**其它**板子的 Hello | 只在密码解锁后的设置界面里打开 | 旧客户端把两件事绑在同一道闸门上,于是只能在"能列出候选"和"挡住对手"之间二选一,改绑 -只能靠手改配置文件。现在分开了:设置界面能列出候选摄像头(否则无从选起),但**画面不进 -来**——对手的板子即使把流推到本机端口、即使被记进发现列表,它的帧也进不了重组器。 +只能靠手改配置文件。现在分开了:设置界面能列出候选摄像头(否则无从选起),但**画面进不来**: +对手的板子即使把流推到本机端口、即使被记进发现列表,它的帧也进不了重组器。 -四种情况不限制:裁判端(中立方)、debug 构建、用户端还没绑定、`--camera-id` 显式覆盖。 +`only_sender` 只在两种情况下放开:裁判端(中立方),以及用户端还没绑定摄像头。debug 构建不会放开它。 ### 遥测按编译模式门控 @@ -329,7 +328,7 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/ ### 这挡不住什么 -诚实地说:**IP 仍然通过 Hello 广播泄露。** 对手知道你的 IP 之后可以自己发一个 +**IP 仍然通过 Hello 广播泄露。** 对手知道你的 IP 之后可以自己发一个 `Subscribe` 过来——固件会向任何订阅者推流,那道闸门在他的客户端上,不在你的。真要挡住 需要固件侧做配对 token(首次绑定发一个令牌,固件只接受带令牌的订阅),代价是固件重启就 失配。 @@ -339,8 +338,6 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/ ## 密码:它挡的是谁 -必须说清楚,否则会被当成一把它并不是的锁。 - - **挡得住**:现场随手改绑定。选手想把自己的画面换到对手那块板子上,得先知道密码;围观 的人翻不到明文(只有 Argon2id 哈希),也不能靠改配置文件把绑定改掉——改了之后界面会 要求先解锁。**退出/关闭程序**同样要密码,因为"随手关掉"也是一种要挡的误操作。 @@ -532,7 +529,7 @@ AP、板子,而不是二十行分配代码。现在它有一个直接对着这 ## 配置 -`~/.config/cam-desktop/config.toml`,`--config` 可改。**目录名沿用旧的 `cam-desktop`**:改名不该顺手把现场的密码哈希与绑定丢掉。要干净重来就删掉这个目录。 +`~/.config/cam-desktop/config.toml`,`--config` 可改。**目录名沿用旧的 `cam-desktop`**(原因见开头)。要干净重来就删掉这个目录。 ```toml role = "team" @@ -599,8 +596,8 @@ open 'http://127.0.0.1:1420/?preview=password' # 首启的设密码 ```sh cd desktop/core && cargo test # 83:重组、发现、两道闸门、帧率窗口、槽位、配置、密码、胜负判定 -cd desktop/src-tauri && cargo test # 32:帧头布局、录制、截图、演示画面、命令行 -cd protocol && cargo test # 20 +cd desktop/src-tauri && cargo test # 40:帧头布局、录制、截图、演示画面、命令行 +cd protocol && cargo test # 23 ``` 前端解码队列测试:在 `desktop/` 运行 `pnpm test:stream`,5 项测试覆盖慢解码时保留最新帧、多路独立处理、关闭清理、坏帧恢复与设备重启。它不替代实际 WebView 的画面验证。 @@ -649,8 +646,8 @@ RUST_LOG=debug ./target/aarch64-apple-darwin/debug/cam-desktop | grep 'stream:' DTO,所以至少能解释清楚。 - **`csp` 是一条收紧过的白名单**,但它是"够用"而不是"最小":`style-src` 带 `'unsafe-inline'`(React 的内联 `style` 属性需要它),`connect-src` 放开了整个 - `ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端" - ——见下面"测试"一节最后一条。 + `ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端", + 见"测试"一节最后一条。 - **证书与公证**没做,`.dmg` 只在本机可用。 - 前端已有解码队列测试;布局、实际 WebView 解码和画面显示仍需浏览器预览及真机运行验证。 diff --git a/desktop/src/lib/version.ts b/desktop/src/lib/version.ts index 61069c0..e323fe4 100644 --- a/desktop/src/lib/version.ts +++ b/desktop/src/lib/version.ts @@ -11,7 +11,7 @@ export const BUILD = { minor: 2, patch: 1, /** git 提交数,单调递增。 */ - build: 35, + build: 36, /** 线路协议版本。与固件自报的那个数直接比对。 */ protocol: 1, /** 生成时的提交日期(`YYYY-MM-DD`)。 */ diff --git a/docs/CHANGELOG-legacy.md b/docs/CHANGELOG-legacy.md new file mode 100644 index 0000000..9e5116a --- /dev/null +++ b/docs/CHANGELOG-legacy.md @@ -0,0 +1,102 @@ +# 改动记录 + +记录面向使用者和维护者的主要变化。历史条目依据 Git 提交整理,日期为提交日期,尚未对应正式发布版本;详细历史见 `git log`。 + +## 未发布 + +### 文档 + +- 重组根 README,补充 ESP Rust 入门、跨平台编译与标准烧录指南。 +- 将硬件限制、开发说明与改动记录分别整理为独立文档。 +- 修正 JPEG 质量与数据大小的说明,区分历史性能记录和当前验证。 +- 补充桌面端 Windows x64 / NSIS 构建、测试与产物路径说明。 + +### 构建 + +- 固定使用命名工具链 `esp-1.97.0.0`,避免较新预览版标准库的 ESP-IDF 兼容问题。 +- 移除 S3 构建配置中的个人 macOS SDK 路径,默认 ESP-IDF v5.5.5,支持环境变量覆盖。 +- 新增从 `.env` 读取配置的 PowerShell 构建脚本。 +- 图传构建脚本使用 `--locked`,忽略 SDK 缓存和生成配置。 +- 分区表复制到生成的 CMake 工程,不再依赖 Cargo 输出层级;支持 Windows 使用较短的外部构建目录。 + +### 修复 + +- 增加运行中 Wi-Fi 断线恢复、关联和 DHCP 超时退避;取址后重建 UDP socket,清理订阅目标与旧帧,保留相机状态和帧序号。 +- 桌面端向已发现的图传板 IP 定向续订,修复 VPN / 多网卡把全局广播发往虚拟接口后“设备在线但无画面”的问题。 +- 修正丢帧统计:计入未收齐就被替换、超时及帧号缺口,重复包和等待分片不计作丢帧;拒绝迟到旧帧覆盖新帧。 +- 校验分片数量、长度与同帧元数据,处理帧号回绕及设备重启。 + +### 图传性能 + +- 根据距离收益有限、拥塞增加的实测反馈,将交付 Wi-Fi 最大发射功率上限从 17 dBm 回退到 15 dBm,实板驱动回读 15.00 dBm;启动、重连均应用同一设置,拥塞和温控逻辑不自动降功率。保留此前 14/17 dBm 的原始记录,新增功率与拥塞分析。 +- 阻塞恢复时非阻塞取最新完整 JPEG,返回被替换的旧采集缓冲;保留三块采集内存,不增加待发送 FIFO。 +- 每帧增加采集帧龄和绝对发送期限,重试预算默认缩短至 8 ms;`UDP_RETRY_BUDGET_US=0` 可在回压时立即放弃剩余分片。 +- 客户端通过本机帧时间标记拒绝 WebView 积压和慢解码的过期画面;本地 JPEG 二进制头保持 16 字节兼容。 +- 根据长程运行的峰值丢帧反馈,当前交付恢复三缓冲;双缓冲的短测数据保留为历史对照。 +- 新增可选 OV5640 `CAM_AE_LEVEL` 细调:`-1` 将亮度目标从 55 调至 45,保留 AEC/AGC 与曝光上限,供轻微过亮时调整。 +- 增加完整 OV5640 `vga30` PLL 配置与可选双缓冲;保持默认行、帧时序与彩色读出窗口,配套调整自动曝光行数上限。 +- Windows 客户端增加裁判端 `--latency-report` 校准模式,通过屏幕标记和实际视频画布测量首次可解码回显时间,保存本地 NDJSON;区分画布刷新估计与物理面板响应。 +- 固件直接读取 PSRAM 中的 JPEG,复用单个数据报缓冲,减少每帧分片复制与分配。 +- 视频 socket 使用 IP precedence 4,映射至保留 AMPDU 的 Wi-Fi 视频队列 AC_VI。 +- 对临时 UDP 发送拥塞进行有时限的让出调度重试,统计实际提交帧、失败次数与丢弃帧。 +- Windows UDP 接收缓冲请求增至 1 MiB;界面视频通道和解码队列保留最新待处理帧,避免慢端持续积压。 +- 新客户端使用随机会话 ID;固件更新同一会话的源端口,避免 NAT / TUN 端口变化产生重复推流目标,旧固定 ID 客户端继续按地址区分。 +- 新增可选编译参数 `WIFI_TX_POWER_DBM`(2–20 dBm);未设置时保留 SDK 默认上限,按实际板卡和网络测量调节。 +- 新增 `scripts/perf_probe.py`,测量完整帧率、帧损失、接收间隔和分片组装耗时。 +- 新增 `CAM_MAX_FPS`(默认 0,不限帧),可限制取帧和发送节拍,过载后不追赶补发。 +- 新增 `CAM_GRAYSCALE`,通过传感器灰度效果保留硬件 JPEG;增加可配置的芯片温度减载和 5°C 恢复回差。 +- 增加芯片内部温度、采集错误与复位原因日志,以及串口和原生客户端视频通道的长测记录脚本。 +- 新增 OV5640 曝光目标偏置与固定快门行数配置,保留自动增益;记录实际曝光行数,避免将降低亮度等同于缩短快门。 +- 新增 OV5640 水平总长试验参数 `CAM_HTS`;过短消隐会出现绿紫闪烁,几何约束通过不代表画质稳定,必须在板上验证。 +- 新增可配置 UDP 分片间隔及有界帧发送时间,支持按五分钟窗口比较突发发送与让出调度发送。 +- 新增可选 JPEG 压缩回压调节 `CAM_QUALITY_MAX`,拥塞时快速减小彩色帧,连续稳定后缓慢恢复画质;记录实际压缩档位和传感器增益。 +- 新增 `CAM_EXPOSURE_MAX_LINES`,保留自动曝光和增益联动,以曝光上限控制拖影;固定短快门不再作为室内亮度优化的默认方案。 +- 新增 OV5640 自动增益上限 `CAM_GAIN_CEILING`,为室内短曝光提供增益余量,保留运行时增益读数供验证。 + +### 硬件配置 + +- 显式设置 `CONFIG_CAMERA_CORE1=y`;仅关闭 `CAMERA_CORE0` 时,Kconfig 的 choice 仍会默认选择 CPU0。 +- 按当前 Wi-Fi 配置的镜像核对 Factory 偏移与原测试模组 `0xFADE7` 坏字节的可写性;后续重编译仍需重新检查,以 `partitions.csv` 为准。 + +## 2026-10-15 + +### 持续集成 + +- 新增 Windows、Linux、macOS 客户端构建工作流。 +- 调整制品上传版本及缓存行为,适配 Gitea runner。 +- 修复 Windows runner 的 shell、临时目录和低完整性环境适配。 +- 使用 npm 安装的 JS 版 pnpm,处理安装目录残留与 `EEXIST`。 + +对应提交:`98dc87e` 至 `4794bed`。 + +## 2026-10-14 + +### 新增 + +- 引入 Tauri v2 + React 队伍端和裁判端,网络逻辑拆分到 `desktop/core`。 +- 加入 ESP32-C3 一主三从装甲板:四路受击检测、灯带及血量广播。 +- 保存早期 C 版装甲板工程到 `reference/armor/`。 +- 桌面端加入双方血条、归零胜负提示与裁判端队名编辑。 + +### 调整与修复 + +- 移除旧 Slint 客户端。 +- 修复演示模式命中节奏与设置文字显示,移除受击画面抖动。 + +对应提交:`a89d7ae`、`4f7a142`、`7fdebe3`、`a30eb32`、`fd71b83`、`a93ae06`。 + +## 2026-10-05 + +### 新增 + +- 建立摄像头固件、共享 UDP 协议与初代桌面客户端。 +- 支持多端发现、订阅、设备绑定及队伍端 / 裁判端视图。 +- 加入滚轮缩放、拖动平移与启动最大化。 + +### 修复 + +- 修复探测脚本重组状态未按帧隔离导致的坏帧误报,以及 FPS 窗口计算错误。 +- 修复裁判端两格宽度不一致,槽位按队伍语义分配。 +- 队伍端 release 接收路径限制到已绑定图传板。 + +早期对重复分片的协议归因已被后续提交更正,不能将当时的坏帧直接归为协议缺陷。对应提交:`4f893ce`、`1c65eaa`、`7be8d7d`、`c7f42a1`、`a16ad85`、`cd8344c`、`3631a03`。 diff --git a/docs/desktop-ui-redesign.md b/docs/desktop-ui-redesign.md new file mode 100644 index 0000000..b668eaa --- /dev/null +++ b/docs/desktop-ui-redesign.md @@ -0,0 +1,1111 @@ +# cam-desktop 重构设计:The Division 风格 UI/UX · 比赛控制 · 直播包装 · .zip 固件包 + +> 版本:草案 v0.1 | 日期:2026-10-19 +> 范围:`desktop/`(React + Tauri v2)、`desktop/core`、`desktop/src-tauri`、`protocol/`、`scripts/`、`docs/firmware-maintenance.md` +> 约束:保留视频、血量、装甲板、录制、固件维护;Tauri v2 + React + 现有 Rust 后端;中文界面;跨平台。 +> 规则依据:[SIMBA ΣDOCS · 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/) +> +> **实施状态(2026-10-19)**:`.zip` 打包/读取、F11/Alt+Enter 全屏、SHD 视觉 token 已落地; +> 比赛状态机/判负/技术暂停/直播页按 §7.1 的 M2–M4 分阶段推进,尚待 §7.4 Q1–Q4 确认。 + +--- + +## 0. 现状基线(读代码后的结论) + +| 层 | 现状 | 对本次重构的意义 | +| --- | --- | --- | +| 页面 | `App.tsx` 单一分支树:`MaintenancePage` → 致命错误 → 等待快照 → 身份选择 → 设密码 → `TeamPage` / `RefereePage` | 没有路由、没有 Shell、没有直播视图;需先抽出 `AppShell` 与视图枚举 | +| 样式 | `styles/global.css` 单文件 1687 行,`:root` 20 个 token,深底 + 荧光绿强调 | token 体系要替换,且**中文排版规则已踩过坑**(见 1.3),不能推倒重来 | +| 状态 | 后端 5 Hz 推整份 `Snapshot`,前端 `useSyncExternalStore` 单例 | 比赛状态机应作为快照的一部分下发,前端不做第二份真相 | +| 视频 | WebSocket 二进制 JPEG + canvas 绘制,缩放/平移/网格/瞄准器/命中反馈全在 `VideoSurface` | 动效不得侵入 canvas 绘制路径(否则抢帧) | +| HUD | 5 格斜切血条,左上自己/右上对手,数字默认显示 | 5 格 = 20 点/格,而规则是**每发扣 3 点** → 数字必须保留(见 6.2) | +| 裁判端 | 双路等宽 + 队名编辑 + 装甲板表 + 录制/回放 + 底部 250px 固定高度 | 无计时/比分/暂停/判负;底部 Dock 要重排 | +| 后端 | UDP `5600` 视频 / `5601` 装甲板;命令集中在 `commands.rs`;密码闸门在 `state.rs` | 新命令同样要过闸门;比赛状态需要 `role == referee` 的限制 | +| 协议 | `MsgType`: Video=1, Subscribe=2, Hello=3, Heartbeat=4, ArmorHealth=5,**只有上行/广播,没有下行控制** | 客户端判负/暂停**无法**改变硬件血量(见 3.2、R1) | +| 固件包 | `bundle.json` + `esp32s3-app.bin` + `partition-table.bin`(+`bootloader.bin`),客户端选**目录**,`format: 1`,逐项 SHA256 | 改成 `.zip` 只需改 `maintenance::load_bundle` 的取数入口,校验逻辑可原样复用 | +| 产物 | `target/firmware/` 已有 `esp32s3-app.bin`(1,143,504 B)、`partition-table.bin`(3072=0xC00)、`bootloader.bin`、`BUILD-INFO.json`、`bundle.json`、`SHA256SUMS.txt` | 打包环节有现成输入,缺的是"构建后自动产出 zip" | +| 现场 | 适应性训练 **10-25**,决赛 **11-01** | 距决赛 13 天 → 优先级必须围绕这一天排(见 7.1) | + +--- + +## 1. 设计系统(可落地) + +### 1.1 六条设计原则(附取舍) + +| # | 原则 | 具体约束 | 被否决的做法 | +| --- | --- | --- | --- | +| P1 | **画面优先** | 视频窗格内除 HUD 外零装饰;HUD 只贴四角与上沿;中心半径 18% 区域**永不**放常驻元素 | 半透明斜纹背景铺满、常驻扫描线 | +| P2 | **三秒可读** | 血量/时间/比分/状态四类信息位置固定、字号 ≥ 24px@1080p、对比 ≥ 4.5:1;3 m 外单眼可读 | 用颜色/图标单独表达状态 | +| P3 | **状态永不含糊** | 无数据不显示 0;「离线/演示/暂停/已判定」必须全屏可见;同一时刻只有**一个**主结论 | 用 toast 表达比赛状态 | +| P4 | **误操作成本 > 操作成本** | 破坏性动作(判负、清空配置、烧录)一律 `长按 ≥ 1000ms` + 理由 + 可撤销/可审计;高频只读动作零确认 | 用 `confirm()` 敷衍 | +| P5 | **同源渲染** | 用户端 HUD / 裁判端 / 直播三层共用同一 `Snapshot` + `MatchView` 与同一套安全区 | 给直播另写一套数据拼装 | +| P6 | **无障碍与光敏安全** | 闪烁 ≤ 3 Hz 且面积 < 屏幕 25%;`prefers-reduced-motion` 全局降级;色不单独承载信息 | 无限 glitch、随机闪白 | + +The Division 能迁移过来的视觉语言只有四样:**近黑底 + 单一强调色 + 细线切角 + 等宽数字**。它的地图、装备详情、粒子特效属于"非实时、可暂停"的场景,搬进 3 分钟一局的比赛界面会直接损害 P1/P2。 + +### 1.2 色彩 + +**色板**(可直接替换 `global.css` 的 `:root`): + +```css +:root { + /* 底色:三层就够,第四层用"提亮 4%"而不是再加一个变量 */ + --bg-void: #07080a; /* 窗口最底 / 直播黑场 */ + --bg-base: #0b0d10; /* 页面底 */ + --bg-panel: #11151a; /* 面板 / 顶栏 / 状态行 */ + --bg-raised: #171c22; /* 卡片 / 行 */ + --bg-input: #090b0e; /* 输入框 / 日志 */ + --bg-scrim: rgba(6, 8, 10, 0.78); /* 遮罩 */ + + /* 线:Division 的"1px 亮边"是主要体积感来源,阴影只做兜底 */ + --line: #232a33; + --line-strong: #333c48; + --line-accent: rgba(255, 122, 26, 0.45); + + /* 文本 */ + --text: #e9ecf1; /* 15.2:1 on --bg-base */ + --text-dim: #98a2b0; /* 6.4:1 */ + --text-faint: #5e6875; /* 3.2:1 —— 只用于非信息性文字 */ + --text-invert: #07080a; + + /* 强调:SHD 橙。唯一强调色,用于焦点/当前项/CTA/直播描边 */ + --accent: #ff7a1a; + --accent-hot: #ff9a4d; + --accent-deep: #b44e08; + --accent-veil: rgba(255, 122, 26, 0.12); + + /* 语义 */ + --danger: #ff3b30; /* 判负 / 录制点 / 错误 */ + --warn: #f5b13d; /* 低血 / 链路抖动 / 未上锁 */ + --ok: #3ddc84; /* 通过 / 在线 */ + --info: #35d6e6; /* 技术暂停 / 扫描 / 遥测 */ + + /* 队伍:规则"编号小=红方、编号大=蓝方" → 队号 0 固定红、1 固定蓝 */ + --team-0: #ff4d4f; + --team-1: #3e9bff; + --team-0-veil: rgba(255, 77, 79, 0.14); + --team-1-veil: rgba(62, 155, 255, 0.14); +} +``` + +**用色规则** + +1. 强调色**不用于血量**——血量只用队伍色,低血时切 `--danger`(现有 `HealthBar` 行为,保留)。 +2. 每个屏幕最多 **1 个** `--accent` 元素做主 CTA;第二个强调改用 `--line-accent` 细边。 +3. 红色只出现在三处:判负、录制中、错误;**不要**用它做"取消"。 +4. 直播层禁止使用 `--accent-veil` 以上的大面积橙(压缩后发脏),装饰靠线不靠面。 +5. 纯黑 `#000` 只用于视频信箱区;面板底色不低于 `#0b0d10`,避免 OLED/编码 banding。 +6. 对比度自检清单:所有正文 ≥ 4.5:1、所有 ≥18px 的数字 ≥ 3:1、队伍色文字必须叠 `text-shadow: 0 1px 3px rgba(0,0,0,.9)`(现有做法保留)。 + +### 1.3 字体与排版 + +```css +:root { + --font-ui: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", + "Source Han Sans SC", ui-sans-serif, system-ui, sans-serif; + --font-mono: ui-monospace, "JetBrains Mono", "Cascadia Mono", + "SF Mono", Consolas, monospace; + + --fs-micro: 10px; --fs-small: 11px; --fs-body: 13px; + --fs-title: 15px; --fs-h2: 17px; --fs-h1: 24px; + --fs-hud: clamp(18px, 1.8vmin, 28px); /* 血量数字 */ + --fs-clock: clamp(28px, 3.4vmin, 44px); /* 比赛计时 */ + --fs-score: clamp(40px, 5vmin, 64px); /* 比分 */ + --fs-display:clamp(44px, 8vmin, 96px); /* 判定大字(仅直播/结果卡) */ +} +``` + +规则: + +- **中文不加 `letter-spacing`**。`global.css` 已注明"表头是中文,加字距只会让它看起来散架"——这条继续执行,只对拉丁文(`LIVE`、`TECH PAUSE`、`SERIES`)用 `letter-spacing: .14em` + `text-transform: uppercase`。 +- 所有数字(血量、时间、比分、ID、帧率)走 `--font-mono` + `font-variant-numeric: tabular-nums`,避免数字跳动时宽度变化引起布局抖动。 +- 字号阶梯只有 6 档正文 + 4 档显示级;禁止在组件里写 `font-size: 12.5px` 这种一次性值。 +- 字体不打包(现场离线,中文字体体积又大)。若要 Division 味的拉丁字形,可后续单独引入 OFL 的 `Barlow Condensed SemiBold`(约 40 KB),**仅用于数字与拉丁标签**,并保留系统字体作为 fallback。 +- 行高:正文 1.7、标题 1.3、数字 1.0。 + +### 1.4 层级(z-index / elevation / 几何) + +```css +:root { + --z-stage: 0; /* 视频 canvas */ + --z-hud: 10; /* 画面内 HUD */ + --z-overlay: 20; /* 暂停/判定覆盖层 */ + --z-chrome: 30; /* 顶栏/状态行/悬浮徽标 */ + --z-drawer: 40; /* 设置抽屉 */ + --z-dialog: 50; /* 模态框 */ + --z-toast: 60; /* 提示 */ + --z-critical: 70; /* 链路断开/致命错误 */ +} +``` + +- **面板体积感** = `1px var(--line)` + `inset 0 1px 0 rgba(255,255,255,.05)`,**不用**大阴影;阴影只用于浮动层(抽屉/对话框/toast)。 +- **切角**:`--cut: 8px`。只在按钮、徽标、比分板、判定卡上用 `clip-path`;面板不要用(`clip-path` 会吃掉 border 和 box-shadow,改完还得补 SVG 边框,收益不抵成本)。 + ```css + .cut { clip-path: polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), + calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut)); } + ``` +- 圆角:`--r-xs 2 / --r-sm 4 / --r-md 8 / --r-lg 12`。Division 几乎全直角,只在小控件上保留 2–4px;`10px` 的大圆角(现有 `--radius`)改到 4px。 +- 角标(bracket):面板四角用 `::before/::after` 画 8×1px 亮线,只在"当前焦点面板"上出现一个角,用来替代发光描边。 + +### 1.5 HUD 规格 + +| 元素 | 位置 | 尺寸 | 规则 | +| --- | --- | --- | --- | +| 我方血条 | 画面左上,距边 `3.5%` 宽 | 宽 36%(现 42%)、高 16px、5 格、`skewX(-15deg)` | 数字必显;`available=false` 时显示"装甲板未接入",**不显示 0** | +| 对手血条 | 画面右上,镜像 | 同上 | 同上;标签"我方/对手"保留 | +| 瞄准器 | 跟随图像 | 大小不随缩放 | 现有 `drawCrosshair` 不动,仅颜色默认值改为 `#ff7a1a`(可配置) | +| 倍率 | 画面右下角 | 11px mono | 仅在 zoom > 1 时出现 | +| 命中提示条 | 画面顶部 `top: 56px` 居中 | 高 26px | 避开准星(已踩坑,保留) | +| **比赛计时**(新) | 画面顶部居中 `top: 12px` | 32px mono | 只在 `Running/Paused/Ended` 出现;`< 30s` 变 `--danger` 且每秒脉冲一次(**不闪**) | +| **暂停/判负遮罩**(新) | 整画面 `--z-overlay` | — | 半透明 + 居中文字;不遮血条与计时(保留可读性) | +| 网格 | 画面内 | 现有 | 默认关;`opacity ≤ .22` | +| 技术读数 | 底部状态行 | 11px mono | 用户端 release 下不显示(`TEAM_DIAGNOSTICS` 已有门控) | + +**HUD 熄灯规则**(新增):非关键 HUD(网格、倍率、帧率、界面数)在 3 s 无指针输入后降到 `opacity: .35`;`prefers-reduced-motion: reduce` 时**直接隐藏**而不做透明度过渡(避免持续重绘)。 + +### 1.6 动效原则 + +```css +:root { + --t-fast: 90ms; + --t-base: 140ms; + --t-slow: 220ms; + --t-emphatic: 320ms; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); + --ease-in: cubic-bezier(0.7, 0, 1, 0.6); + --motion-scale: 1; /* 0 = 降级 */ +} +@media (prefers-reduced-motion: reduce) { + :root { --motion-scale: 0; --t-fast: 1ms; --t-base: 1ms; + --t-slow: 1ms; --t-emphatic: 1ms; } +} +``` + +**动效只分三类,不另设表外时长** + +| 类别 | 时长 | 用途 | 属性 | +| --- | --- | --- | --- | +| 状态确认 | 90–140 ms | 按钮按下、开关、页签、hover | `transform`/`opacity`/`background-color` | +| 状态迁移 | 220–320 ms | 抽屉、对话框、覆盖层、全屏切换、比分变化 | `transform`/`opacity` | +| 连续数据 | ≤ 200 ms 插值 | 血量格、数字滚动、计时 | 只改文本与宽度,不改布局 | + +**动效清单**(实现时逐条对表) + +| 组件 | 触发 | 动效 | 时长/曲线 | +| --- | --- | --- | --- | +| `Button` | press | `scale(.98)` | 90ms / standard | +| `Button` hover | hover | 边框转 `--line-accent` + 背景 +4% | 140ms | +| 页签 | 切换 | 底线 2px 从左滑到新页签 | 220ms / out | +| `HealthBar` 格 | 掉血 | 该格 `background` 140ms 淡出;数字 160ms 计数 | 140/160ms | +| 命中闪红 | hit | 620ms 全屏径向渐变淡出(现有 `hit-fade`) | 保留 | +| 暂停遮罩 | 进入/退出 | `opacity 0→1` + 文字 `translateY(6px)→0` | 220ms / out | +| 判定卡 | 结束 | `scale(.94)→1` + `opacity` | 320ms / out | +| 计时最后 30 s | 每秒 | 数字 `scale(1→1.06→1)` 一次 | 300ms | +| 键盘提示 | Tab 按住 | 底部面板 `translateY(8px)→0` | 220ms | +| 全屏切换 | F11 | 无自定义动画(由窗口管理器负责) | — | + +**禁止清单** + +1. 全屏抖动(`HitFeedbackConfig.shake` 已废弃,代码里仍然不要复活)。 +2. 任何 > 3 Hz 的闪烁;任何占屏 > 25% 的红色闪烁(判负的 320 ms 一次性淡入除外)。 +3. 常驻循环动画(扫描线、呼吸背景、粒子)。 +4. 遮住准星中心 18% 半径的动画。 +5. 会触发 layout 的动画(`width`/`height`/`top`/`left` 的 transition)——血条格子用 `background`,进度用 `transform: scaleX`。 +6. 依赖 `requestAnimationFrame` 的新增长循环(现有 `VideoSurface` 的绘制循环是唯一允许的一个)。 + +**降级开关**:设置里加"低动效模式"(`ui.reduceMotion`),等价于强制 `prefers-reduced-motion`,给低配裁判机与"现场怕闪"的场合用。 + +--- + +## 2. 页面与组件重构方案 + +### 2.1 应用外壳(新增 `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` 分支。 + +### 2.2 用户端 `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`)保留,但提示从底部横幅改到画面内角标,避免遮挡。 + +### 2.3 裁判端 `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` 占固定高度;现有 `referee-bottom` 的 250px 改成 **Dock 可拖拽高度**(默认 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 取消",避免一直占一行高。 + +### 2.4 直播 `BroadcastPage`(新) + +见第 4 节。要点:一个 1920×1080 虚拟画布 + 4 个模板 + 安全区参考线 + 只读数据。 + +### 2.5 维护页 `MaintenancePage` + +现在是一个 2 列网格的长页面(设备卡 / 固件包卡 / 分区表 / 报告),信息密度高但"下一步做什么"不明显。改成 **5 步 Stepper**: + +``` +① 设备 → ② 固件包(.zip) → ③ 烧录计划 → ④ 烧录 → ⑤ 结果 +────────────────────────────────────────────────────────────── +左:步骤栏(已完成 ✓ / 当前 ▶ / 未开始 ○) +右:当前步骤卡(大字号 + 一个主按钮 + 次级信息折叠) +``` + +- 步骤 ② 接受 **`.zip` 与目录**(目录收在"高级 / 兼容旧包"里),支持拖拽。 +- 步骤 ③ 用 diff 表显示"factory: 0x070000 → 0x200000",并把 `warnings` 提到主区域(现在埋在下面)。 +- 步骤 ④ 全屏进度 + 不可关闭 + `取消`(已有 `maintenance_cancel`)+ 实时日志。 +- 步骤 ⑤ 报告卡:结果、坏字节/坏扇区、备份目录、`恢复旧分区表`、`复制报告`。 +- 保留全部诊断细节(分区表、候选槽位、回读不一致列表),只是默认折叠。 + +### 2.6 组件清单 + +**新增** + +| 文件 | 职责 | +| --- | --- | +| `components/ui/Panel.tsx` | 带切角/角标的容器(`tone`/`dense`/`flush`) | +| `components/ui/HoldButton.tsx` | 长按确认按钮(1000ms 环形进度,松手取消,Esc 取消) | +| `components/ui/KeyHint.tsx` | 键帽样式(``) | +| `components/ui/Stepper.tsx` | 维护页步骤栏 | +| `components/ui/EmptyState.tsx` | 统一空状态(图标 + 一句 + 一个动作) | +| `components/match/MatchBar.tsx` | 裁判端比赛条 | +| `components/match/MatchClock.tsx` | 计时显示(插值 + 最后 30 s 强调) | +| `components/match/ScoreBoard.tsx` | 比分(局/系列赛) | +| `components/match/ForfeitDialog.tsx` | 判负对话框(败方 + 理由 + 长按) | +| `components/match/TechPauseDialog.tsx` | 技术暂停(申请方 + 理由 + 已批准勾选) | +| `components/match/EventPanel.tsx` | 事件时间轴 + 导出 | +| `components/match/PausedOverlay.tsx` / `JudgedOverlay.tsx` | 覆盖层 | +| `components/replay/ReplayView.tsx` | 双路同步回放 + 事件定位 | +| `components/broadcast/BroadcastStage.tsx` | 直播画布 | +| `components/broadcast/SafeGuides.tsx` | 安全区参考线(仅预览) | +| `components/broadcast/{TitleBar,ScoreBug,RoundClock,TalentCard,LowerThird,AlertCard,SponsorRail,StandbyCard}.tsx` | 包装层 | +| `components/firmware/FirmwareDropZone.tsx` | .zip 拖拽/选择 | +| `hooks/useFullscreen.ts` / `useHotkeys.ts` / `useIdle.ts` / `useServerClock.ts` | 全屏 / 快捷键 / 空闲 / 服务器时钟 | + +**修改** + +`App.tsx`(Shell + 视图)、`TopBar`(比赛条位、直播入口、工具收纳)、`TeamPage`、`RefereePage`、`MaintenancePage`、`MediaPanel`(按局分组/回放)、`ArmorTable`(双队/迷你条)、`HealthBar`(宽 36%、字号 token)、`SettingsPanel`(新增"界面/动效/直播"段)、`global.css` → 拆成 `tokens.css` + `base.css` + `components.css`(或保持单文件但按 1–7 节分区)。 + +### 2.7 状态模型(前端) + +```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` 在窗口失焦时会被节流,而比赛计时绝不能因为切窗口而变慢。 + +--- + +## 3. 交互逻辑 + +### 3.1 F11 全屏 + +**实现** + +```ts +// hooks/useFullscreen.ts +const win = getCurrentWindow(); +const [full, setFull] = useState(false); +useEffect(() => { win.isFullscreen().then(setFull); }, []); +const toggle = useCallback(async () => { + const next = !(await win.isFullscreen()); + await win.setFullscreen(next); + setFull(next); +}, []); +``` + +**权限**:`capabilities/default.json` 现在只有 `core:default` + `dialog:default`,**不含**全屏。需要追加(identifier 已在本仓库 `gen/schemas/acl-manifests.json` 中确认存在): + +```json +"permissions": [ + "core:default", + "dialog:default", + "core:window:allow-set-fullscreen", + "core:window:allow-is-fullscreen" +] +``` + +**按键** + +| 平台 | 按键 | 备注 | +| --- | --- | --- | +| Windows / Linux | `F11` | `keydown` capture + `preventDefault()` | +| macOS | `⌃⌘F` | 同时支持 `F11`(部分外接键盘有) | +| 全平台 | `Alt+Enter` | 备用,游戏玩家肌肉记忆 | +| 全平台 | `Esc` | 退出全屏 / 关闭顶层对话框(按层级,一次只关一个) | + +**细节** + +- 切换后 400 ms 内忽略再次触发(部分窗口管理器会发出重复 keydown)。 +- 全屏切到 `immersive`:chrome 隐藏、`broadcast` 模式下同时隐藏安全区参考线与光标。 +- 退出通道必须**看得见**:全屏时右上角保留一个 1px 高热区,指针进入显示"退出全屏 F11",3 s 后淡出。 +- 不做"全屏时禁止关闭窗口"这类硬锁——现场需要能立刻退出。 +- 多显示器:P2 再加 `setFullscreenOnMonitor`(`core:window:allow-set-fullscreen-on-monitor` 已存在),让直播输出指定显示器。 + +### 3.2 一键判负(最危险的动作) + +**入口**:`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 } } +``` + +- 状态 → `Ended { by: Referee, reason, at, actor }`,比分按规则 +1 给胜方。 +- 用户端若已同步(见 R2):显示 `JudgedOverlay`「本局判负」;未同步则只影响裁判端与直播。 +- **不改硬件血量**:`ArmorHealthStore` 只接收上报,没有任何写入口(见 R1)。界面必须显式写明这一点,否则现场会以为"判负之后对方血量归零了"。 +- **审计**:事件 `{ at, actor, action:"forfeit", loser, reason, note, hp:[76,100], remainingMs:161000 }`,追加到 `matches///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 } +``` + +否则"回放/统计"里会分不清这局是怎么结束的。 + +### 3.3 技术暂停 + +**规则对齐**:手册写"任意一方…可向场边裁判申请发起时长为 **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.4 计时 / 比分 + +**两个不同的 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`。先到 `ceil(bestOf/2)` 局的一方赢下系列赛,界面显示 `2:0 · 系列赛结束`。 + +**计时实现**(后端为准): + +```rust +pub struct MatchClock { + started: Option, + 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` 自己减。 + +### 3.5 回放 + +**入口**:`Ended` 后结果卡 →「回放本局」;或 Dock 的「录制与回放 → 按局分组」。 + +**布局** + +``` +┌ 回放 · 2026-11-01 第2局 红 vs 蓝 ─────────────── [导出] [返回] ┐ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ 红方录屏 │ │ 蓝方录屏 │ │ +│ └──────────────────────┘ └──────────────────────┘ │ +│ ▶ ━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 01:12/03:00 │ +│ ⚑ 00:00 开始 · ● 00:41 命中 · ⏸ 01:12 技术暂停 · ⚑ 02:33 判负 │ +└────────────────────────────────────────────────────────────────┘ +``` + +- 两个 `