docs: 按 humanizer 重写项目文档并同步 RMIC::2026 现状
构建客户端 / 检查(类型 + 单元测试) (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

This commit is contained in:
2026-10-19 10:47:00 +08:00
parent d9a27b69f3
commit e3acb37d32
22 changed files with 3029 additions and 203 deletions
+2 -2
View File
@@ -40,5 +40,5 @@
__pycache__/ __pycache__/
# `docs/simba_docs`:工作区里另起的一个 Astro 文档站,与本仓库的目录结构无关, # `docs/simba_docs`:工作区里另起的一个 Astro 文档站,与本仓库的目录结构无关,
# 也不属于本项目的产物。留着不跟踪,免得以后 `git add -A` 顺手把它带进来。 # 也不属于本项目的产物。只忽略它本身;docs/ 下的项目文档要跟踪。
/docs/ /docs/simba_docs/
+5 -20
View File
@@ -3,31 +3,16 @@
"major": 0, "major": 0,
"minor": 2, "minor": 2,
"patch": 1, "patch": 1,
"build": 35, "build": 36,
"protocol": 1, "protocol": 1,
"date": "2026-10-19", "date": "2026-10-19",
"baseline": "v0.2.0", "baseline": "v0.2.1",
"commits": 1, "commits": 1,
"groups": { "groups": {
"fix": [ "docs": [
{ {
"scope": "ci", "scope": null,
"text": "版本校验在浅克隆下必然误报", "text": "按 humanizer 重写项目文档并同步 RMIC::2026 现状",
"breaking": false
},
{
"scope": "ci",
"text": "checkout 改成 fetch-depth: 0——没有标签、提交数恒为 1 时,",
"breaking": false
},
{
"scope": "version",
"text": "CHANGELOG 按标签区间重算全部历史,不再只渲染当前版本,",
"breaking": false
},
{
"scope": "build",
"text": "固件包文件名用算出来的版本,不再用 BUILD-INFO.json 里那套旧编号",
"breaking": false "breaking": false
} }
] ]
+9 -1
View File
@@ -6,7 +6,15 @@
## v0.2.1 — 2026-10-19 ## 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 个提交
### 修复 ### 修复
+5 -5
View File
@@ -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 ```powershell
$targetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' } $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=[]' 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 在根目录执行: 限帧、温度回差和 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 ```text
cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys 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;原始日志、时长和完整网络条件未随这些数字保存。 下表为原 README 保存的**历史单次测试数据**,不是本次构建实测或性能保证。配置为 OV5640、VGA;原始日志、时长和完整网络条件未随这些数字保存。
+21 -20
View File
@@ -1,6 +1,7 @@
# 烧录 / 维护客户端架构设计(ESP32-S3 图传板) # 烧录 / 维护客户端架构设计(ESP32-S3 图传板)
> 状态:**提议中(Proposed)**,等待第 9 节的问题清单答复后冻结。 > 状态:**提议中(Proposed)**,等待第 9 节的问题清单答复后冻结。
> 落地情况:实际实现走的是 desktop 维护页 + `.zip` bundle,见 [docs/firmware-maintenance.md](docs/firmware-maintenance.md)。
> 输入约束:现场存在 SPI Flash 坏块;需要兼顾 OTA 与现场排障;**不改固件源码、不重新编译**; > 输入约束:现场存在 SPI Flash 坏块;需要兼顾 OTA 与现场排障;**不改固件源码、不重新编译**;
> 优先“模板固件 + 动态 patch”。 > 优先“模板固件 + 动态 patch”。
> 本文所有结论都在仓库里逐条对过代码/脚本/分区表/文档,引用位置见各节链接。 > 本文所有结论都在仓库里逐条对过代码/脚本/分区表/文档,引用位置见各节链接。
@@ -12,22 +13,22 @@
1. **“不改固件源码”可以做到;但“真 OTA”做不到。** 当前固件没有 `ota_0/ota_1/otadata` 分区, 1. **“不改固件源码”可以做到;但“真 OTA”做不到。** 当前固件没有 `ota_0/ota_1/otadata` 分区,
没有 `esp_ota` 调用,UDP 协议里也没有任何命令通道(只有 Hello/Subscribe/视频/血量)。 没有 `esp_ota` 调用,UDP 协议里也没有任何命令通道(只有 Hello/Subscribe/视频/血量)。
设备自己没有能力接收固件。所谓“兼顾 OTA”,在现约束下只能是**现场 USB 串口更新**, 设备自己没有能力接收固件。所谓“兼顾 OTA”,在现约束下只能是**现场 USB 串口更新**,
或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。**这是必须先决的问题**。 或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。这是必须先决的问题。
2. **坏块规避其实根本不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回 2. **坏块规避其实不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回
`partitions.csv`(构建输入)再触发重建;而 **App 镜像不包含自己的分区偏移**,偏移只存在于 `partitions.csv`(构建输入)再触发重建;而 **App 镜像不包含自己的分区偏移**,偏移只存在于
`0x8000` 的 `partition-table.bin`(0xC00 字节 + 末尾 16 字节 MD5)。 `0x8000` 的 `partition-table.bin`(0xC00 字节 + 末尾 16 字节 MD5)。
→ 对同一份已构建镜像,**搜索偏移 + 重写分区表 + 原样写 App**,全程零编译。 → 对同一份已构建镜像,**搜索偏移 + 重写分区表 + 原样写 App**,全程零编译。
这是本方案最大的收益点,也是“模板 + patch”的第一块拼图。 这是本方案最大的收益点,也是“模板 + patch”的第一块拼图。
3. **SSID/密码不是 NVS,是 `.rodata` 里的字符串字面量**(`env!` 编译期注入)。 3. **SSID/密码不是 NVS,是 `.rodata` 里的字符串字面量**(`env!` 编译期注入)。
二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch **App 镜像**, 二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch App 镜像,
且必须连带处理**字符串长度常量**与**镜像 checksum/SHA256**。 且必须连带处理字符串长度常量与镜像 checksum/SHA256。
长度常量能否可靠定位是这个方案真正的技术风险(见 7.2)。 长度常量能否可靠定位是这个方案真正的技术风险(见 7.2)。
4. **坏块信息不能“读”出来,只能“写进去再读回来”差分出来。** `flash_id` 只给芯片型号与容量, 4. **坏块信息不能“读”出来,只能“写进去再读回来”差分出来。** `flash_id` 只给芯片型号与容量,
SFDP 只给参数,启动日志只给症状。仓库里 `0x10863` 的 bit6 挂在 1,就是整段回读比对发现的 SFDP 只给参数,启动日志只给症状。仓库里 `0x10863` 的 bit6 挂在 1,就是整段回读比对发现的
(HARDWARE.md)。因此客户端必须自带「写入图案 → 回读 → 差分」与「写后整段校验差分」两条链路。 (HARDWARE.md)。因此客户端必须自带「写入图案 → 回读 → 差分」与「写后整段校验差分」两条链路。
5. **推荐形态:纯逻辑核心库 `flash-core` + 优先 CLI + 桌面端加一页**。 5. **推荐形态:纯逻辑核心库 `flash-core` + 优先 CLI + 桌面端加一页**。
把最危险的逻辑(镜像格式、分区表、偏移搜索、补丁)做成不碰 I/O 的纯函数, 把最危险的逻辑(镜像格式、分区表、偏移搜索、补丁)做成不碰 I/O 的纯函数,
在主机上用**真实 1.1 MB 镜像做往返测试**——这与仓库已有的 `protocol/`、`stream_control`、 在主机上用真实 1.1 MB 镜像做往返测试——这与仓库已有的 `protocol/`、`stream_control`、
`link_control`「能在主机跑测试的策略代码」风格一致。 `link_control`「能在主机跑测试的策略代码」风格一致。
--- ---
@@ -59,7 +60,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
``` ```
- 每次启动都会用编译期值覆盖驱动配置:`wifi.set_configuration(&client_config(auth)?)` - 每次启动都会用编译期值覆盖驱动配置:`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)), - `EspDefaultNvsPartition` 只是被交给 `EspWifi::new`([src/main.rs:178](src/main.rs#L178)),
用于驱动自身的存储,不参与凭据解析。 用于驱动自身的存储,不参与凭据解析。
- 对比:[armor/armor-firmware/src/settings.rs](armor/armor-firmware/src/settings.rs) 的 C3 装甲板 - 对比:[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` 开始之间, - `0x10863` 位于**未分配空洞** `0x10000–0x70000`(`phy_init` 结束到 `factory` 开始之间,
380 KB 全是空闲 flash)。→ 这块空洞就是天然的**坏块探测试验场**,写坏也不要紧。 380 KB 全是空闲 flash)。→ 这块空洞就是天然的坏块探测试验场,写坏也不要紧。
- `0xFADE7` 落在 App 镜像的**代码段**内(镜像布局见 1.5),所以“把那个字节改成别的安全值” - `0xFADE7` 落在 App 镜像的**代码段**内(镜像布局见 1.5),所以“把那个字节改成别的安全值”
会改坏指令;**唯一安全的动作是挪分区**。 会改坏指令;唯一安全的动作是挪分区。
- 现行偏移已经漂过至少三次:`0x10000` → `0x50000`([LATENCY.md:88](LATENCY.md#L88))→ `0x70000` - 现行偏移已经漂过至少三次:`0x10000` → `0x50000`([LATENCY.md:88](LATENCY.md#L88))→ `0x70000`
(当前 [partitions.csv](partitions.csv))。每次重编都要重新核对,这正是要消灭的脆弱点。 (当前 [partitions.csv](partitions.csv))。每次重编都要重新核对,这正是要消灭的脆弱点。
- 缺陷有两种**方向相反**的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF” - 缺陷有两种方向相反的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF”
的检查里是**隐形**的,必须写 0 才暴露。探测逻辑必须两种都打图案。 的检查里是隐形的,必须写 0 才暴露。探测逻辑必须两种都打图案。
### 1.5 已实测出来的镜像布局(`fps-final-raw-serial.log` 逐段对齐算出) ### 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` / 编译时间。 - `esp_app_desc_t` 在 **App 分区 + 0x20**,内含 `version` / `project_name` / `idf_ver` / 编译时间。
**不需要让板子跑起来**,读 256 字节就能回答“这块板上到底是哪版固件”。 不需要让板子跑起来,读 256 字节就能回答“这块板上到底是哪版固件”。
- 分区表就是 0x8000 起的 0xC00 字节(`verify 0xc00 @ 0x8000` 佐证),末尾 16 字节 MD5。 - 分区表就是 0x8000 起的 0xC00 字节(`verify 0xc00 @ 0x8000` 佐证),末尾 16 字节 MD5。
改写偏移只要重算这 16 字节。 改写偏移只要重算这 16 字节。
@@ -142,14 +143,14 @@ const PASSWORD: &str = env!("WIFI_PASS");
- ✅ 现场只有一个程序;已有 NSIS 打包、CI、配置与密码闸门可复用。 - ✅ 现场只有一个程序;已有 NSIS 打包、CI、配置与密码闸门可复用。
- ✅ 视频端本来就要连板子,烧完顺手进监控很自然。 - ✅ 视频端本来就要连板子,烧完顺手进监控很自然。
- ❌ WebView 里跑长任务 + 进度 + 日志尾随,需要额外的事件/取消/背压设计。 - ❌ WebView 里跑长任务 + 进度 + 日志尾随,需要额外的事件/取消/背压设计。
- ❌ 烧录是**系统级、不可中断、会变砖风险**的操作,和“看画面”共享一个进程的失败域。 - ❌ 烧录是系统级、不可中断、有变砖风险的操作,和“看画面”共享一个进程的失败域。
- ❌ 无法在 CI / 无人值守 / 远程 SSH 场景复用。 - ❌ 无法在 CI / 无人值守 / 远程 SSH 场景复用。
### 方案 B(推荐):纯逻辑核心库 + 双前端(CLI 优先,GUI 复用) ### 方案 B(推荐):纯逻辑核心库 + 双前端(CLI 优先,GUI 复用)
`flash/core`(无 I/O,纯函数,主机可测)+ `flashctl` CLI + 桌面端后续加一页调用 Tauri 命令。 `flash/core`(无 I/O,纯函数,主机可测)+ `flashctl` CLI + 桌面端后续加一页调用 Tauri 命令。
- ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)**全部可在主机测**, - ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)全部可在主机测,
且可以用仓库里真实的 1.1 MB 镜像做往返验收。 且可以用仓库里真实的 1.1 MB 镜像做往返验收。
- ✅ CLI 先天可脚本化:CI 能跑、现场能远程跑、能出确定退出码与机器可读报告。 - ✅ CLI 先天可脚本化:CI 能跑、现场能远程跑、能出确定退出码与机器可读报告。
- ✅ GUI 后置,做完 CLI 后界面只是薄壳;失败域分开(GUI 崩了不影响烧录工具)。 - ✅ GUI 后置,做完 CLI 后界面只是薄壳;失败域分开(GUI 崩了不影响烧录工具)。
@@ -160,7 +161,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
- ✅ 落地最快,协议风险最低;`target/tools/espflash/espflash.exe` 已经在了。 - ✅ 落地最快,协议风险最低;`target/tools/espflash/espflash.exe` 已经在了。
- ✅ 与现有 `scripts/*.sh` 心智一致。 - ✅ 与现有 `scripts/*.sh` 心智一致。
- ❌ **拿不到回读差分**(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案 - ❌ 拿不到回读差分(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案
坏块检测的核心,只能再调一次 `read-flash` 自己比。 坏块检测的核心,只能再调一次 `read-flash` 自己比。
- ❌ 解析人类可读输出、处理 `esptool.py` 的 Python 依赖,现场极易碎(现有 flash.sh 就是被 - ❌ 解析人类可读输出、处理 `esptool.py` 的 Python 依赖,现场极易碎(现有 flash.sh 就是被
“macOS 串口名 + termios + 本地 IDF venv”绑死的)。 “macOS 串口名 + termios + 本地 IDF venv”绑死的)。
@@ -177,7 +178,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
| 交付成本 | 10% | 6 | 6 | **9** | | 交付成本 | 10% | 6 | 6 | **9** |
| 加权 | | 6.45 | **8.10** | 5.65 | | 加权 | | 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 原因 | 秒级 | 板子已经在现场、只能看现象时 | | 串口日志诊断 | 症状:`invalid header` / checksum 失败 / rst 原因 | 秒级 | 板子已经在现场、只能看现象时 |
要点: 要点:
- 图案探测必须**双向**:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形—— - 图案探测必须双向:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形,
`0x10863` 就是整段回读才发现就是这个原因。 `0x10863` 就是这样在整段回读里才发现的。
- 探测区选空洞而不是 NVS:**不要为了体检去毁 NVS**。若怀疑 NVS 区有坏点,单独提供 - 探测区选空洞而不是 NVS:**不要为了体检去毁 NVS**。若怀疑 NVS 区有坏点,单独提供
“备份 NVS → 探测 → 还原”的危险模式,默认关闭。 “备份 NVS → 探测 → 还原”的危险模式,默认关闭。
- probe 结束后必须 `erase` 干净,并**复核空洞回到 0xFF**,避免留下疑似数据。 - probe 结束后必须 `erase` 干净,并**复核空洞回到 0xFF**,避免留下疑似数据。
@@ -309,7 +310,7 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─
- **对齐放宽到 4 KiB**:App 分区只要求 4 KiB 对齐,`flash.sh` 用 64 KiB 是自我限制。 - **对齐放宽到 4 KiB**:App 分区只要求 4 KiB 对齐,`flash.sh` 用 64 KiB 是自我限制。
候选从 ~230 个变成 ~3700 个,命中概率高一个数量级(单坏点 1/8,双坏点也能稳过)。 候选从 ~230 个变成 ~3700 个,命中概率高一个数量级(单坏点 1/8,双坏点也能稳过)。
- **分区大小 ≠ 镜像大小**:`0x120000` 的分区里镜像只占 `0x10E700`,尾部 71 KiB 是空的。 - **分区大小 ≠ 镜像大小**:`0x120000` 的分区里镜像只占 `0x10E700`,尾部 71 KiB 是空的。
坏扇区落在尾部 slack 里**无害**,别把它算成阻塞。 坏扇区落在尾部 slack 里无害,别把它算成阻塞。
- **bootloader 与分区表固定偏移**:`0x0`(0x5160) 与 `0x8000`(0xC00) 挪不了。 - **bootloader 与分区表固定偏移**:`0x0`(0x5160) 与 `0x8000`(0xC00) 挪不了。
这里只能做**字节级吸收**:例如坏点落在分区表某条 `label` 字段里,就换一个 label 让那一位合法 这里只能做**字节级吸收**:例如坏点落在分区表某条 `label` 字段里,就换一个 label 让那一位合法
(label 是自由文本);落在 `magic/offset/size/MD5` 上则该板报废。这条路径要显式实现并明确其边界。 (label 是自由文本);落在 `magic/offset/size/MD5` 上则该板报废。这条路径要显式实现并明确其边界。
@@ -317,7 +318,7 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─
### 4.3 执行(`flash-core::parttable` + `image`) ### 4.3 执行(`flash-core::parttable` + `image`)
- 改写 `partition-table.bin` 中 `factory` 的 offset(和必要时 size)→ 重算末尾 16 字节 MD5。 - 改写 `partition-table.bin` 中 `factory` 的 offset(和必要时 size)→ 重算末尾 16 字节 MD5。
- **App 镜像原样写入新偏移**——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。 - App 镜像原样写入新偏移——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。
- 只有在“分区表/bootloader 必须字节吸收”这一层才动镜像,并且要重算 checksum/SHA256。 - 只有在“分区表/bootloader 必须字节吸收”这一层才动镜像,并且要重算 checksum/SHA256。
### 4.4 验证与回滚(`session` + `report`) ### 4.4 验证与回滚(`session` + `report`)
@@ -346,7 +347,7 @@ checksum = 0xEF XOR (所有段的所有数据字节) // 1 字节,位于
sha256 = SHA256(镜像中它之前的全部字节) // 32 字节,仅当 header.hash_appended=1 sha256 = SHA256(镜像中它之前的全部字节) // 32 字节,仅当 header.hash_appended=1
``` ```
任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在**真实镜像上做往返测试** 任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在真实镜像上做往返测试
(重算结果与文件里已有的字节必须完全相同)——这是整个方案最便宜、最有效的验收手段, (重算结果与文件里已有的字节必须完全相同)——这是整个方案最便宜、最有效的验收手段,
在碰硬件之前就能证明实现是对的。 在碰硬件之前就能证明实现是对的。
+4 -4
View File
@@ -20,7 +20,7 @@
当前 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 挂死、串口静默。
## 内存与任务配置 ## 内存与任务配置
@@ -38,7 +38,7 @@
| `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM` | `64` | 发送缓冲数量,使用内部 RAM | | `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM` | `64` | 发送缓冲数量,使用内部 RAM |
| `CONFIG_ESP_WIFI_TX_BA_WIN` | `32` | 保留 AMPDU 发送聚合 | | `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 搜索窗口之外,随后停止交付帧。因此默认关闭该配置;更换驱动后应单独验证持续采集。 原测试板启用 PSRAM DMA 后曾出现 `NO-EOI`:JPEG 结束标记位于驱动末尾 DMA 搜索窗口之外,随后停止交付帧。因此默认关闭该配置;更换驱动后应单独验证持续采集。
@@ -54,7 +54,7 @@
分区表位于 `0x8000`,S3 Bootloader 从 `0x0` 开始。App 写入分区表声明的偏移,不能直接套用 `0x10000`。修改分区或 Flash 模式后需重新构建,成套更新 Bootloader、分区表和 App。 分区表位于 `0x8000`,S3 Bootloader 从 `0x0` 开始。App 写入分区表声明的偏移,不能直接套用 `0x10000`。修改分区或 Flash 模式后需重新构建,成套更新 Bootloader、分区表和 App。
健康硬件可以评估恢复 `0x10000` App 偏移或更快 Flash 配置,但需重新验证。 健康硬件可以考虑把 App 偏移恢复到 `0x10000`,或改用更快的 Flash 配置,但需重新验证。
## 损坏 Flash 模组的兼容处理 ## 损坏 Flash 模组的兼容处理
@@ -62,7 +62,7 @@
2026-10-18 的整段回读还发现 **`0x10863`** 的 bit 6 异常读回为 1:期望 `0x2e`,读回 `0x6e`,该轮 App 只有这一处字节不同,尚未单独擦写复核是否为永久故障。该模组的 App 偏移需至少为 `0x20000`,并继续检查 `0xFADE7`;烧录工具的校验失败必须先排除,不能当作网络或相机故障。 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 流程。更换故障模组比长期依赖镜像位置更可靠。 脚本依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录及旧版工具参数,不适合作为通用烧录入口。坏板需核对工具版本及坏地址字节;普通板卡使用 README 流程。更换故障模组比长期依赖镜像位置更可靠。
+5 -5
View File
@@ -1,14 +1,14 @@
# 约 30 fps、Wi-Fi 重连与画面链路延迟验证(2026-10-18) # 约 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。 针对现场轻微过亮,增加 `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)。 这次窗口的 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 换址尚未分别做硬件测试。启动阶段第一次联网仍使用原有扫描和认证模式探测逻辑。 实机诊断构建主动断开并保持 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 超时、指数退避和新断线重置状态。
## 延迟测量方法与边界 ## 延迟测量方法与边界
+4 -4
View File
@@ -1,6 +1,6 @@
# 图传性能排查与验证(2026-10-18) # 图传性能排查与验证(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 缓冲均未解决主要瓶颈。 传感器在 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 = 完整帧数 / 整个测量时长。帧损失包括被替换的不完整帧、超时及帧号缺口;末尾仍在接收的帧不计损失。丢帧率与旧客户端的口径不同,旧版会低估实际丢帧。 表中 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。 约 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 最终绘制像素。
+7 -7
View File
@@ -2,7 +2,7 @@
当前交付请求 `WIFI_TX_POWER_DBM=15`,实板驱动回读 **15.00 dBm**。根据用户的距离与拥塞反馈,从 17 dBm 回退;启动和 Wi-Fi 重连后应用同一设置,压缩与温控控制器不自动降低发射功率。三缓冲、VGA 彩色、AE 目标 45、约 20 ms 曝光上限和 PLL 时钟保持不变。 当前交付请求 `WIFI_TX_POWER_DBM=15`,实板驱动回读 **15.00 dBm**。根据用户的距离与拥塞反馈,从 17 dBm 回退;启动和 Wi-Fi 重连后应用同一设置,压缩与温控控制器不自动降低发射功率。三缓冲、VGA 彩色、AE 目标 45、约 20 ms 曝光上限和 PLL 时钟保持不变。
这里的数值是驱动配置的发射功率上限。API 回读不能代替功率计,不能认为所有 PHY 速率下、每个数据包的天线端输出都恰好相同。 这里的数值是驱动配置的发射功率上限。API 回读不能代替功率计,也不能据此认为所有 PHY 速率下、每个数据包的天线端输出都恰好相同。
## 先前 17 dBm 记录说明了什么 ## 先前 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 | 29.78 fps | 0.667% | 90 / 80 / 10(整段串口) |
| 17 dBm 第二轮,增加周期回读 | 视频 30 s / 串口 75 s | 27.92 fps | 6.889% | 498 / 446 / 52(整段串口) | | 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)。 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 的旧分片。 已确认的应用行为是:每约 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 记录也不能证明射频供电完全没有瞬态变化。 更高功率改变干扰覆盖、射频线性度或供电瞬态,也可能间接影响链路,但这里没有相应测量,不将“接收端过载”“功放饱和”或“电压跌落”写成已确认原因。无 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 重传和信道忙占比。 下一项值得单独对照的是**分片突发整形**:固定 15 dBm、曝光和 JPEG 策略,按发送回压逐步比较小批次提交与短暂让出调度,并同时看原生完整帧率、网络丢帧、主动跳帧和帧龄。不能仅通过增大缓冲或延长重试,把丢帧转化为持续排队延迟。若要确认距离瓶颈,需要同步记录远点断连原因、AP 侧板卡 RSSI、MAC 重传和信道忙占比。
@@ -59,6 +59,6 @@ ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲
## 15 dBm 本轮结果 ## 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 内容不同,不能将差值归因于功率这一项。 按时间近似对齐的板端五分钟区间有 731 次发送失败、653 次应用重试、78 次目标帧放弃;温度 56–58°C,RSSI -35 至 -11 dBm。板端区间由五秒采样的结束时间选择,边界并非逐包精确对齐。与此前短测时长、现场无线状态和 JPEG 内容不同,不能将差值归因于功率这一项。
+85 -44
View File
@@ -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 装甲板的血量广播,并负责固件维护。
## 功能与架构 当前版本 **v0.2.1**(构建号 35,线路协议 1)。仓库已从 `ESP32S3_vt` 改名为 `simba-rmic-2026`,旧地址会重定向到新地址:
- 摄像头自动探测、JPEG 采集、UDP 分片发送与完整帧重组。
- 局域网广播发现与订阅,单块图传板最多向 4 个客户端发送画面。
- 运行中 Wi-Fi 断开后自动重连,重新获得 IP 后重建 UDP socket 并恢复订阅。
- 使用 MAC 派生的稳定设备 ID;两块图传板可烧录同一固件,在客户端绑定队伍。
- Tauri v2 + React 桌面端:视频显示、双方血量、设备绑定与录制。
- 独立 ESP32-C3 装甲板固件:四路受击检测、WS2812 灯带及血量上报。
```text ```text
ESP32-S3 图传板 A ─┐ ┌─ 队伍客户端 git@git.simba-robotics.com:PRAM/simba-rmic-2026.git
├─ 同一局域网 / UDP ─┼─ 裁判客户端
ESP32-S3 图传板 B ─┤ └─ 队伍客户端
ESP32-C3 装甲主板 ─┘
``` ```
图传使用 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 协议与帧重组,支持主机测试 | | `protocol/` | 共享 UDP 协议与帧重组,支持主机测试 |
| `armor/` | 独立 ESP32-C3 装甲板 workspace | | `armor/` | 独立 ESP32-C3 装甲板 workspace |
| `desktop/` | Tauri + React 客户端及独立网络核心 `core/` | | `desktop/` | Tauri + React 客户端及独立网络核心 `core/` |
| `scripts/` | 构建、烧录与探测脚本 | | `scripts/` | 构建、烧录、打包与探测脚本 |
| `reference/armor/` | 早期 C 版装甲板工程 | | `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 引脚。 默认采集 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 工具 ### 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` 中。 先安装 [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 主机工具链。仓库和工具链尽量使用较短、无空格和中文的路径。 - 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`。 - 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。 - macOS:安装 Xcode Command Line Tools(`xcode-select --install`),以及 Python、CMake 和 Ninja。
以下命令显式使用 `+stable`,即使在仓库内执行,也不会要求事先已有 `esp`: 以下命令显式使用 `+stable`,即使在仓库内执行,也不会要求事先已有 `esp`:
@@ -89,7 +107,7 @@ espflash --version
### 2. 准备 ESP-IDF ### 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`,不要提交个人绝对路径: 已有匹配版本 SDK 时可在终端设置 `IDF_PATH`,不要提交个人绝对路径:
@@ -142,6 +160,8 @@ WIFI_PASS='你的Wi-Fi密码'
#CAM_THERMAL_LIMIT_C=75 #CAM_THERMAL_LIMIT_C=75
#CAM_VSYNC_INVERT=1 #CAM_VSYNC_INVERT=1
#WIFI_AUTH=wpa2 #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_SSID` | 无,必填 | 2.4 GHz 网络名称,最多 32 字节 |
| `WIFI_PASS` | 空字符串 | Wi-Fi 密码;开放网络可留空 | | `WIFI_PASS` | 空字符串 | Wi-Fi 密码;开放网络可留空 |
| `CAM_RES` | `vga` | `qqvga` / `qvga` / `cif` / `hvga` / `vga` / `svga` / `xga` / `sxga` / `uxga` | | `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_QUALITY_MAX` | `0` | `0` 固定画质;设为 `CAM_QUALITY`–63 时按发送回压在两者之间调节压缩,保留彩色和分辨率。拥塞时每 5 秒增大最多 3 档,连续稳定 15 秒才恢复 1 档 |
| `CAM_MAX_FPS` | `0` | 取帧与发送的最高节拍,1–60;`0` 不限帧。仅限制消费节拍,不修改传感器时钟 | | `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。切换后需重新核对曝光行数 | | `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;分片间隔也计入期限,不会额外延长 | | `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_THERMAL_LIMIT_C` | `75` | 芯片内部温度达到该值时减半帧率(向上取整),下降 5°C 后恢复;40–95,`0` 禁用。原先不限帧时降至 8 fps |
| `CAM_VSYNC_INVERT` | `1` | 驱动反相 VSYNC;硬件已反相时填 `0` | | `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_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 忽略;固件也包含凭据,分享前应重新配置。 `.env` 使用简单的 `KEY=value`;值含空格、`#` 或 `$` 时用单引号包围,不使用变量展开或命令替换。脚本会加载 `.env`,直接 `cargo build` 不会自动读取它。上述参数在编译时写入固件,修改后需要重新编译和烧录。`.env` 已被 Git 忽略;固件也包含凭据,分享前应重新配置。
@@ -187,7 +208,7 @@ $env:CARGO_TARGET_DIR = 'C:\s3vt'
./scripts/build.sh ./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` 不能绕过该检查。下表的 `<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`。
@@ -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" 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)。 `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 ```text
cd desktop cd desktop
@@ -248,7 +269,26 @@ pnpm install
pnpm app 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 | | 相机无输出 / 初始化失败 | 核对板型接线、供电与 Octal PSRAM |
| `NO-EOI`、几帧后停止 | 确认 `CONFIG_CAMERA_PSRAM_DMA=n` | | `NO-EOI`、几帧后停止 | 确认 `CONFIG_CAMERA_PSRAM_DMA=n` |
| 在线但没有画面 | 检查客户端订阅、UDP 5600、防火墙及 AP 隔离 | | 在线但没有画面 | 检查客户端订阅、UDP 5600、防火墙及 AP 隔离 |
| 界面提示协议不匹配 | 板子与客户端的线路协议号不同;按提示重新烧录同版本的固件 |
| 开启 VPN / 虚拟网卡后设备在线但无画面 | 使用支持按已发现设备 IP 定向续订的客户端;仅发送 `255.255.255.255` 广播可能走错网卡 | | 开启 VPN / 虚拟网卡后设备在线但无画面 | 使用支持按已发现设备 IP 定向续订的客户端;仅发送 `255.255.255.255` 广播可能走错网卡 |
| 改 C 组件后无变化 | 清理 `esp-idf-sys` 构建缓存后重编,见开发说明 | | 改 C 组件后无变化 | 清理 `esp-idf-sys` 构建缓存后重编,见开发说明 |
| 主机项目被编为 Xtensa | 显式指定宿主 target,见各子项目说明 | | 主机项目被编为 Xtensa | 显式指定宿主 target,见各子项目说明 |
@@ -277,7 +318,7 @@ python scripts/perf_probe.py 192.168.1.139 --seconds 30 --json perf.json
脚本定向续订,支持保留 VPN / TUN 的环境。输出包含完整帧率、帧损失率、接收间隔和组装耗时;这些时间不等于摄像头到屏幕的端到端延迟。 脚本定向续订,支持保留 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 与发布前用) 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`,单调、不撞车。 - 构建号 = `git rev-list --count HEAD`,单调、不撞车。
- 同一次计算还产出 `version.json`(固件 `build.rs` 读它)、`CHANGELOG.json`、 - 同一次计算还产出 `version.json`(固件 `build.rs` 读它)、`CHANGELOG.json`、`desktop/src/lib/version.ts`。
`desktop/src/lib/version.ts`。
**固件与客户端的版本是联动过的**:板子每 2 秒广播的 `Hello` 里带着语义化版本、构建号与 固件与客户端的版本是联动的:板子每 2 秒广播的 `Hello` 里带着语义化版本、构建号与它编译进去的线路协议号;客户端读出来显示在设置与裁判端每一格下面,协议对不上时直接说明原因(“板子是协议 2,客户端是协议 1,画面会被丢弃,请重新烧录”),而不是只表现为没有画面。
它编译进去的线路协议号;客户端读出来显示在设置与裁判端每一格下面,协议对不上时直接说明
原因("板子是协议 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 与驱动限制。 - [HARDWARE.md](HARDWARE.md):接线、内存、Flash 与驱动限制。
- [DEVELOPMENT.md](DEVELOPMENT.md):镜像导出、测试、协议与性能测量。 - [DEVELOPMENT.md](DEVELOPMENT.md):镜像导出、测试、协议与性能测量。
- [PERFORMANCE.md](PERFORMANCE.md):保留 TUN 的图传排查、参数取舍与实际测试结果。 - [PERFORMANCE.md](PERFORMANCE.md):保留 TUN 的图传排查、参数取舍与实际测试结果。
- [STABILITY.md](STABILITY.md):限帧、灰度与温度减载,长时间运行测试。 - [STABILITY.md](STABILITY.md):限帧、灰度与温度减载,长时间运行测试。
- [REALTIME.md](REALTIME.md):阻塞时取最新帧、超期丢弃和客户端积压处理。 - [REALTIME.md](REALTIME.md):阻塞时取最新帧、超期丢弃和客户端积压处理。
- [POWER.md](POWER.md):15 dBm 回退、功率与距离的关系,以及发送拥塞证据。 - [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 线路格式及帧重组。 - [protocol/README.md](protocol/README.md):UDP 线路格式及帧重组。
- [armor/README.md](armor/README.md):装甲板固件与规则。 - [armor/README.md](armor/README.md):装甲板固件与规则。
- [desktop/README.md](desktop/README.md):桌面端配置、录制与排查。 - [desktop/README.md](desktop/README.md):RMIC::2026 的配置、录制与排查。
- [docs/firmware-provisioning-evaluation.md](docs/firmware-provisioning-evaluation.md):自动烧录、坏区规避与 SSID 配置 patch 的工具评估、MVE 与测试矩阵。
- [docs/firmware-maintenance.md](docs/firmware-maintenance.md):已实现的固件维护页、官方 bundle 格式、坏块规避流程与实机验证记录。 - [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):建立自动版本机制之前的人工改动记录。
+5 -5
View File
@@ -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;阻塞恢复后通过有限次数的零等待取帧选择最新有效帧,旧帧立即归还驱动。三块采集内存用于捕获余量,与必须按序播放的三帧队列不同。已有相机队列仍然有限,应用交接会跳过其旧帧。
完整 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 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 中已经接受后无法由本策略撤回,底层调度和网络排队也无法由应用层强制抢占。以上为应用层新鲜度策略,不能保证每帧、无线链路或物理屏幕具有硬实时上限。 分片间隔也计入 25 ms 检查期限,不再额外延长帧的发送寿命;设置过大的间隔可能使整帧无法在期限内提交。分片在 SDK socket 中已经接受后无法由本策略撤回,底层调度和网络排队也无法由应用层强制抢占。以上为应用层新鲜度策略,不能保证每帧、无线链路或物理屏幕具有硬实时上限。
+5 -5
View File
@@ -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。 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,实际值由场景与频闪步长决定。 该阶段采用正常亮度目标、自动 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,不通过降低色深来提帧。
彩色 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 大小和网络负载在各轮之间变化,不能把全部差异归因于单一控制参数,也不能将五分钟结果作为长期或所有路由器的保证。帧间隔不是摄像头到屏幕的光学延迟。 这次完整接收帧率接近当前可靠彩色时序的约 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 错误,随后系统不再枚举开发板串口,摄像头同时离线。观察器提前停止,未完成有效的五分钟对照,断连尾段不能归因于调度。 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;全程在线、无采集错误。它保留为拥塞对照,未被选为最终配置。 固定 20 ms 的完整五分钟轮次接收 5220 帧、丢帧 1430(21.50%),接收 17.40 fps,本地 JPEG 通道 13.45 fps,接收间隔 P95 277.473 ms、最大 1163.792 ms;全程在线、无采集错误。它保留为拥塞对照,未被选为最终配置。
+27 -28
View File
@@ -1,8 +1,8 @@
# 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与裁判终端联动 # 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与 RMIC::2026 桌面端联动
一辆车四块装甲板(前/后/左/右),**只有主板一颗 MCU**:另外三块"从"板里没有单片机, 一辆车四块装甲板(前/后/左/右),**只有主板一颗 MCU**:另外三块"从"板里没有单片机,
各自只有一路 WS2812 和一路压电片,直接接线到主板上。主板负责四路击打检测、四路灯带、 各自只有一路 WS2812 和一路压电片,直接接线到主板上。主板负责四路击打检测、四路灯带、
整车血量,并把血量广播给本项目裁判终端。 整车血量,并把血量广播给 RMIC::2026 桌面端。
固件是跑在 ESP-IDF 上的 Rust,目标芯片 **ESP32-C3(RISC-V)**。 固件是跑在 ESP-IDF 上的 Rust,目标芯片 **ESP32-C3(RISC-V)**。
@@ -28,10 +28,10 @@ armor/
| 功能 | GPIO | 说明 | | 功能 | GPIO | 说明 |
| --- | --- | --- | | --- | --- | --- |
| WS2812 前 | **IO10** | 每条灯带 3 颗灯珠,GRB、MSB first | | WS2812 前 | IO10 | 每条灯带 3 颗灯珠,GRB、MSB first |
| WS2812 后 | **IO5** | | | WS2812 后 | IO5 | |
| WS2812 左 | **IO6** | | | WS2812 左 | IO6 | |
| WS2812 右 | **IO7** | | | WS2812 右 | IO7 | |
| 压电片 前 | GPIO0(ADC1_CH0) | 0 dB 衰减、12 bit | | 压电片 前 | GPIO0(ADC1_CH0) | 0 dB 衰减、12 bit |
| 压电片 后 | GPIO4(ADC1_CH4) | | | 压电片 后 | GPIO4(ADC1_CH4) | |
| 压电片 左 | GPIO3(ADC1_CH3) | | | 压电片 左 | GPIO3(ADC1_CH3) | |
@@ -62,8 +62,8 @@ armor/
| 复位(新一局) | 长按 BOOT ≥500 ms → 满血、清计数 | `Button` + `Armor::reset` | | 复位(新一局) | 长按 BOOT ≥500 ms → 满血、清计数 | `Button` + `Armor::reset` |
| 打印状态 | 短按 BOOT → 立刻打一行状态并重发上报 | `ButtonEvent::Click` | | 打印状态 | 短按 BOOT → 立刻打一行状态并重发上报 | `ButtonEvent::Click` |
**整车一条血量,四格同值。** 板子算账、裁判端只显示:每条上报的 `hp` 相同, **整车一条血量。** 板子算账、裁判端只显示:四条上报的 `hp` 相同,`index` 分别是 0..3,
`index` 分别是 0..3,所以裁判端已经画好的四格血量界面不用改。 界面按 `(team, index)` 收齐四条后显示同一个数值。
**为什么要"每板独立冷却"。** 参考实现用全局 50 ms 冷却:一块板挨打后,别块板在这 **为什么要"每板独立冷却"。** 参考实现用全局 50 ms 冷却:一块板挨打后,别块板在这
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 是裁判 - `sender_id` 用 MAC 派生的板子 id(`protocol::id_from_mac`),不是 0 —— 0 是裁判
终端的保留 id,抓包时一块装甲板发的包不该长得像裁判端发的。 终端的保留 id,抓包时一块装甲板发的包不该长得像裁判端发的。
- 四条 `index` 的 `hp` 相同(整车一条血),裁判端按 `(team, index)` 收齐四条才显示。 - 四条 `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)` 存最后一次读数。 - 绑 `0.0.0.0:5601` 收广播,按 `(team, index)` 存最后一次读数。
- **四条都在 1.5 s 内收到**才交给界面显示;否则回退占位符。心跳 200 ms,1.5 s 是连丢 - **四条都在 1.5 s 内收到**才交给界面显示;否则回退占位符。心跳 200 ms,1.5 s 是连丢
7 个包,偶发丢包不会让四个格子各自跳。 7 个包,偶发丢包不会让读数来回跳。
- 队伍端不用它(界面不显示对手血量),所以那台机器不会去占 5601。 - 用户端也要显示双方血条,所以两端都会占 5601。
- 血量变化会打日志(`armor: team 0 hp 100 -> 97`):窗口是给别人看的,抓日志的人 - 血量变化会打日志(`armor: board 0x… team 0 hp 100 -> 97`):窗口是给别人看的,
也得能看见掉血。 抓日志的人也得能看见掉血。
**裁判端界面原本看不到血量。** 协议与 `ArmorSource` 早已就位,但 Slint 里那 4 格只是 **界面显示血量数字。** `TeamHealthDto` 带 `hp` 与 `critical`:数值就在队伍名旁边,低于
绿/灰方块 —— `hps` 的数值从没被渲染,于是"板子被打到 0"在界面上完全看不出来(现场 30 或归零时它与血条一起转红。归零**不写"已击毁"**——那一刻图传中央会给出
表现就是"敲死了裁判系统没掉血")。现在 `TeamHealth` 多了一个 `hp-text` 与 `critical`: 「胜利 / 击败」的结论(见 [`desktop/README.md`](../desktop/README.md))。
队伍名旁边直接显示血量数字,低于 30 或归零时数字与血条转红。归零**不写"已击毁"**——那一刻
图传中央会给出「胜利 / 击败」的结论(见 [`desktop/README.md`](../desktop/README.md))。
## 构建 ## 构建
@@ -157,7 +155,9 @@ cd protocol && cargo test # 线路格式往返(含 ArmorHealth
``` ```
(`armor-protocol/.cargo/config.toml` 把 target 覆盖回 host:它坐在 `armor/` 目录树 (`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/重启;状态行每秒一条 | | 启动 | 无 panic/重启;状态行每秒一条 |
| WiFi | 关联 `SR-602-2G4`(rssi −36,ch 6),DHCP 拿到 `192.168.1.117` | | WiFi | 关联 `SR-602-2G4`(rssi −36,ch 6),DHCP 拿到 `192.168.1.117` |
| 上报 | `scripts/armor_probe.py` 收到队伍一四条、约 5 包/秒、`sender_id 0xA820D321`、四格同值 | | 上报 | `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 计数 | | 受击 | 敲前装甲板:`hit plate=front raw=2663..4095`,每次 −3(100→97→94…),只有 front 计数 |
| 变暗 | 敲击瞬间**只有前那一路**全黑 50 ms,其余不变(现场确认) | | 变暗 | 敲击瞬间**只有前那一路**全黑 50 ms,其余不变(现场确认) |
| 死亡 | 34 击到 0;状态行 `dead=true strips[front=off rear=off left=off right=off]`,四路全黑 | | 死亡 | 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 格只是绿/灰方块, 协议、`ArmorSource`、`ArmorHealthStore` 都对,但改之前旧界面上那几格只是绿/灰方块,
`hps` 的**数值从不渲染**——板子被打到 0,界面四格还是绿的,看起来像"没掉血"。现在 `hps` 的**数值从不渲染**——板子被打到 0,格子还是绿的,看起来像"没掉血"。修好之后
队伍名旁边直接显示数字(低于 30 转红,归零不写"已击毁",改由图传中央的胜负横幅表达), 队伍名旁边直接显示数字,客户端日志也会打 `armor: board 0x… team 0 hp 100 -> 97`。
并且客户端日志会打 `armor: 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 | 每板独立 50 ms | 别块板的击打不该被吃掉 | | 冷却窗口 | 全局 50 ms | 每板独立 50 ms | 别块板的击打不该被吃掉 |
| 灯带刷新 | 每 10 ms 无条件刷四遍 | 只在内容变化时刷 + 每秒强制全刷 | 四路独立控制后,省掉四轮 RMT 开销 | | 灯带刷新 | 每 10 ms 无条件刷四遍 | 只在内容变化时刷 + 每秒强制全刷 | 四路独立控制后,省掉四轮 RMT 开销 |
| 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端已画好的四格血量对齐 | | 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端界面的血量显示对齐 |
| 颜色切换 | BOOT 短按切红/蓝,存 NVS | 颜色由 `ARMOR_TEAM` 决定(红/蓝),短按打状态 | 队别不该靠现场按键切错 | | 颜色切换 | BOOT 短按切红/蓝,存 NVS | 颜色由 `ARMOR_TEAM` 决定(红/蓝),短按打状态 | 队别不该靠现场按键切错 |
| 按键 | 移植 OneButton 状态机(短按配对,长按切色) | 本地 FSM:长按复位血量、短按打状态 | 只有两种动作,且要能在主机上测 | | 按键 | 移植 OneButton 状态机(短按配对,长按切色) | 本地 FSM:长按复位血量、短按打状态 | 只有两种动作,且要能在主机上测 |
| 上报 sender_id | —(不走 UDP) | MAC 派生 id | 0 是裁判终端保留 id | | 上报 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 即可。 `armor-protocol` 里已经预留了板位枚举与上报格式,加一个 `slave` bin 即可。
- **双击 / 多击**:`Button` 只实现短按与长按,参考实现里未使用的 OneButton 功能没有移植。 - **双击 / 多击**:`Button` 只实现短按与长按,参考实现里未使用的 OneButton 功能没有移植。
- **下发血量**:当前板子本地算账(裁判端只显示)。若以后要裁判端权威(下发血量/复位), - **下发血量**:当前板子本地算账(裁判端只显示)。若以后要裁判端权威(下发血量/复位),
需要新增一条下行消息并在 `client` 里发。 需要新增一条下行消息并在 `desktop` 里发。
+26 -29
View File
@@ -1,15 +1,15 @@
# RMIC::2026 # RMIC::2026
黑龙江大学 602 实验室 · Simba Robotics 的**视野链路终端**。 黑龙江大学 602 实验室 · Simba Robotics 的**视野链路终端**,桌面端。
桌面端。**两种身份,两套页面,一个进程。** 两种身份、两套页面,都在同一个进程里。
| 身份 | 画面 | 血量 | 装甲板 | 绑定 | | 身份 | 画面 | 血量 | 装甲板 | 绑定 |
|---|---|---|---|---| |---|---|---|---|---|
| 用户端(`team`) | 一路 | **双方**(左上角自己 / 右上角对手) | 自己那块 | **要,改动需密码** | | 用户端(`team`) | 一路 | **双方**(左上角自己 / 右上角对手) | 自己那块 | **要,改动需密码** |
| 裁判端(`referee`) | 两路并排 | 双方(各自贴画面角上,**队名可改**) | 发现列表 + 槽位 | 不需要 | | 裁判端(`referee`) | 两路并排 | 双方(各自贴画面角上,**队名可改**) | 发现列表 + 槽位 | 不需要 |
界面是 WebView 里的 React(画面画在 canvas 上),后端是 Rust。**它不是把旧客户端套进 界面是 WebView 里的 React(画面画在 canvas 上),后端是 Rust。它不是把旧客户端套进
一个窗口**:网络、重组、发现、配置、密码、录制全部重写成了可单测的模块。 一个窗口:网络、重组、发现、配置、密码、录制全部重写成可单测的模块。
> **名字与数据目录**:显示名是 `RMIC::2026`;安装包与可执行文件叫 `RMIC-2026`(Windows > **名字与数据目录**:显示名是 `RMIC::2026`;安装包与可执行文件叫 `RMIC-2026`(Windows
> 文件名不允许 `:`)。配置、录制、日志仍落在旧的 `cam-desktop` 目录下——这是**故意的**, > 文件名不允许 `:`)。配置、录制、日志仍落在旧的 `cam-desktop` 目录下——这是**故意的**,
@@ -18,7 +18,7 @@
```text ```text
desktop/ desktop/
core/ cam-core:不含任何界面与 Tauri 的东西(83 个测试) core/ cam-core:不含任何界面与 Tauri 的东西(83 个测试)
src-tauri/ Tauri 后端:推流、命令、录制(32 个测试) src-tauri/ Tauri 后端:推流、命令、录制(40 个测试)
src/ React 前端:两套页面 + HUD src/ React 前端:两套页面 + HUD
src/lib/ IPC 封装、WebSocket 客户端、状态存储、格式化、准星绘制、界面小节拍 src/lib/ IPC 封装、WebSocket 客户端、状态存储、格式化、准星绘制、界面小节拍
src/components/ 取景器、装甲条、命中、结算、装甲板表、队名、录制、设置抽屉、启动序列、门、提示 src/components/ 取景器、装甲条、命中、结算、装甲板表、队名、录制、设置抽屉、启动序列、门、提示
@@ -30,8 +30,6 @@ desktop/
## 界面:一套终端语言 ## 界面:一套终端语言
这一栏不是装饰说明,而是"改界面之前该知道的几件事"。
### 配色:墨黑蓝打底,只有一支红 ### 配色:墨黑蓝打底,只有一支红
| 角色 | 值 | 用在哪 | | 角色 | 值 | 用在哪 |
@@ -43,14 +41,14 @@ desktop/
| 队伍 | `--team-0 #ff6b5e` / `--team-1 #4a9bff` | **硬件约束**:装甲板 LED 就是红与蓝 | | 队伍 | `--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` 里带着**语义化版本、构建号、以及它编译进去的线路协议号** 板子每 2 秒广播的 `Hello` 里带着**语义化版本、构建号、以及它编译进去的线路协议号**
(布局见 `protocol/README.md`)。客户端读出来之后: (布局见 `protocol/README.md`)。客户端读出来之后:
@@ -178,6 +176,7 @@ node scripts/version.mjs --print # 把算出来的版本打到 stdout
**为什么值得占这 10 个字节**:一块烧错固件的板子,原来只表现为"没画面",现场会去查网线、 **为什么值得占这 10 个字节**:一块烧错固件的板子,原来只表现为"没画面",现场会去查网线、
AP、供电。现在同一个现象会直接指向"重新烧固件"。 AP、供电。现在同一个现象会直接指向"重新烧固件"。
## 快速开始 ## 快速开始
```sh ```sh
@@ -192,7 +191,7 @@ pnpm tauri build # 默认打包 macOS 的 .app 与 .dmg;Windows 见下文
```sh ```sh
cd desktop/core && cargo test # 83 cd desktop/core && cargo test # 83
cd desktop/src-tauri && cargo test # 32,其中几条真的会起 ffmpeg cd desktop/src-tauri && cargo test # 40,其中几条真的会起 ffmpeg
``` ```
### Windows x64 构建 ### Windows x64 构建
@@ -214,7 +213,7 @@ pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --ci -- --locked
视频录制另需可从 `PATH` 找到的 `ffmpeg`,也可以在配置的 `[recording] ffmpeg` 中指定路径;基础图传显示不依赖它。 视频录制另需可从 `PATH` 找到的 `ffmpeg`,也可以在配置的 `[recording] ffmpeg` 中指定路径;基础图传显示不依赖它。
设备发现使用板卡的 Hello 广播;客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口;定向续订可避免“能发现并绑定,但始终等待画面”的情况。队伍端的绑定和画面过滤仍然生效。 设备发现使用板卡的 Hello 广播;客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口;定向续订可避免"能发现并绑定,但始终等待画面"的情况。队伍端的绑定和画面过滤仍然生效。
### 演示模式:没有板子也能把整条链路跑起来 ### 演示模式:没有板子也能把整条链路跑起来
@@ -223,8 +222,8 @@ cd desktop
CAM_DEMO=1 pnpm app CAM_DEMO=1 pnpm app
``` ```
它**不伪造数据给界面**,而是合成**协议数据报**,从 `Receiver::datagram` 与 它不伪造数据给界面,而是合成协议数据报,从 `Receiver::datagram` 与
`ArmorHealthStore::datagram` 进去。也就是说演示模式跑的是同一条真实路径: `ArmorHealthStore::datagram` 进去。演示模式跑的是同一条真实路径:
```text ```text
合成 JPEG ──分片──▶ Receiver::datagram ──重组──▶ 帧 sink ──▶ WebSocket ──▶ canvas 合成 JPEG ──分片──▶ Receiver::datagram ──重组──▶ 帧 sink ──▶ WebSocket ──▶ canvas
@@ -273,7 +272,7 @@ CAM_CONFIG=<路径> 等价于 --config <路径>
## 视频怎么进 WebView ## 视频怎么进 WebView
**一个只监听回环地址的 WebSocket,推二进制 JPEG。** 界面从 `stream_info` 命令拿到 视频走一个只监听回环地址的 WebSocket,推二进制 JPEG。界面从 `stream_info` 命令拿到
`ws://127.0.0.1:<随机端口>/?token=...`。 `ws://127.0.0.1:<随机端口>/?token=...`。
### 为什么不是 Tauri 的 IPC ### 为什么不是 Tauri 的 IPC
@@ -307,8 +306,8 @@ CAM_CONFIG=<路径> 等价于 --config <路径>
对抗赛里两队都用这套系统看自己小车的图传,要防止对手开第二个窗口连到另一队的板子。 对抗赛里两队都用这套系统看自己小车的图传,要防止对手开第二个窗口连到另一队的板子。
**关键是这件事做在接收端,不是界面里。** 广播的 Hello 里就有 MAC 派生的 `sender_id` 与 关键是这件事做在接收端,不是界面里。广播的 Hello 里就有 MAC 派生的 `sender_id` 与
IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/CSS 只控制显示。 IP,藏起来毫无意义;数据也可能已经收下并解码了,CSS 只控制显示。
| 闸门 | 管什么 | 什么时候开 | | 闸门 | 管什么 | 什么时候开 |
|---|---|---| |---|---|---|
@@ -316,10 +315,10 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/
| `discovery_all` | 是否记录**其它**板子的 Hello | 只在密码解锁后的设置界面里打开 | | `discovery_all` | 是否记录**其它**板子的 Hello | 只在密码解锁后的设置界面里打开 |
旧客户端把两件事绑在同一道闸门上,于是只能在"能列出候选"和"挡住对手"之间二选一,改绑 旧客户端把两件事绑在同一道闸门上,于是只能在"能列出候选"和"挡住对手"之间二选一,改绑
只能靠手改配置文件。现在分开了:设置界面能列出候选摄像头(否则无从选起),但**画面不进 只能靠手改配置文件。现在分开了:设置界面能列出候选摄像头(否则无从选起),但**画面进不来**:
来**——对手的板子即使把流推到本机端口、即使被记进发现列表,它的帧也进不了重组器。 对手的板子即使把流推到本机端口、即使被记进发现列表,它的帧也进不了重组器。
四种情况不限制:裁判端(中立方)、debug 构建、用户端还没绑定、`--camera-id` 显式覆盖。 `only_sender` 只在两种情况下放开:裁判端(中立方),以及用户端还没绑定摄像头。debug 构建不会放开它。
### 遥测按编译模式门控 ### 遥测按编译模式门控
@@ -329,7 +328,7 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/
### 这挡不住什么 ### 这挡不住什么
诚实地说:**IP 仍然通过 Hello 广播泄露。** 对手知道你的 IP 之后可以自己发一个 **IP 仍然通过 Hello 广播泄露。** 对手知道你的 IP 之后可以自己发一个
`Subscribe` 过来——固件会向任何订阅者推流,那道闸门在他的客户端上,不在你的。真要挡住 `Subscribe` 过来——固件会向任何订阅者推流,那道闸门在他的客户端上,不在你的。真要挡住
需要固件侧做配对 token(首次绑定发一个令牌,固件只接受带令牌的订阅),代价是固件重启就 需要固件侧做配对 token(首次绑定发一个令牌,固件只接受带令牌的订阅),代价是固件重启就
失配。 失配。
@@ -339,8 +338,6 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/
## 密码:它挡的是谁 ## 密码:它挡的是谁
必须说清楚,否则会被当成一把它并不是的锁。
- **挡得住**:现场随手改绑定。选手想把自己的画面换到对手那块板子上,得先知道密码;围观 - **挡得住**:现场随手改绑定。选手想把自己的画面换到对手那块板子上,得先知道密码;围观
的人翻不到明文(只有 Argon2id 哈希),也不能靠改配置文件把绑定改掉——改了之后界面会 的人翻不到明文(只有 Argon2id 哈希),也不能靠改配置文件把绑定改掉——改了之后界面会
要求先解锁。**退出/关闭程序**同样要密码,因为"随手关掉"也是一种要挡的误操作。 要求先解锁。**退出/关闭程序**同样要密码,因为"随手关掉"也是一种要挡的误操作。
@@ -532,7 +529,7 @@ AP、板子,而不是二十行分配代码。现在它有一个直接对着这
## 配置 ## 配置
`~/.config/cam-desktop/config.toml`,`--config` 可改。**目录名沿用旧的 `cam-desktop`**:改名不该顺手把现场的密码哈希与绑定丢掉。要干净重来就删掉这个目录。 `~/.config/cam-desktop/config.toml`,`--config` 可改。**目录名沿用旧的 `cam-desktop`**(原因见开头)。要干净重来就删掉这个目录。
```toml ```toml
role = "team" role = "team"
@@ -599,8 +596,8 @@ open 'http://127.0.0.1:1420/?preview=password' # 首启的设密码
```sh ```sh
cd desktop/core && cargo test # 83:重组、发现、两道闸门、帧率窗口、槽位、配置、密码、胜负判定 cd desktop/core && cargo test # 83:重组、发现、两道闸门、帧率窗口、槽位、配置、密码、胜负判定
cd desktop/src-tauri && cargo test # 32:帧头布局、录制、截图、演示画面、命令行 cd desktop/src-tauri && cargo test # 40:帧头布局、录制、截图、演示画面、命令行
cd protocol && cargo test # 20 cd protocol && cargo test # 23
``` ```
前端解码队列测试:在 `desktop/` 运行 `pnpm test:stream`,5 项测试覆盖慢解码时保留最新帧、多路独立处理、关闭清理、坏帧恢复与设备重启。它不替代实际 WebView 的画面验证。 前端解码队列测试:在 `desktop/` 运行 `pnpm test:stream`,5 项测试覆盖慢解码时保留最新帧、多路独立处理、关闭清理、坏帧恢复与设备重启。它不替代实际 WebView 的画面验证。
@@ -649,8 +646,8 @@ RUST_LOG=debug ./target/aarch64-apple-darwin/debug/cam-desktop | grep 'stream:'
DTO,所以至少能解释清楚。 DTO,所以至少能解释清楚。
- **`csp` 是一条收紧过的白名单**,但它是"够用"而不是"最小":`style-src` 带 - **`csp` 是一条收紧过的白名单**,但它是"够用"而不是"最小":`style-src` 带
`'unsafe-inline'`(React 的内联 `style` 属性需要它),`connect-src` 放开了整个 `'unsafe-inline'`(React 的内联 `style` 属性需要它),`connect-src` 放开了整个
`ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端" `ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端",
——见下面"测试"一节最后一条。 见"测试"一节最后一条。
- **证书与公证**没做,`.dmg` 只在本机可用。 - **证书与公证**没做,`.dmg` 只在本机可用。
- 前端已有解码队列测试;布局、实际 WebView 解码和画面显示仍需浏览器预览及真机运行验证。 - 前端已有解码队列测试;布局、实际 WebView 解码和画面显示仍需浏览器预览及真机运行验证。
+1 -1
View File
@@ -11,7 +11,7 @@ export const BUILD = {
minor: 2, minor: 2,
patch: 1, patch: 1,
/** git 提交数,单调递增。 */ /** git 提交数,单调递增。 */
build: 35, build: 36,
/** 线路协议版本。与固件自报的那个数直接比对。 */ /** 线路协议版本。与固件自报的那个数直接比对。 */
protocol: 1, protocol: 1,
/** 生成时的提交日期(`YYYY-MM-DD`)。 */ /** 生成时的提交日期(`YYYY-MM-DD`)。 */
+102
View File
@@ -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`。
File diff suppressed because it is too large Load Diff
+236
View File
@@ -0,0 +1,236 @@
# 固件维护页:实现说明
## 范围
本次只做 **USB 现场更新**(用户确认的 OTA 语义 = A),不做设备联网自升级。
- **不做** SSID 动态修改(用户已放弃该功能),App 镜像逐字节原样写入;
- **不做** 网络 OTA;当前固件没有 OTA 分区、没有 `esp_ota` 接收端、没有控制通道;
- **不改固件源码、不重编译**:使用当前已构建的官方 bundle,客户端只 patch 分区表的
factory 偏移/大小;
- 目标芯片以 ESP32-S3 为当前实现,`chip` 字段与 `espflash` 的芯片探测已经为 C3 预留,
但 C3 的 bundle/布局尚未验收。
## 为什么坏块规避不需要重编译
App 镜像不包含自己的分区偏移;偏移只在 `0x8000` 的分区表里。所以同一份 App 可以
写到旧 App 之后的空闲槽位,只需要改分区表里的 factory 偏移,不需要重新构建。
旧 `scripts/flash.sh` 之所以重编,是因为它把新偏移写回 `partitions.csv` 再重建。
客户端改为直接 patch 已构建的 `partition-table.bin`:
1. 从设备读当前分区表(0xC00 字节);
2. 解析条目,找到 factory;
3. 把 factory 的 offset/size 改成目标槽位;
4. 重算分区表 MD5(本仓库实测在表内 `0x70`,不是 `0xC00` 末尾);
5. 写回并整段回读。
## 烧录顺序与坏块检测
```
探测设备 → 读当前分区表/App 描述 → 选候选槽位(旧 App 之后)
→ 备份旧分区表 + 旧 App
→ 对每个候选槽位:
写 App → 整段回读逐字节比较
通过 → 选中;失败 → 记录坏字节/坏扇区,换下一个
→ 只有选中槽位通过,才写 patch 后的分区表
→ 回读分区表 → 复位 → 抓启动日志
```
- 候选槽位按 64 KiB 对齐,优先 `>= 0x200000`,并跳过与旧 App 重叠的区间;
- 当前布局(factory `0x70000`,size `0x120000`)下,第一个候选是 `0x200000`,
新分区 size `0x140000`;旧 App 保留在 `0x70000`,可回滚;
- 实际镜像写读校验是最终判据:图案扫描会误报“当前镜像恰好可写”的槽位;主流程
写真实镜像再回读;
- 不使用 merged image:填充区的坏字节也会让整包失败;只写 App 和分区表;
- 分区表只在 App 回读通过后才切换;切换失败/启动失败可用备份恢复旧表,旧 App 未覆盖。
### 已知坏点
- `0xFADE7`:bit 5–7 无法正确编程(已确认);
- `0x10863`:bit 6 读回为 1(`HARDWARE.md` 中尚未确认是否永久)。
两个地址都在 `0x100000` 以下;`0x200000` 槽位天然避开它们。客户端仍会对写入的
App 整段回读,不依赖“已知坏点表”。
## 官方 .zip 固件包
编译完成后由 [`scripts/package_firmware.py`](../scripts/package_firmware.py) 打成单文件
`.zip`,客户端直接读取,不需要先解压。旧目录包仍兼容。
zip 根目录:
```text
bundle.json
esp32s3-app.bin
partition-table.bin # 0xC00 字节
bootloader.bin # 可选;客户端默认不写
BUILD-INFO.json # 可选;展示版本/构建信息
```
客户端读取 zip 时的硬限制:
- 条目数 ≤ 32;只接受根目录单层文件名,拒绝 `..`、绝对路径、盘符、`/`、`\`;
- `bundle.json` ≤ 64 KiB;分区表必须正好 `0xC00`;App 不得超过 `appPartitionSize`;
- zip CRC + `appSha256` / `partitionTableSha256` 双重校验;
- 只解压到 app cache 下的临时目录,读完立即删除;不把 zip 内容写进用户目录;
- `BundleInfo` 会显示 `sourceKind=zip`、整包 SHA256 与条目数。
`bundle.json` 示例(由 [`scripts/make_bundle.py`](../scripts/make_bundle.py) 生成):
```json
{
"format": 1,
"name": "ESP32-S3 图传固件",
"version": "vga30-fb3-realtime-tx15+4794bed",
"chip": "esp32s3",
"app": "esp32s3-app.bin",
"appSha256": "…",
"partitionTable": "partition-table.bin",
"partitionTableSha256": "…",
"bootloader": "bootloader.bin",
"bootloaderSha256": "…",
"appPartitionSize": "0x140000",
"flashSize": "16mb",
"flashMode": "dio",
"flashFrequency": "40mhz",
"notes": "…"
}
```
客户端会校验 `format`、`chip`、`flashSize`、`appSha256`、`partitionTableSha256`;
不匹配就拒绝,不接受任意 `.bin`。当前示例在
[`target/firmware/bundle.json`](../target/firmware/bundle.json)。
编译后打包:
```powershell
# 只打包(会先调用 make_bundle.py 刷新 bundle.json)
python scripts/package_firmware.py --firmware target/firmware
# 构建完成后自动打包
scripts/build.ps1 -Package
scripts/build.sh --package
```
产物:
```text
target/firmware/camfw-<chip>-<variant>-<rev>-<yyyymmdd>.zip
target/firmware/camfw-...zip.sha256
```
zip 使用固定条目顺序、固定 mtime 与 deflate level 9,同一个输入应得到同一个整包
SHA256,便于核对“两个 U 盘里的包是不是同一版”。`package_firmware.py` 会复用
`make_bundle.py`,不维护第二套 manifest 逻辑。
> 注意:bundle 里的 `partition-table.bin` 是参考模板;客户端实际写的是设备当前
> 分区表的 patch 版本,以保留设备的 NVS/PHY 布局。bundle 的分区表 SHA 仍会校验。
## 界面
入口有三处,不要求先选身份或绑定摄像头:
- 首次启动的身份选择页:`固件维护 / 烧录(不进入身份选择)`;
- 用户端设置密码页:同上;
- 主界面右上角:`固件维护`。
页面流程:
1. 自动列出串口并选中 Espressif 设备;
2. `读取设备` 显示芯片、MAC、Flash、安全启动/加密状态、当前 App、分区表、空闲区间;
3. `选择 .zip 固件包` 选择编译后打包的单文件;`选目录(兼容旧包)` 仍可用;
4. `生成计划` 显示目标槽位、候选槽位、切换后的 factory、警告;
5. 勾选是否备份旧 App;`开始烧录` 二次确认后执行;
6. 进度条 + 运行日志 + 结果;失败时显示坏字节/坏扇区、备份目录,并可 `恢复旧分区表`。
读取设备和烧录都会复位一次;开始前应停止录制,视频流会短暂中断。
## 代码位置
| 层 | 文件 |
| --- | --- |
| 后端:探测/分区表/候选槽位/写入/回读/备份/恢复 | [`desktop/src-tauri/src/maintenance.rs`](../desktop/src-tauri/src/maintenance.rs) |
| Tauri 命令注册、`--maintenance-probe`/`--maintenance-bundle`/`--maintenance-plan` | [`desktop/src-tauri/src/main.rs`](../desktop/src-tauri/src/main.rs) |
| 前端 IPC 与 DTO | [`desktop/src/lib/maintenance.ts`](../desktop/src/lib/maintenance.ts) |
| 维护页 | [`desktop/src/pages/MaintenancePage.tsx`](../desktop/src/pages/MaintenancePage.tsx) |
| 样式 | [`desktop/src/styles/maintenance.css`](../desktop/src/styles/maintenance.css) |
| manifest 生成 | [`scripts/make_bundle.py`](../scripts/make_bundle.py) |
| zip 打包 | [`scripts/package_firmware.py`](../scripts/package_firmware.py) |
后端使用 `espflash` 4.6 库(`default-features = false, features = ["serialport"]`)、
`serialport` 4.10 和 `zip` 2.6(只开 `deflate`);Tauri 用 `tauri-plugin-dialog` 选文件/目录。
## 构建与验证
Windows x64:
```powershell
cd desktop
pnpm install
pnpm check
pnpm build
cd src-tauri
cargo check --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]'
cargo test --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]' maintenance::tests
```
固件侧打包:
```powershell
python scripts/package_firmware.py --firmware target/firmware
# 或:scripts/build.ps1 -Package / scripts/build.sh --package
```
运行 GUI:
```powershell
cd desktop
pnpm tauri dev --target x86_64-pc-windows-msvc -- --config 'unstable.build-std=[]'
```
> `desktop/.cargo/config.toml` 里的 `build.target` 是 macOS ARM 的默认值;Windows
> 需要上面的 `--target` 覆盖。`tauri.conf.json` 的 bundle targets 是 `"all"`,
> 各平台打包时使用自己的默认 bundle 类型。
无窗口诊断(仍会复位设备):
```powershell
cd desktop/src-tauri
.\target\x86_64-pc-windows-msvc\debug\cam-desktop.exe --maintenance-probe COM8
# 只校验 zip/目录包,不连设备
.\target\x86_64-pc-windows-msvc\debug\cam-desktop.exe --maintenance-bundle C:\path\to\camfw-*.zip
.\target\x86_64-pc-windows-msvc\debug\cam-desktop.exe --maintenance-plan COM8 C:\path\to\camfw-*.zip
# 只写 App + 回读,不切分区表;设备仍从旧 App 启动
.\target\x86_64-pc-windows-msvc\debug\cam-desktop.exe --maintenance-write-test COM8 C:\path\to\camfw-*.zip 0x200000
```
## 已验证 / 未验证
已在当前连接板(`COM8`,ESP32-S3 v0.2,MAC `90:70:69:f8:f2:14`,16 MB,
Secure Boot / Flash Encryption 均未启用)上验证:
- `--maintenance-probe`:读取芯片、MAC、分区表、App 描述;
- `--maintenance-plan`:选中 `0x200000` / `0x140000`,patch 后 factory 为
`0x200000`,旧 App 保留;
- `--maintenance-write-test COM8 … 0x200000`:实机写入 App 并整段回读,
`writeOk=true`、`verifyOk=true`、0 处不一致;分区表未动,设备仍从旧 App 启动;
- `.zip` 打包与读取:`camfw-…zip` 5 项、758,149 字节,`--maintenance-bundle` 校验通过,
`sourceKind=zip`、整包 SHA256 与条目数正确;
- 离线单元测试:分区表解析、MD5 重算、候选槽位、尺寸解析、zip 路径白名单,共 7 个;
- 前端 `tsc --noEmit`、`vite build`,后端 `cargo check` 通过。
**尚未执行**完整实机切换(写 patch 后分区表 → 复位 → 启动日志)。App 写入/回读
已经在同一块板上验证;切换步骤的风险集中在 0x8000 的 3 KiB 分区表写入,写入后会
回读,失败会尝试恢复旧表。建议在维护页点一次;如果新 App 启动失败,页面会给出
备份目录和 `恢复旧分区表` 按钮。
## 边界
- 不做运行时坏块重映射/FTL;ESP32 ROM bootloader 按分区线性读取,不能在 App 镜像
中间跳过坏扇区。这里的“屏蔽”= 选一个实际镜像写读校验通过的整槽位。
- 不写 bootloader、不碰 NVS/PHY;bundle 里的 bootloader 只做展示/校验。
- 分区表写入是单点风险:写入后会回读;失败会尝试恢复旧表;极端断电情况下可用
ROM 下载模式重新写入,备份在 `%APPDATA%/com.simbarobotics.rmic2026/backups/`(Windows)或
对应平台的 app data 目录。
- 网络 OTA 仍需要一次性改固件(OTA 分区 + `esp_ota` 接收端 + 控制通道),不在本次范围。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
id,category,priority,precondition,steps,expected,pass_criteria,evidence,tools,est_minutes
"N-01","正常","P0","健康板;新模板;工具版本锁定","分段写 Bootloader/分区表/App;逐段 verify;复位;串口与 UDP 验收","3 段 verify OK;启动日志正常;15 s 内收到完整帧","verify 全通过;无 panic/brownout;perf 帧率>=15 fps 且丢帧<=5%","verify 日志;serial.log;perf.json","esptool;perf_probe;serial_soak","20"
"N-02","正常","P0","健康板;已 provision;同布局","只写新 App;verify;复位;检查 config 保留","App 版本更新;原 SSID 配置仍生效","App hash 与 manifest 一致;config seq 不变;联网正常","verify;serial;perf","esptool;perf_probe","15"
"N-03","正常","P0","健康板;已 provision;模板支持 config","只 patch config(换 SSID);不写 App;复位","新 SSID 生效;App SHA-256 不变","app-readback SHA-256 与 manifest 一致;serial 显示 config source=partition;联网正常","app hash;serial;perf","esptool;provision.py;perf_probe","10"
"N-04","正常","P1","健康板","连续 provisioning 3 次(相同输入)","幂等;无 brick;报告一致","3/3 通过;设备身份一致;无重复写入错误","3 份 result.json","provision.py","20"
"N-05","正常","P1","健康板;归档镜像","写 merged image 并 verify(对照)","记录是否通过;不用于坏板","结果记录;不改变坏板策略","verify 日志","esptool","10"
"B-01","坏板","P0","故障板;E1 备份已验证","牺牲板 E2 写读表征;生产板定向读 0xFADE7/0x10863 + 扫描模板写入区","E2 复现缺陷;生产板地图正确;恢复 verify","至少识别两个已知地址;恢复后关键区 verify OK","scan.json;verify;backup hash","esptool;provision.py","30"
"B-02","坏板","P0","故障板;新布局;模板镜像","分段写新 App/表;verify;复位","写入区不含坏地址;App 实际镜像 verify OK;启动","3 段 verify OK;serial live;UDP 有帧","verify;serial;perf","esptool;perf_probe","25"
"B-03","坏板","P0","故障板;强制 App 槽包含坏字节","写实际镜像;读回比较","若镜像字节不可写则 verify 失败;工具换槽或拒绝;不 brick","失败可定位到地址;设备仍可进下载模式;报告不报成功","verify;scan;report","esptool;provision.py","25"
"B-04","坏板","P0","故障板;config 一个槽含坏扇区","写 config 多副本;复位","跳过坏槽;至少一个副本有效;联网","serial 显示有效 seq;报告记录 skipped 槽;App hash 不变","scan;serial;report","esptool;provision.py","20"
"B-05","坏板","P1","故障板;merged 镜像","写 merged image(对照)","预期失败或高风险;工具默认禁止","记录失败地址/错误;后续流程仍走分段","verify 日志;report","esptool","15"
"B-06","坏板","P1","故障板;疑似间歇坏字节","同一扇区扫描 3 次;不同温度/供电","标记 SUSPECT;隔离;不用于 App","3 次结果一致或明确记录间歇;SUSPECT 不进 App 槽","scan.json","esptool;provision.py","30"
"B-07","坏板","P2","故障板;全片备份","全片扫描 + 恢复","得到全片地图;恢复关键区;不丢数据","恢复后关键区 verify OK;时间记录","scan.json;backup hash","esptool","60"
"O-01","OTA","P1","OTA-ready;Bootloader rollback","写 ota_0;切换;复位","新版本启动;分区日志正确","serial 显示 ota_0;版本正确;联网","serial;otatool","otatool.py;perf_probe","30"
"O-02","OTA","P1","OTA-ready;当前槽有效","写损坏镜像/错误 SHA;复位","Bootloader 拒绝;旧槽仍启动","旧版本启动;无 brick;serial 有拒绝原因","serial","otatool.py;esptool","20"
"O-03","OTA","P1","OTA-ready;USB 继电器","OTA 写入 25/50/75% 时断电;恢复","至少一个槽可启动;无需整片擦","3 个断点均能启动;版本/分区符合预期","serial;relay log","otatool.py;relay","45"
"O-04","OTA","P1","OTA-ready;目标槽含坏区","OTA 到坏槽;复位","安全失败;旧槽不受影响","旧版本启动;无 brick;报告记录坏槽","serial;scan","otatool.py;esptool","25"
"O-05","OTA","P2","启用 anti-rollback","OTA 降级;复位","按策略拒绝或接受;不 brick","策略与日志一致;设备可启动","serial","otatool.py","20"
"H-01","热插拔","P0","1 块板;驱动已装","插拔 20 次;记录发现时间","20/20 发现;P95<=2 s","无漏检;无端口残留;无错板","discovery.json","serialport;provision.py","20"
"H-02","热插拔","P0","2 块板同时插入","按 MAC 选择;执行 patch/verify","0 次错板;端口/MAC 正确","两块设备报告 MAC 与操作一致","report","serialport;provision.py","20"
"H-03","热插拔","P1","写入进行中","拔掉 USB","报错;设备可恢复;不报成功","错误码明确;重插后可进下载模式","log;report","provision.py","15"
"H-04","热插拔","P1","扫描进行中","拔掉 USB","报错;备份可恢复;不半写成功","重插后恢复;关键区 verify OK","log;backup","provision.py","20"
"H-05","热插拔","P1","串口监视器占用端口","执行 provisioning","明确端口占用;不挂死","错误码/提示;进程退出","log","provision.py;espflash monitor","10"
"H-06","热插拔","P0","错固件/错芯片","选择不兼容 manifest","写入前拒绝;设备不变","App hash 前后一致;错误码 3","report","provision.py","10"
"P-01","断电","P0","健康板/坏板;可恢复","App 写入 50% 断电;上电;重刷","可进下载模式;重刷后启动;不 brick","BOOT 恢复;verify OK;serial live","serial;verify","esptool;relay","20"
"P-02","断电","P0","config 已有一个有效副本","config 写入时断电;上电","旧副本有效;启动","serial 显示旧 seq;联网正常","serial","esptool;relay","15"
"P-03","断电","P1","分区表迁移中","分区表写入时断电;上电;重刷","BOOT 恢复;重刷表/App;不 brick","可进下载模式;重刷后启动","serial;verify","esptool;relay","25"
"P-04","断电","P2","Bootloader 更新中(最高风险)","Bootloader 写入时断电;上电;BOOT 恢复","可恢复;不 permanent brick","BOOT 可进下载;重刷后启动","serial;verify","esptool;relay","30"
"P-05","断电","P2","扫描/恢复中","拔 USB/断电;恢复","备份可用;关键区不丢","关键区 verify OK;无半写成功","backup hash","esptool","30"
"S-01","安全","P0","正常 provision","检查日志、报告、命令行历史","无密码明文;只有 SSID/hash/seq","grep 不到密码;报告字段合规","grep;report","provision.py","10"
"S-02","安全","P1","Secure Boot/加密板(如有)","espefuse summary;尝试未签名 patch","检测 eFuse;拒绝未签名/未加密 patch","工具退出码 10;设备不变","espefuse 输出;report","espefuse;espsecure","20"
"S-03","安全","P1","篡改 manifest/App","校验 SHA-256 后烧录","拒绝烧录","错误码 3;设备不变","report","provision.py","10"
"S-04","安全","P2","伪造 VID/PID 设备","主动探测 chip/MAC/Flash","识别不匹配;拒绝","不写入;报告记录","report","serialport;esptool","20"
"R-01","回归","P0","host 环境","pytest 全部 host tests","100% 通过","0 failed;覆盖 config/分区/manifest/选择逻辑","JUnit/HTML","pytest","10"
"R-02","回归","P1","硬件在环","20 次循环(N/B/H 子集)","首次通过率>=95%;0 brick;0 错板","统计达标;失败可定位","summary.json","pytest;provision.py","120"
"R-03","回归","P0","锁定工具版本","重复执行同一命令 3 次","同输入同命令/同结果","版本/哈希/输出一致","versions.txt;report","all","15"
"R-04","回归","P1","报告 schema","用 jsonschema 校验 result/scan/manifest","全部通过","0 schema error","schema 校验日志","pytest;jsonschema","10"
"M-01","迁移","P1","旧布局 0x70000 板","备份;写新 App→新表→可选 Bootloader;保留 NVS/PHY;复位","新布局启动;NVS/PHY 保留;config 可用","serial live;MAC 不变;Wi-Fi 校准未丢","serial;report","esptool;provision.py","30"
"M-02","迁移","P0","旧布局板","尝试只 patch config","工具拒绝并提示迁移","错误码 3/提示明确;设备不变","report","provision.py","10"
1 id category priority precondition steps expected pass_criteria evidence tools est_minutes
2 N-01 正常 P0 健康板;新模板;工具版本锁定 分段写 Bootloader/分区表/App;逐段 verify;复位;串口与 UDP 验收 3 段 verify OK;启动日志正常;15 s 内收到完整帧 verify 全通过;无 panic/brownout;perf 帧率>=15 fps 且丢帧<=5% verify 日志;serial.log;perf.json esptool;perf_probe;serial_soak 20
3 N-02 正常 P0 健康板;已 provision;同布局 只写新 App;verify;复位;检查 config 保留 App 版本更新;原 SSID 配置仍生效 App hash 与 manifest 一致;config seq 不变;联网正常 verify;serial;perf esptool;perf_probe 15
4 N-03 正常 P0 健康板;已 provision;模板支持 config 只 patch config(换 SSID);不写 App;复位 新 SSID 生效;App SHA-256 不变 app-readback SHA-256 与 manifest 一致;serial 显示 config source=partition;联网正常 app hash;serial;perf esptool;provision.py;perf_probe 10
5 N-04 正常 P1 健康板 连续 provisioning 3 次(相同输入) 幂等;无 brick;报告一致 3/3 通过;设备身份一致;无重复写入错误 3 份 result.json provision.py 20
6 N-05 正常 P1 健康板;归档镜像 写 merged image 并 verify(对照) 记录是否通过;不用于坏板 结果记录;不改变坏板策略 verify 日志 esptool 10
7 B-01 坏板 P0 故障板;E1 备份已验证 牺牲板 E2 写读表征;生产板定向读 0xFADE7/0x10863 + 扫描模板写入区 E2 复现缺陷;生产板地图正确;恢复 verify 至少识别两个已知地址;恢复后关键区 verify OK scan.json;verify;backup hash esptool;provision.py 30
8 B-02 坏板 P0 故障板;新布局;模板镜像 分段写新 App/表;verify;复位 写入区不含坏地址;App 实际镜像 verify OK;启动 3 段 verify OK;serial live;UDP 有帧 verify;serial;perf esptool;perf_probe 25
9 B-03 坏板 P0 故障板;强制 App 槽包含坏字节 写实际镜像;读回比较 若镜像字节不可写则 verify 失败;工具换槽或拒绝;不 brick 失败可定位到地址;设备仍可进下载模式;报告不报成功 verify;scan;report esptool;provision.py 25
10 B-04 坏板 P0 故障板;config 一个槽含坏扇区 写 config 多副本;复位 跳过坏槽;至少一个副本有效;联网 serial 显示有效 seq;报告记录 skipped 槽;App hash 不变 scan;serial;report esptool;provision.py 20
11 B-05 坏板 P1 故障板;merged 镜像 写 merged image(对照) 预期失败或高风险;工具默认禁止 记录失败地址/错误;后续流程仍走分段 verify 日志;report esptool 15
12 B-06 坏板 P1 故障板;疑似间歇坏字节 同一扇区扫描 3 次;不同温度/供电 标记 SUSPECT;隔离;不用于 App 3 次结果一致或明确记录间歇;SUSPECT 不进 App 槽 scan.json esptool;provision.py 30
13 B-07 坏板 P2 故障板;全片备份 全片扫描 + 恢复 得到全片地图;恢复关键区;不丢数据 恢复后关键区 verify OK;时间记录 scan.json;backup hash esptool 60
14 O-01 OTA P1 OTA-ready;Bootloader rollback 写 ota_0;切换;复位 新版本启动;分区日志正确 serial 显示 ota_0;版本正确;联网 serial;otatool otatool.py;perf_probe 30
15 O-02 OTA P1 OTA-ready;当前槽有效 写损坏镜像/错误 SHA;复位 Bootloader 拒绝;旧槽仍启动 旧版本启动;无 brick;serial 有拒绝原因 serial otatool.py;esptool 20
16 O-03 OTA P1 OTA-ready;USB 继电器 OTA 写入 25/50/75% 时断电;恢复 至少一个槽可启动;无需整片擦 3 个断点均能启动;版本/分区符合预期 serial;relay log otatool.py;relay 45
17 O-04 OTA P1 OTA-ready;目标槽含坏区 OTA 到坏槽;复位 安全失败;旧槽不受影响 旧版本启动;无 brick;报告记录坏槽 serial;scan otatool.py;esptool 25
18 O-05 OTA P2 启用 anti-rollback OTA 降级;复位 按策略拒绝或接受;不 brick 策略与日志一致;设备可启动 serial otatool.py 20
19 H-01 热插拔 P0 1 块板;驱动已装 插拔 20 次;记录发现时间 20/20 发现;P95<=2 s 无漏检;无端口残留;无错板 discovery.json serialport;provision.py 20
20 H-02 热插拔 P0 2 块板同时插入 按 MAC 选择;执行 patch/verify 0 次错板;端口/MAC 正确 两块设备报告 MAC 与操作一致 report serialport;provision.py 20
21 H-03 热插拔 P1 写入进行中 拔掉 USB 报错;设备可恢复;不报成功 错误码明确;重插后可进下载模式 log;report provision.py 15
22 H-04 热插拔 P1 扫描进行中 拔掉 USB 报错;备份可恢复;不半写成功 重插后恢复;关键区 verify OK log;backup provision.py 20
23 H-05 热插拔 P1 串口监视器占用端口 执行 provisioning 明确端口占用;不挂死 错误码/提示;进程退出 log provision.py;espflash monitor 10
24 H-06 热插拔 P0 错固件/错芯片 选择不兼容 manifest 写入前拒绝;设备不变 App hash 前后一致;错误码 3 report provision.py 10
25 P-01 断电 P0 健康板/坏板;可恢复 App 写入 50% 断电;上电;重刷 可进下载模式;重刷后启动;不 brick BOOT 恢复;verify OK;serial live serial;verify esptool;relay 20
26 P-02 断电 P0 config 已有一个有效副本 config 写入时断电;上电 旧副本有效;启动 serial 显示旧 seq;联网正常 serial esptool;relay 15
27 P-03 断电 P1 分区表迁移中 分区表写入时断电;上电;重刷 BOOT 恢复;重刷表/App;不 brick 可进下载模式;重刷后启动 serial;verify esptool;relay 25
28 P-04 断电 P2 Bootloader 更新中(最高风险) Bootloader 写入时断电;上电;BOOT 恢复 可恢复;不 permanent brick BOOT 可进下载;重刷后启动 serial;verify esptool;relay 30
29 P-05 断电 P2 扫描/恢复中 拔 USB/断电;恢复 备份可用;关键区不丢 关键区 verify OK;无半写成功 backup hash esptool 30
30 S-01 安全 P0 正常 provision 检查日志、报告、命令行历史 无密码明文;只有 SSID/hash/seq grep 不到密码;报告字段合规 grep;report provision.py 10
31 S-02 安全 P1 Secure Boot/加密板(如有) espefuse summary;尝试未签名 patch 检测 eFuse;拒绝未签名/未加密 patch 工具退出码 10;设备不变 espefuse 输出;report espefuse;espsecure 20
32 S-03 安全 P1 篡改 manifest/App 校验 SHA-256 后烧录 拒绝烧录 错误码 3;设备不变 report provision.py 10
33 S-04 安全 P2 伪造 VID/PID 设备 主动探测 chip/MAC/Flash 识别不匹配;拒绝 不写入;报告记录 report serialport;esptool 20
34 R-01 回归 P0 host 环境 pytest 全部 host tests 100% 通过 0 failed;覆盖 config/分区/manifest/选择逻辑 JUnit/HTML pytest 10
35 R-02 回归 P1 硬件在环 20 次循环(N/B/H 子集) 首次通过率>=95%;0 brick;0 错板 统计达标;失败可定位 summary.json pytest;provision.py 120
36 R-03 回归 P0 锁定工具版本 重复执行同一命令 3 次 同输入同命令/同结果 版本/哈希/输出一致 versions.txt;report all 15
37 R-04 回归 P1 报告 schema 用 jsonschema 校验 result/scan/manifest 全部通过 0 schema error schema 校验日志 pytest;jsonschema 10
38 M-01 迁移 P1 旧布局 0x70000 板 备份;写新 App→新表→可选 Bootloader;保留 NVS/PHY;复位 新布局启动;NVS/PHY 保留;config 可用 serial live;MAC 不变;Wi-Fi 校准未丢 serial;report esptool;provision.py 30
39 M-02 迁移 P0 旧布局板 尝试只 patch config 工具拒绝并提示迁移 错误码 3/提示明确;设备不变 report provision.py 10
+21 -21
View File
@@ -3,9 +3,9 @@
摄像头 UDP 图传的线路格式,**固件与两端客户端共用**。 摄像头 UDP 图传的线路格式,**固件与两端客户端共用**。
``` ```
protocol/ 线路格式 + 帧重组 (13 个单元测试,无 ESP-IDF 依赖) protocol/ 线路格式 + 帧重组 (23 个单元测试,无 ESP-IDF 依赖)
. 固件 -> ESP32-S3 (交叉编译,Xtensa) . 固件 -> ESP32-S3 (交叉编译,Xtensa)
client/ 桌面客户端 (Slint,两种身份) desktop/ 桌面客户端 (RMIC::2026,Tauri v2 + React + Rust,两种身份)
``` ```
这是本仓库里最该被测试的部分:帧重组和编码都极易出微妙错误,而这两件事都无法 这是本仓库里最该被测试的部分:帧重组和编码都极易出微妙错误,而这两件事都无法
@@ -17,23 +17,25 @@ client/ 桌面客户端 (Slint,两种身份)
cd protocol && cargo test cd protocol && cargo test
``` ```
非 macOS / Apple Silicon 主机上,`protocol/.cargo/config.toml` 里钉住的 target 不适用,需按 [DEVELOPMENT.md](../DEVELOPMENT.md) 用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。本机实测:
``` ```
13 passed 23 passed
``` ```
## 拓扑 ## 拓扑
```text ```text
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ 裁判终端(PC,仅显示) │ │ RMIC::2026 桌面端(仅显示) │
│ 队伍一 + 队伍二 + 双方血量 │ │ 队伍一 + 队伍二 + 双方血量 │
└───────────────┬──────────────┘ └───────────────┬──────────────┘
│ 同一个局域网,广播可用 │ 同一个局域网,广播可用
┌────────────────────┼────────────────────┐ ┌────────────────────┼────────────────────┐
│ │ │ │ │ │
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
│ 图传板 A │ │ 图传板 B │ │ 装甲板 ×4 │ │ 图传板 A │ │ 图传板 B │ │ 装甲板 ×4 │
│ (队伍一) │ │ (队伍二) │ │ (预留,未实现) │ │ (队伍一) │ │ (队伍二) │ │ (已实现) │
└───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘
``` ```
@@ -42,7 +44,7 @@ cd protocol && cargo test
## 线路格式 ## 线路格式
每个数据报 = **27 字节头** + 至多 **1400 字节**载荷: 每个数据报 = 27 字节头 + 至多 1400 字节载荷:
```text ```text
0 1 2 3 0 1 2 3
@@ -72,12 +74,12 @@ cd protocol && cargo test
| 2 `Subscribe` | 客户端 → 板子 | "我是这个角色,请给我画面" | | 2 `Subscribe` | 客户端 → 板子 | "我是这个角色,请给我画面" |
| 3 `Hello` | 板子 → 客户端 | "我在线,我是谁" | | 3 `Hello` | 板子 → 客户端 | "我在线,我是谁" |
| 4 `Heartbeat` | 板子 → 客户端 | 同上,心跳形式 | | 4 `Heartbeat` | 板子 → 客户端 | 同上,心跳形式 |
| 5 `ArmorHealth` | 装甲板 → 客户端 | 血量(**预留,当前无实现**) | | 5 `ArmorHealth` | 装甲板 → 客户端 | 血量(已实现) |
### 三个设计决定 ### 四个设计决定
**27 字节不凑整。** 头只占每个 1427 字节数据报的 1.9%,而为对齐在固件和客户端两侧 **27 字节不凑整。** 头只占每个 1427 字节数据报的 1.9%,而为对齐在固件和客户端两侧
各写一段手写偏移常量,是引入静默错位的更划算的代价。 各写一段手写偏移常量,更容易引入静默错位。
**`sender_id` 是多路的前提。** 裁判终端同时收两块板子,靠它在同一端口上区分来源。 **`sender_id` 是多路的前提。** 裁判终端同时收两块板子,靠它在同一端口上区分来源。
@@ -120,6 +122,7 @@ cd protocol && cargo test
烧录",而不是让人去猜。 烧录",而不是让人去猜。
这不削弱边界:视频、订阅、装甲板上报仍然走严格解析。 这不削弱边界:视频、订阅、装甲板上报仍然走严格解析。
## 为什么视频走 UDP 而不是 TCP ## 为什么视频走 UDP 而不是 TCP
实测(VGA 640x480,传感器产出 22 fps): 实测(VGA 640x480,传感器产出 22 fps):
@@ -160,7 +163,7 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
它(`a_duplicate_cannot_complete_a_frame_with_a_hole`、 它(`a_duplicate_cannot_complete_a_frame_with_a_hole`、
`a_gap_that_is_never_filled_never_yields_a_frame`)。 `a_gap_that_is_never_filled_never_yields_a_frame`)。
**不过要说清楚:在实测链路上一次重复投递都没出现过。** 一到两个订阅者、20 秒 2000+ **在实测链路上一次重复投递都没出现过。** 一到两个订阅者、20 秒 2000+
个分片,重复数为 0(固定端口与临时端口两种订阅方式都试过)。所以这层防护是为了正确 个分片,重复数为 0(固定端口与临时端口两种订阅方式都试过)。所以这层防护是为了正确
性,不是为了修一个正在发生的故障。 性,不是为了修一个正在发生的故障。
@@ -188,9 +191,8 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
### 发送端的复用 ### 发送端的复用
`Fragment::encode_into()` 追加写入而不分配:发送端对每帧的每个分片复用同一个缓冲, `Fragment::encode_into()` 追加写入而不分配:发送端对每帧的每个分片复用同一个缓冲。
在 22fps 下这是每秒约 250 次分配与不节省别的差别——在这块同时要抢内部 RAM 的芯片 22 fps 下这是每秒约 250 次分配,省掉它们在这块同时要抢内部 RAM 的芯片上很实在。
上很实在。
`fragment_frame()` 从 JPEG 长度推出分片数,因此调用方不可能产出与自身矛盾的集合。 `fragment_frame()` 从 JPEG 长度推出分片数,因此调用方不可能产出与自身矛盾的集合。
@@ -215,7 +217,7 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
丢包都**记在应用账上**而不是网络账上——后者本来更容易排查。 丢包都**记在应用账上**而不是网络账上——后者本来更容易排查。
- VGA 下每帧约 17 个分片。 - VGA 下每帧约 17 个分片。
- `DEFAULT_PORT` = 5600,两端共用同一个常量。 - `DEFAULT_PORT` = 5600,两端共用同一个常量。
- `ARMOR_PORT` = 5601,装甲板预留,当前无实现。 - `ARMOR_PORT` = 5601,装甲板血量上报。
## 身份 ## 身份
@@ -234,10 +236,8 @@ UDP 不保证跨数据报的顺序与到达,所以试图"补洞"的接收端
例子也有——它的 `role` 字节曾多填 12 个零字节,固件按偏移严格读取因而拒收,表现为 例子也有——它的 `role` 字节曾多填 12 个零字节,固件按偏移严格读取因而拒收,表现为
"板子在线但 0 clients subscribed"。改协议时记得同步它。 "板子在线但 0 clients subscribed"。改协议时记得同步它。
## 装甲板(预留) ## 装甲板
`ArmorHealth` 与 `ARMOR_PORT` 已就位,`MsgType` 也留了 5 号。现在没有实现也没有 `ArmorHealth` 占 `MsgType` 的 5 号。装甲板主板向 `ARMOR_PORT` 广播,`desktop/core/src/armor.rs`
数据源,裁判界面显示占位符而不是 0——**0 看起来像真值,现场看到会以为对方已经没 里的 `ArmorHealthStore` 负责接收。收不到上报时界面显示占位符而不是 0——**0 看起来像真值,
血了**。 现场看到会以为对方已经没血了**。
接上以后板子向 `ARMOR_PORT` 发这个结构,客户端监听即可,界面与其它代码都不用改。
+2 -2
View File
@@ -3,9 +3,9 @@
"major": 0, "major": 0,
"minor": 2, "minor": 2,
"patch": 1, "patch": 1,
"build": 35, "build": 36,
"protocol": 1, "protocol": 1,
"date": "2026-10-19", "date": "2026-10-19",
"baseline": "v0.2.0", "baseline": "v0.2.1",
"note": "由 scripts/version.mjs 从 git 历史算出,请勿手改。" "note": "由 scripts/version.mjs 从 git 历史算出,请勿手改。"
} }