将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。 - fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示 - test(client): 按文件名核对媒体类型,避免依赖时间戳排序 验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
236 lines
10 KiB
Markdown
236 lines
10 KiB
Markdown
# 固件维护页:实现说明
|
||
|
||
## 范围
|
||
|
||
维护页通过 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
|
||
# 只打包(会先调用 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,便于核对分发的固件包是否一致。`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` 接收端 + 控制通道),不在本次范围。
|