Files
sloray 6a0eb14b28
构建客户端 / 检查(类型 + 单元测试) (push) Waiting to run
构建客户端 / Linux x64(deb + AppImage) (push) Blocked by required conditions
构建客户端 / Windows x64(NSIS 安装包) (push) Blocked by required conditions
构建客户端 / macOS 通用(.app + .dmg) (push) Blocked by required conditions
docs: 整理项目文档并统一界面表述
将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。

- fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示
- test(client): 按文件名核对媒体类型,避免依赖时间戳排序

验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
2026-10-19 11:26:26 +08:00

236 lines
10 KiB
Markdown
Raw Permalink 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
# 只打包(会先调用 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` 接收端 + 控制通道),不在本次范围。