Files
simba-rmic-2026/docs/firmware-maintenance.md
T
sloray e3acb37d32
构建客户端 / 检查(类型 + 单元测试) (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
docs: 按 humanizer 重写项目文档并同步 RMIC::2026 现状
2026-10-19 10:47:00 +08:00

237 lines
10 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 现场更新**(用户确认的 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` 接收端 + 控制通道),不在本次范围。