将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。 - fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示 - test(client): 按文件名核对媒体类型,避免依赖时间戳排序 验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
10 KiB
固件维护页:实现说明
范围
维护页通过 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 只作参考模板与校验:
- 从设备读当前分区表(0xC00 字节);
- 解析条目,找到 factory;
- 把 factory 的 offset/size 改成目标槽位;
- 重算分区表 MD5(本仓库实测在表内
0x70,不是0xC00末尾); - 写回并整段回读。
烧录顺序与坏块检测
探测设备 → 读当前分区表/App 描述 → 选候选槽位(旧 App 之后)
→ 备份旧分区表 + 旧 App
→ 对每个候选槽位:
写 App → 整段回读逐字节比较
通过 → 选中;失败 → 记录坏字节/坏扇区,换下一个
→ 只有选中槽位通过,才写 patch 后的分区表
→ 回读分区表 → 复位 → 抓启动日志
- 候选槽位按 64 KiB 对齐,优先
>= 0x200000,并跳过与旧 App 重叠的区间; - 当前布局(factory
0x70000,size0x120000)下,第一个候选是0x200000, 新分区 size0x140000;旧 App 保留在0x70000,可回滚; - 实际镜像写读校验是最终判据:图案扫描会误报“当前镜像恰好可写”的槽位;主流程 写真实镜像再回读;
- 不使用 merged image:填充区的坏字节也会让整包失败;只写 App 和分区表;
- 分区表只在 App 回读通过后才切换;切换失败/启动失败可用备份恢复旧表,旧 App 未覆盖。
已知坏点
0xFADE7:bit 5–7 无法正确编程(已确认);0x10863:bit 6 读回为 1,尚未确认是否为永久故障,见硬件说明。
两个地址都在 0x100000 以下;0x200000 槽位天然避开它们。客户端仍会对写入的
App 整段回读,不依赖“已知坏点表”。
官方 .zip 固件包
编译完成后由 scripts/package_firmware.py 打成单文件
.zip,客户端直接读取,不需要先解压。旧目录包仍兼容。
zip 根目录:
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 生成):
{
"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,不随源码提交。
编译后打包:
# 只打包(会先调用 make_bundle.py 刷新 bundle.json)
python scripts/package_firmware.py --firmware target/firmware
# 构建完成后自动打包
scripts/build.ps1 -Package
scripts/build.sh --package
产物:
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 仍会校验。
界面
入口有三处,不要求先选身份或绑定摄像头:
- 首次启动的身份选择页:
打开固件维护; - 用户端设置密码页:同上;
- 主界面右上角:
固件。
页面流程:
- 自动列出串口并选中 Espressif 设备;
读取设备显示芯片、MAC、Flash、安全启动/加密状态、当前 App、分区表、空闲区间;选择 .zip 固件包选择编译后打包的单文件;选目录(兼容旧包)仍可用;生成计划显示目标槽位、候选槽位、切换后的 factory、警告;- 勾选是否备份旧 App;
开始烧录二次确认后执行; - 进度条 + 运行日志 + 结果;失败时显示坏字节/坏扇区、备份目录,并可
恢复旧分区表。
读取设备和烧录都会复位一次;开始前应停止录制,视频流会短暂中断。
代码位置
| 层 | 文件 |
|---|---|
| 后端:探测/分区表/候选槽位/写入/回读/备份/恢复 | desktop/src-tauri/src/maintenance.rs |
Tauri 命令注册、--maintenance-probe/--maintenance-bundle/--maintenance-plan |
desktop/src-tauri/src/main.rs |
| 前端 IPC 与 DTO | desktop/src/lib/maintenance.ts |
| 维护页 | desktop/src/pages/MaintenancePage.tsx |
| 样式 | desktop/src/styles/maintenance.css |
| manifest 生成 | scripts/make_bundle.py |
| zip 打包 | 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:
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
固件侧打包:
python scripts/package_firmware.py --firmware target/firmware
# 或:scripts/build.ps1 -Package / scripts/build.sh --package
运行 GUI:
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 类型。
无窗口诊断(仍会复位设备):
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-…zip5 项、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接收端 + 控制通道),不在本次范围。