Files
simba-rmic-2026/docs/maintenance/firmware.md
T
sloray 054379dc64
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- style(client): 应用图标增加圆角,统一“裁判系统客户端”命名
- fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- docs: 润色项目说明并统一裁判系统术语
- test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成

验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
2026-10-21 09:25:17 +08:00

233 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 固件维护页:实现说明
## 范围
维护页通过 USB 串口更新设备,不支持设备联网自升级。以下命令均从仓库根目录执行,另有 `cd` 的示例除外。
- App 镜像逐字节原样写入,不支持动态修改 SSID;
- 当前固件没有 OTA 分区、`esp_ota` 接收端或控制通道;
- 使用已构建的官方 bundle,无需修改固件源码或重新编译;客户端只修改设备当前分区表的
factory 偏移和大小;
- 目标芯片以 ESP32-S3 为当前实现,`chip` 字段与 `espflash` 的芯片探测已经为 C3 预留,
但 C3 的 bundle/布局尚未验收。
## 为什么坏块规避不需要重编译
App 镜像不包含自身的分区偏移,偏移保存在 `0x8000` 的分区表中。因此,同一份 App 可写入旧 App 之后的空闲槽位,只需修改分区表中的 factory 偏移,无需重新构建。
旧 `scripts/flash.sh` 之所以重编,是因为它把新偏移写回 `partitions.csv` 再重建。
客户端直接修改从设备读取的当前分区表;包内的 `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/camera.md#损坏-flash-模组的兼容处理)。
两个地址均低于 `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`,不随源码提交。
编译后打包:
```powershell
# Windows:使用与构建时相同的短目录。
$env:CARGO_TARGET_DIR = 'C:\s3vt'
# 只打包(从 ELF 导出镜像,再调用 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>-v<version>-<build>.zip
target/firmware/camfw-...zip.sha256
```
zip 使用固定条目顺序、固定 mtime 和 deflate level 9。同一输入应生成相同的整包 SHA256,便于核对分发的固件包是否一致。`package_firmware.py` 复用 `make_bundle.py`,无需另维护一套 manifest 逻辑。
PowerShell 构建入口通过 Cargo 的产物报告传递 ELF 路径;单独运行 Python 打包时,通过 `cargo metadata` 读取输出目录,支持 `CARGO_TARGET_DIR` 和 Cargo 的 `build.target-dir`。也可用 `--elf <路径>` 显式指定 ELF。脚本从该 ELF 导出 App,复制同目录的 Bootloader 和分区表,并重写构建信息,避免沿用旧镜像或旧实机测试结论。缺少 ELF、配套镜像或导出工具时默认停止;仅在明确需要复用现有目录包时使用 `--allow-stale`。版本默认读取根目录 `version.json`。切回桌面端构建前执行 `Remove-Item Env:CARGO_TARGET_DIR`。
> 注意:bundle 里的 `partition-table.bin` 是参考模板;客户端实际写的是设备当前
> 分区表的 patch 版本,以保留设备的 NVS/PHY 布局。bundle 的分区表 SHA 仍会校验。
## 界面
入口有三处,不要求先选身份或绑定摄像头:
- 首次启动的身份选择页:`打开固件维护`;
- 用户端设置密码页:同上;
- 主界面右上角:`固件`。
页面流程:
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` 接收端 + 控制通道),不在本次范围。