1308 lines
81 KiB
Markdown
1308 lines
81 KiB
Markdown
# ESP32-S3 客户端自动烧录、坏区规避与 SSID 配置 Patch:工具评估与验证路径
|
||
|
||
> 评估对象:`C:\Users\HelixCopex\code\ESP32S3_vt` 当前工作区(ESP32-S3 图传固件 + Tauri 桌面端)。
|
||
> 评估日期:2026-10-19。评估员:工具评估师(DSH)。
|
||
> 置信度:中高。静态代码、构建产物、烧录/校验日志证据充分;坏区扫描、断电与 OTA 实验尚未在本机执行,相关结论需按第 6 节 MVE 复核。
|
||
> 下次评审:MVE E2–E5 完成后;或固件模板/分区表一旦变更立即复审。
|
||
>
|
||
> 状态补注(2026-10-19):本稿是提议稿,正文按撰写时的仓库状态记录。已实现的固件维护页走另一条路线:只做 USB 现场更新,客户端 patch 设备当前分区表的 factory 偏移/大小,不改固件源码;SSID 动态修改已放弃,OTA 不在本次范围。实现见 [`firmware-maintenance.md`](firmware-maintenance.md)。文中的 config 分区与 OTA-ready 布局仍是未实施建议;2.3 节“桌面端串口/打包”两行的现状已过时(现已引入 `serialport`、`espflash`,bundle targets 为 `all`)。
|
||
|
||
---
|
||
|
||
## 0. 管理层摘要
|
||
|
||
**推荐方案(条件性)**
|
||
|
||
> 一次性冻结模板固件(新增 `config` 分区读取 + OTA-ready 分区布局)+ 工具侧按分区/槽位做实际镜像写读校验 + 配置区多副本 CRC + P0 用 Python + esptool 做参考流水线,P1 再封装进 Tauri。
|
||
|
||
这不是“选一个万能工具”,而是三层组合:
|
||
|
||
1. **参考/校验层**:`esptool`(当前 4.12.0)负责原始读、擦、写、逐段 `verify_flash`、镜像信息检查;
|
||
2. **产品写入层**:`espflash` 4.4.0(Apache-2.0、单文件 Rust)负责发现端口与快速写入;其校验结果必须与 esptool 做 parity(一致性)测试后才能单独作为门禁;
|
||
3. **业务编排层**:Python CLI(P0)或 Tauri Rust 后端(P1)负责设备发现、清单匹配、坏区映射、槽位选择、配置 patch、报告与回归。
|
||
|
||
**关键判断**
|
||
|
||
- `esptool` 和 `espflash` 都没有坏块扫描、坏块跳过或坏块重映射命令;它们只有“按地址读/擦/写/校验”的原语。所谓“自动屏蔽”必须由上层工具实现。
|
||
- 典型 SPI NOR 没有 NAND 式坏块表/坏块标记;本板的故障是字节级位缺陷(`0xFADE7` 的 bit 5–7、`0x10863` 的 bit 6 可疑),不是可被文件系统跳过的“坏块”。
|
||
- **可启动 App 不能在镜像内部跳过坏扇区**:Bootloader 按分区偏移线性读取,App 的链接地址也假定连续 Flash。唯一正确的规避是“整分区/整槽位选择”,或用实际镜像写读校验证明该槽位对当前镜像可用。
|
||
- **合并镜像(merged image)在坏板上是高风险路径**:它从 `0x0` 连续写入,包含分区间填充,坏字节落在填充区也会导致校验失败或写入无意义。必须分段写入、分段校验。
|
||
- **不要直接 patch App 字节来改 SSID**:当前 App 镜像的 `Checksum` 与 `Validation Hash` 都是 valid(见 [`target/firmware/IMAGE-INFO.txt`](../target/firmware/IMAGE-INFO.txt)),直接改字节会破坏它们;若启用 Secure Boot,还会破坏签名。正确位置是独立 config 分区。
|
||
- 当前 [`scripts/flash.sh`](../scripts/flash.sh) 的“搜索 64 KiB 对齐偏移 + 改分区表 + 重编译”是应急规避,不是可回归的 provisioning 方案:它依赖 macOS 串口/termios、会改 `partitions.csv`、会重编译两次,且每次重编都可能移动镜像字节。
|
||
- **需要一次性模板变更**:新增 config 分区读取逻辑、可选 OTA 布局。这里的“不重新编译”应定义为“每台设备不再编译”,而不是“永远不改固件源码”;若连一次性模板变更都不允许,只能走脆弱的 App 字节 patch,本报告不推荐。
|
||
|
||
**投入与收益(量级)**
|
||
|
||
| 项目 | 估算 |
|
||
| --- | --- |
|
||
| P0 参考流水线 + MVE | 5–8 工程师日 |
|
||
| 模板固件(config 分区 + 解析 + 单测) | 3–5 工程师日 |
|
||
| 坏区扫描硬化 | 3–5 工程师日 |
|
||
| P1 桌面端集成(发现/UI/报告/侧车) | 8–12 工程师日 |
|
||
| 测试矩阵 + 硬件在环 | 5–8 工程师日 |
|
||
| 文档/培训 | 2–3 工程师日 |
|
||
| OTA(若纳入范围) | 5–10 工程师日 |
|
||
| 硬件(继电器/Hub/适配器/备件,不含板卡) | 约 ¥500–1,300($70–180);板卡另计 |
|
||
| 软件许可 | ¥0;esptool 为 GPL-2.0-or-later,桌面端打包需做 GPL 合规评估;espflash 为 Apache-2.0 |
|
||
|
||
收益主要不是省编译时间,而是降低坏板 brick/返修风险、让每台设备的配置可追溯、让烧录与坏区判定可自动回归。若年产量只有几十台,ROI 不是主要理由;若达到数百台/年,配置 patch 与自动校验的工时收益会明显。
|
||
|
||
**需要用户先确认的 4 件事**
|
||
|
||
1. 是否允许一次性修改模板固件(新增 config 分区读取;可选 OTA)?
|
||
2. OTA 是否在本次范围内?当前分区表没有任何 OTA 分区,但测试矩阵已包含 OTA,建议本次直接采用 OTA-ready 布局,避免二次迁移。
|
||
3. App 分区是否可从 `0x120000` 扩到 `0x140000`?当前 App 已占 **96.94%**(1,143,504 / 1,179,648),只剩 36,144 字节;加入 config 加载/OTA 代码后很可能不够。
|
||
4. 是否启用/计划启用 Secure Boot、Flash Encryption、NVS 加密?这决定 config 分区能否明文写入、App 能否重签。
|
||
|
||
---
|
||
|
||
## 1. 结论先行:七个必须接受的技术事实
|
||
|
||
1. **没有“坏块感知烧录器”**。esptool/espflash 不会读取坏块表,也不会跳过坏扇区;它们只会在写入后做 digest 校验,失败就报错。仓库里已有的 `espflash::verify_failed` 日志([`target/fw-budget40-flash.log`](../target/fw-budget40-flash.log)、[`target/soak/vga30-fb3-flash.log`](../target/soak/vga30-fb3-flash.log))说明“写入命令返回”不等于“内容正确”。
|
||
2. **SPI NOR 的故障粒度是位/字节,不是 NAND 的块**。把 `0xFADE7` 叫“坏块”只是工程口语;工具应按 4 KiB 扇区统计,但根因要记录到字节/位。
|
||
3. **Bootable App 只能整槽位规避**。如果坏字节落在 App 镜像的某个字节上,不能“跳过 4 KiB 继续写”;只能换分区偏移、换 OTA 槽位,或证明当前镜像恰好能写入该字节。
|
||
4. **实际镜像写读校验优先于通用图案扫描**。通用 `0x00/0x55/0xAA` 图案能发现“对任意数据都坏”的扇区,但会误报“仅对某些字节坏、当前镜像恰好可写”的槽位。App 槽位的最终判据应是“写入实际 App 镜像 → 读回 → 全字节比较通过”。
|
||
5. **合并镜像不适合坏板**。它把 Bootloader、分区表、App 和填充区从 `0x0` 连续写入;坏字节在填充区也会被写入/校验,导致整包失败。分段写入能把故障局部化,也便于独立校验。
|
||
6. **App 字节 patch 不是配置方案**。即使只改 SSID 字符串,也会改变 App 的 `Checksum` 和 `Validation Hash`;需要自定义工具重算,且 Secure Boot 下还要重签。config 分区 patch 不触碰 App,App SHA-256 保持不变,天然满足“模板固件 + patch”。
|
||
7. **当前 `flash.sh` 不能作为回归基线**。它会在构建后搜索偏移、改 `partitions.csv`、删 `esp-idf-sys` 构建目录、再编一次;这是“让镜像躲开一个已知字节”,不是“检测并屏蔽坏区”。它可以保留为故障板应急脚本,但不应进入产品 provisioning 流程。
|
||
|
||
---
|
||
|
||
## 2. 现状审计(基于仓库证据)
|
||
|
||
### 2.1 固件、Flash 与分区
|
||
|
||
| 项目 | 当前值 | 证据 | 影响 |
|
||
| --- | --- | --- | --- |
|
||
| SoC / 模组 | ESP32-S3-WROOM-1-N16R8,16 MB Flash + 8 MB Octal PSRAM | [`README.md`](../README.md) 硬件表、[`sdkconfig.defaults`](../sdkconfig.defaults) | 目标固定,工具可按 chip/flash 做兼容性门禁 |
|
||
| Flash 模式 | 16 MB、DIO、40 MHz | [`sdkconfig.defaults`](../sdkconfig.defaults) `CONFIG_ESPTOOLPY_*` | 烧录参数必须与构建一致;QIO/80 MHz 在本板会 boot loop |
|
||
| Bootloader | `0x0`,当前 0x5160 字节 | [`DEVELOPMENT.md` 镜像导出](../DEVELOPMENT.md)、[`target/latency/tx15-verify.log`](../target/latency/tx15-verify.log) | Bootloader 未变时不要重写;写入是单点故障 |
|
||
| 分区表 | `0x8000`,当前 0xC00 字节 | [`sdkconfig.defaults`](../sdkconfig.defaults) `CONFIG_PARTITION_TABLE_OFFSET=0x8000`、verify 日志 | 分区表与 Bootloader/App 必须成套 |
|
||
| NVS | `0x9000`,`0x6000` | [`partitions.csv`](../partitions.csv) | 目前主要由 Wi-Fi 驱动使用,不应塞入业务配置 |
|
||
| PHY init | `0xF000`,`0x1000` | 同上 | 保存射频校准,不要整片擦除 |
|
||
| Factory App | `0x70000`,`0x120000`(1,179,648 B) | 同上、[`BUILD-INFO.json`](../target/firmware/BUILD-INFO.json) | App 实际 1,143,504 B,占用 **96.94%**,余量仅 36,144 B |
|
||
| OTA | **无** `otadata` / `ota_0` / `ota_1` | [`partitions.csv`](../partitions.csv)、`grep OTA` 无结果 | OTA 测试当前无法执行;需一次性布局变更 |
|
||
| 坏字节 1 | `0xFADE7`,bit 5–7 写入异常 | [`HARDWARE.md`](../HARDWARE.md#损坏-flash-模组的兼容处理) | 当前 App 镜像在该地址的字节为 `0x00`,恰好可写 |
|
||
| 坏字节 2 | `0x10863`,bit 6 读回为 1(期望 `0x2e`,读回 `0x6e`) | 同上、[`BUILD-INFO.json`](../target/firmware/BUILD-INFO.json) | 尚未确认永久故障;当前靠 App 偏移 ≥ `0x20000` 避开 |
|
||
| App 镜像完整性 | `Checksum: 9e (valid)`、`Validation Hash: ... (valid)` | [`target/firmware/IMAGE-INFO.txt`](../target/firmware/IMAGE-INFO.txt) | 直接改 App 字节会破坏两者 |
|
||
|
||
**当前坏板规避逻辑**([`scripts/flash.sh`](../scripts/flash.sh)):
|
||
|
||
1. `espflash save-image` 导出 App;
|
||
2. 从 `0x20000` 开始按 64 KiB 步进,找最低的偏移,使 App 中落到 `0xFADE7` 的那个字节满足 `byte & 0xE0 == 0`;
|
||
3. 把偏移写回 `partitions.csv`;
|
||
4. 删除 `esp-idf-sys` 构建目录并重编译;
|
||
5. 再次导出 App,确认字节仍可写;
|
||
6. 用 `esptool write-flash` 写 `0x0` Bootloader、`0x8000` 分区表、`offset` App;
|
||
7. 串口观察。
|
||
|
||
这条链路的问题:
|
||
|
||
- 它只检查 `0xFADE7` 一个地址,不检测其他潜在坏字节;
|
||
- 它要求“镜像字节恰好是 0x00–0x1F”,换一次编译就可能失效;
|
||
- 它修改版本控制中的 `partitions.csv`,重编两次,无法做到“每台不编译”;
|
||
- 它依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录,Windows 不可用;
|
||
- 它没有独立记录“哪些扇区被判定坏、依据是什么”。
|
||
|
||
### 2.2 工具链与现有烧录入口
|
||
|
||
| 工具/入口 | 版本/位置 | 现状 | 结论 |
|
||
| --- | --- | --- | --- |
|
||
| `espflash` | 4.4.0,仓库 [`target/tools/espflash/espflash.exe`](../target/tools/espflash/espflash.exe) | 用于 `flash`、`save-image --merge`、`write-bin`、`monitor`;日志中出现 `verify_failed` | 可做快速写入/合并镜像,但坏板上不能把它的 verify 当唯一门禁 |
|
||
| `esptool` | 4.12.0(`IMAGE-INFO` 与 verify 日志) | 用于三段独立 `verify_flash`;`flash.sh` 用 `write-flash` | 作为原始读/擦/写/逐段校验的参考工具 |
|
||
| ESP-IDF | v5.5.5 | `esptool`/`parttool`/`otatool`/`nvs_partition_gen` 均随 IDF Python 环境提供 | 可用 `parttool.py` 做命名分区、`otatool.py` 做 OTA |
|
||
| Rust 工具链 | `esp-1.97.0.0` | [`README.md`](../README.md) | 模板固件构建固定;不随 provisioning 变化 |
|
||
| `export-firmware.ps1` | 硬编码 `C:\s3vt\...` | 导出 App/Bootloader/分区表/merged | 需改为可移植、带 manifest 的导出 |
|
||
| `verify-firmware.py` | 仓库 `target/` | 解析分区表、检查 merged、检查 `0xFADE7` | 可扩展为“布局不变量 + manifest 校验” |
|
||
| CI | [`.github/workflows/build-client.yml`](../.github/workflows/build-client.yml) | 只构建桌面端、跑 protocol/core 测试 | **没有固件构建、没有硬件在环** |
|
||
|
||
### 2.3 客户端、配置与测试
|
||
|
||
| 项目 | 现状 | 证据 | 差距 |
|
||
| --- | --- | --- | --- |
|
||
| SSID/密码 | 编译期 `env!("WIFI_SSID")` / `env!("WIFI_PASS")` | [`src/main.rs`](../src/main.rs) 36–37、582–599 | 每换 SSID 必须重编;无法按台 patch |
|
||
| 运行时配置 | 无;NVS 只传给 `EspWifi` | [`src/main.rs`](../src/main.rs) 168–180 | 需新增 config 分区读取 + 回退 |
|
||
| 桌面端串口 | 无 `serialport` 依赖 | [`desktop/src-tauri/Cargo.toml`](../desktop/src-tauri/Cargo.toml) | 需新增设备发现/烧录后端 |
|
||
| 桌面端打包 | Tauri v2,bundle targets `app/dmg`,CI 覆盖 | [`desktop/src-tauri/tauri.conf.json`](../desktop/src-tauri/tauri.conf.json) | 需加 sidecar/资源与 Windows NSIS |
|
||
| 串口测试 | `serial_soak.py`、`capture-serial.py` | [`scripts/serial_soak.py`](../scripts/serial_soak.py)、[`target/capture-serial.py`](../target/capture-serial.py) | 可复用为 provisioning 后验证 |
|
||
| UDP 验证 | `udp_probe.py`、`perf_probe.py` | [`scripts/udp_probe.py`](../scripts/udp_probe.py)、[`scripts/perf_probe.py`](../scripts/perf_probe.py) | 可复用为启动后验收 |
|
||
| 主机测试文化 | protocol/core/stream-control 有 host tests | [`DEVELOPMENT.md`](../DEVELOPMENT.md) | 新 config 格式应放共享 crate,做 host tests |
|
||
| 产物完整性 | `SHA256SUMS.txt`、`BUILD-INFO.json`、verify 日志 | [`target/firmware/`](../target/firmware) | 可扩展为 manifest + 设备 provisioning 报告 |
|
||
|
||
### 2.4 差距总表
|
||
|
||
| 需求 | 当前能力 | 差距 | 优先级 |
|
||
| --- | --- | --- | --- |
|
||
| 插入设备自动扫描识别 | 无;桌面端只做 UDP 网络发现 | USB 枚举 + VID/PID 过滤 + 主动探测 + 热插拔去抖 | P0 |
|
||
| 选择固件烧录 | 手工路径/命令 | 固件清单、兼容性校验、分段写入、逐段校验 | P0 |
|
||
| 坏区检测并自动屏蔽 | 单字节偏移搜索 + 重编译 | 分区感知扫描、实际镜像校验、槽位选择、配置副本 | P0 |
|
||
| 动态 patch SSID | 编译期常量 | config 分区 + CRC + 多副本 + 回退 | P0 |
|
||
| 不重新编译固件 | 每改 SSID 重编 | 模板固件一次构建;每台只写 config | P0 |
|
||
| 自动化验证/回归 | 手工脚本 + 零散日志 | pytest + 硬件在环 + JSON 报告 + CI 门禁 | P1 |
|
||
| OTA | 无分区、无回滚 | OTA-ready 布局 + 签名/回滚 + 测试 | P1/P2 |
|
||
| 安全 | 凭据编译进固件 | config 明文风险、Secure Boot/加密预检、日志脱敏 | P0/P1 |
|
||
|
||
---
|
||
|
||
## 3. 需求拆解与术语校准
|
||
|
||
| 术语 | 本报告中的定义 | 工程含义 |
|
||
| --- | --- | --- |
|
||
| 坏块 | 工程口语,指某个 4 KiB 扇区内存在无法可靠写入/读回的位或字节 | SPI NOR 通常没有坏块表;按扇区统计,按字节定位 |
|
||
| 坏字节 | 某个绝对地址的位固定/翻转,例如 `0xFADE7` | 通用图案扫描会命中;实际镜像未必受影响 |
|
||
| 坏扇区 | 4 KiB 擦除/编程/读回至少一种图案失败 | App 槽位可整体避开;config 可换副本 |
|
||
| 自动屏蔽 | 在分区/槽位/副本层面避开不可靠区域 | 不是把坏扇区从 App 镜像中间“跳过” |
|
||
| 模板固件 | 一次性构建、所有设备共用、App SHA-256 相同的固件 | 每台只写 config,不重编 |
|
||
| Config patch | 把 SSID/密码等写入独立 config 分区的某个副本 | 不改 App、不改分区表、不重编 |
|
||
| 实际镜像校验 | 写入真实 App/Config 数据后读回逐字节比较 | 比通用图案更贴近最终用途 |
|
||
| 通用图案扫描 | 写 `0x00/0x55/0xAA` 等图案并读回比较 | 用于发现硬故障/评估余量,不单独决定 App 槽位 |
|
||
| OTA-ready | 分区表预留 `otadata`、`ota_0`、`ota_1`,Bootloader 支持回滚 | 即使暂不启用 OTA,也避免二次迁移 |
|
||
|
||
> **需求校准**:如果“自动屏蔽坏块”被理解为“在 App 镜像中间跳过坏扇区、仍然正常启动”,在不改 Bootloader、不引入 FTL、不让 App 从文件系统加载的前提下**不可实现**。可交付的定义是:App 选一个实际镜像写读校验通过的槽位;Config 用多副本;Data 区可做扇区跳过。若业务坚持“单槽 App 内部跳过”,应改为更换模组或重新设计启动链,而不是继续调烧录参数。
|
||
|
||
**“不重新编译”的边界**
|
||
|
||
- 推荐定义:每台设备不再编译;模板固件由 CI 构建一次,产物带 SHA-256/manifest;
|
||
- 若业务要求“连模板源码都不能改”,则只能做 App 字节 patch:需要固定长度占位符、重算镜像 checksum/Validation Hash、Secure Boot 下重签;这是高风险的 Plan C,不应作为主方案;
|
||
- 若未来修改 App 逻辑,仍然需要重新构建模板;这不违反“每台不编译”,但需要重新跑布局/坏区/OTA 回归。
|
||
|
||
**“自动扫描识别”的边界**
|
||
|
||
- 建议定义为:USB 插入 → 枚举串口 → 按 VID/PID 过滤 → 主动探测 chip/MAC/Flash → 出现在设备列表;
|
||
- 不建议把“网络发现”混进 provisioning:网络发现只能证明设备已启动,不能替代 USB 烧录通道;
|
||
- 主动探测通常会复位设备进入 Bootloader;需要提供“被动枚举”和“主动探测”两种模式,避免在设备正在推流时误复位;
|
||
- 固件侧“自动扫描”只扫描受控目录(如 `firmware/*/manifest.json`),不是任意路径;manifest 的 SHA-256/兼容性校验通过后才进入可选列表。
|
||
|
||
---
|
||
|
||
## 4. 工具能力评估
|
||
|
||
### 4.1 评估维度与权重
|
||
|
||
权重按本项目“坏板 + 免重编 + 可回归”的目标设定:
|
||
|
||
| 维度 | 权重 | 说明 |
|
||
| --- | --- | --- |
|
||
| 坏区检测/规避能力 | 25% | 能否发现坏字节/坏扇区,能否支持槽位/副本规避 |
|
||
| 分区级写入/校验 | 20% | 按偏移/分区名写、擦、读、逐段校验 |
|
||
| 二进制/配置区 patch | 15% | 能否安全改配置区而不破坏镜像 |
|
||
| 自动化/CI 可集成 | 15% | CLI/API、退出码、可解析输出、跨平台 |
|
||
| 坏板可靠性/可恢复性 | 10% | 失败是否可诊断、可重试、可恢复 |
|
||
| 桌面集成/许可/分发 | 10% | 单文件、许可证、Python 依赖、签名 |
|
||
| 成本/学习曲线 | 5% | 团队上手与维护成本 |
|
||
|
||
分数为 0–10 的专家判断,MVE 后用实测修正。
|
||
|
||
### 4.2 能力矩阵
|
||
|
||
图例:✅ 原生支持;⚠️ 需外部脚本/有限支持;❌ 不支持。
|
||
|
||
| 能力 | esptool | espflash | ESP-IDF parttool/otatool | NVS 工具 | 自研 Python 编排 | Tauri + serialport |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| 坏区扫描 | ❌(只有读/写/擦原语) | ❌ | ❌ | ❌ | ✅(组合原语) | ✅(调用编排层) |
|
||
| 坏区跳过/重映射 | ❌ | ❌ | ❌ | ❌ | ⚠️(槽位/副本层面) | ⚠️(同左) |
|
||
| 按偏移写/擦/读 | ✅ | ✅(`write-bin` 等,需确认 4.4.0 子命令) | ✅(按分区名) | ⚠️(整分区) | ✅ | ✅ |
|
||
| 逐段校验 | ✅ `verify_flash` | ⚠️(有 verify,但仓库有失败日志) | ✅(包装 esptool) | ⚠️ | ✅ | ✅ |
|
||
| 原始全片备份 | ✅ `read_flash` | ⚠️(需确认) | ⚠️ | ❌ | ✅ | ✅ |
|
||
| App 二进制 patch | ❌(不重算 checksum/hash) | ❌ | ❌ | ❌ | ⚠️(需自研) | ⚠️ |
|
||
| Config 分区 patch | ✅(按偏移写) | ✅(按偏移写) | ✅(按分区名) | ⚠️(NVS 格式) | ✅ | ✅ |
|
||
| OTA | ❌(只写镜像) | ❌ | ✅ `otatool.py` | ❌ | ⚠️(需 IDF OTA) | ⚠️ |
|
||
| 自动化/CI | ✅ CLI/退出码 | ✅ CLI | ✅ CLI | ✅ CLI | ✅ | ✅ |
|
||
| 单文件分发 | ❌(Python) | ✅ | ❌(Python/IDF) | ❌ | ❌ | ✅(Rust 后端) |
|
||
| 许可证 | GPL-2.0-or-later | Apache-2.0 | Apache-2.0(IDF 脚本;运行时通常调用 GPL 的 esptool) | Apache-2.0(IDF) | 自研 | Tauri MIT/Apache;serialport 等依赖各自许可证(需核对) |
|
||
|
||
> 注:`espflash` 4.4.0 的 `read-flash`/`erase-region` 等子命令需在 MVE E0 用 `--help` 确认;本仓库只使用过 `flash`、`save-image`、`write-bin`、`monitor`。未确认前,扫描/回读统一走 esptool。
|
||
|
||
### 4.3 esptool:能力与限制
|
||
|
||
**能做**
|
||
|
||
- `read_flash`:按绝对地址读任意长度,适合备份、读分区表、读 App/Config 回读;
|
||
- `erase_region`:按 4 KiB 对齐擦除;
|
||
- `write_flash`:按“偏移 + 文件”写入,可一次多段;只擦除覆盖到的扇区;
|
||
- `verify_flash`:把 Flash 内容与文件做 digest 比较;仓库日志中的 `verify OK (digest matched)` 就是它;
|
||
- `image_info`:解析 App 镜像的 segments、Checksum、Validation Hash;仓库用它生成 [`IMAGE-INFO.txt`](../target/firmware/IMAGE-INFO.txt);
|
||
- `flash_id`/`read_mac`/`chip_id`:识别芯片、Flash、MAC,适合做设备身份与兼容性门禁;
|
||
- `merge_bin`:把多段合并为一个从 `0x0` 写入的镜像(适合健康板/归档,不适合坏板)。
|
||
|
||
**不能做**
|
||
|
||
- 没有“扫描坏块”“跳过坏块”“坏块重映射”“坏块表”命令;
|
||
- 不会理解分区语义(除了镜像格式);按分区名操作要靠 `parttool.py`;
|
||
- 不会重算 App 的 `Checksum`/`Validation Hash`;不能安全地做 App 字节 patch;
|
||
- 不会在写入失败时自动换槽位/重试到好扇区;
|
||
- 默认校验只覆盖“本次写入的区间”,不覆盖全片;不要用“命令成功”推断整片健康。
|
||
|
||
**关键限制**
|
||
|
||
1. **合并镜像的填充区**:`merge_bin` 生成的镜像从 `0x0` 连续排布,分区间会填充;`write_flash 0x0 merged.bin` 会把这些填充也写进去。坏板上的坏字节若落在填充区,校验会失败;即使不失败,也浪费写入并扩大故障面。
|
||
2. **小文件写入会擦整个扇区**:写 256 字节 config 也会擦除其所在的 4 KiB 扇区;因此 config 必须“一条记录占一个扇区”,不能把多条记录塞在同一扇区。
|
||
3. **版本/命令名差异**:v4 使用 `write_flash` 等下划线形式;v5 改为 `write-flash` 等连字符形式,并可能改变输出。**锁定 4.12.0 或 5.6.0 其中之一**,不要在同一流水线混用。
|
||
4. **GPL**:esptool 是 GPL-2.0-or-later;如果把它打包进闭源桌面安装包,需要做 GPL 合规(源码提供、许可证声明等)。P0 内部工具无碍;P1 产品化优先 espflash 或独立进程+法律评估。
|
||
|
||
### 4.4 espflash:能力与限制
|
||
|
||
**优势**
|
||
|
||
- 单文件 Rust 二进制,Apache-2.0,桌面端打包/签名比 Python 容易;
|
||
- 本仓库已在用:`flash`、`save-image --merge --skip-padding`、`write-bin`、`monitor`;
|
||
- 写入速度快,支持 `--chip/--port/--flash-size/--flash-mode/--flash-freq/--partition-table/--bootloader`;
|
||
- 适合做“产品侧写入器”和“健康板快速烧录”。
|
||
|
||
**风险**
|
||
|
||
- 仓库中两次 `espflash::verify_failed`([`target/fw-budget40-flash.log`](../target/fw-budget40-flash.log)、[`target/soak/vga30-fb3-flash.log`](../target/soak/vga30-fb3-flash.log))没有记录根因;不能据此断言是坏字节,但足以说明**不能把 espflash 的 verify 当唯一验收**;
|
||
- 其校验实现/覆盖范围与 esptool 不完全相同;在坏板上可能出现“esptool 分段 verify 通过、espflash 整包 verify 失败”或反之;
|
||
- 没有坏区扫描/槽位选择;`write-bin` 只写指定地址;
|
||
- 合并镜像同样有填充区问题。
|
||
|
||
**建议**
|
||
|
||
- 用 espflash 做“写”,用 esptool 做“独立读回/逐段校验”;
|
||
- 在 MVE E4 做 parity 测试:同一镜像分别用 espflash 和 esptool 写/校验,记录差异;只有 parity 稳定后,才允许 espflash 单独作为健康板门禁;
|
||
- 坏板/迁移路径默认走 esptool 分段写入。
|
||
|
||
### 4.5 ESP-IDF parttool / otatool / NVS 工具
|
||
|
||
| 工具 | 用途 | 适用点 | 限制 |
|
||
| --- | --- | --- | --- |
|
||
| `parttool.py` | 按分区名读/写/擦、查分区信息 | config 分区 patch、按名字定位、避免硬编码偏移 | 依赖 IDF Python 环境和分区表;仍是 esptool 包装 |
|
||
| `otatool.py` | 读写 OTA 分区、切换启动分区、查询 OTA 信息 | OTA 测试、强制回滚实验 | 需要 OTA 分区布局;不负责签名/业务校验 |
|
||
| `nvs_partition_gen.py` | 从 CSV 生成 NVS 镜像 | 如果坚持把配置放 NVS | 生成整分区,容易覆盖 Wi-Fi 驱动使用的 NVS;不推荐与 Wi-Fi 共用 |
|
||
| `nvs_tool.py` | 解析/导出 NVS | 诊断 | 不适合作为高频 patch 通道 |
|
||
| `espsecure.py` | 签名/加密 | Secure Boot/Flash Encryption | 需要密钥与流程;不是 patch 工具 |
|
||
| `espefuse.py` | 查看/烧录 eFuse | 安全预检(Secure Boot/Flash Encryption 状态) | 写 eFuse 不可逆,必须只读预检 |
|
||
|
||
**结论**:`parttool.py` 很适合 config 分区,`otatool.py` 适合 OTA;但它们都不是“坏块工具”。NVS 方案不如独立 raw config 分区:NVS 与 Wi-Fi 驱动共用分区,整分区生成/写入会带来覆盖风险。
|
||
|
||
### 4.6 方案评分与选型
|
||
|
||
| 方案 | 坏区 25% | 分区 20% | Patch 15% | 自动化 15% | 坏板可靠 10% | 集成/许可 10% | 成本 5% | 加权 |
|
||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||
| A. 纯 esptool CLI | 3 | 9 | 4 | 9 | 8 | 5 | 9 | **6.25** |
|
||
| B. 纯 espflash | 2 | 8 | 3 | 8 | 5 | 9 | 8 | **5.55** |
|
||
| C. ESP-IDF parttool/otatool + esptool | 2 | 9 | 8 | 7 | 8 | 4 | 6 | **6.05** |
|
||
| D. Python 编排 + esptool(P0) | 8 | 9 | 9 | 9 | 8 | 5 | 6 | **8.10** |
|
||
| E. D 的产品化(Tauri + serialport + espflash 侧车 + esptool 校验) | 8 | 9 | 9 | 8 | 8 | 9 | 5 | **8.30** |
|
||
|
||
**推荐组合**
|
||
|
||
- **P0(现在)**:方案 D。Python 3.11 + esptool 4.12.0 + pyserial + pytest;一个 `provision.py` CLI 完成发现/扫描/烧录/config patch/校验/报告。先不碰桌面端,最快拿到可回归证据。
|
||
- **P1(产品化)**:方案 E。Tauri Rust 后端用 `serialport` 做发现,`espflash` 侧车做写入,esptool 做独立校验;UI 只负责选择/展示/确认。
|
||
- **P2(OTA)**:在 E 的基础上加 `otatool.py` 或 ESP-IDF OTA;OTA 分区/回滚在模板阶段一次到位。
|
||
- **不推荐**:纯 B(坏板可靠性不足)、纯 A 打包进桌面(Python/GPL)、直接 App 字节 patch、坏板写 merged image。
|
||
|
||
**敏感性**
|
||
|
||
- 若“桌面端不能带 Python/GPL”,D 的集成分下降、E 上升;P0 仍可用 D 做内部工具,P1 再换。
|
||
- 若 OTA 是硬需求,C/E 的 OTA 能力加分;但仍需自定义“坏槽位安全失败”逻辑。
|
||
- 若把“坏板可靠”权重提到 20%,D/E 仍领先,因为坏区能力是自研编排层提供的,不是 esptool/espflash 原生能力。
|
||
|
||
---
|
||
|
||
## 5. 推荐架构
|
||
|
||
### 5.1 总体架构
|
||
|
||
```text
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ Tauri 桌面端(P1) / Python CLI(P0) │
|
||
│ - 设备列表/热插拔 - 固件清单/兼容性选择 │
|
||
│ - 扫描结果/坏区地图 - SSID/密码表单(不落日志) │
|
||
│ - 进度/日志/报告导出 - 失败恢复指引 │
|
||
└───────────────┬──────────────────────────────────────────────────┘
|
||
│ 命令/事件(Tauri IPC 或 CLI 参数)
|
||
┌───────────────▼──────────────────────────────────────────────────┐
|
||
│ Provisioning Orchestrator(Rust 或 Python) │
|
||
│ 1. 发现/识别:serialport → VID/PID → chip_id/read_mac/flash_id │
|
||
│ 2. 清单匹配:chip/flash/layout/app_sha256/config_schema │
|
||
│ 3. 备份/扫描:分段 read_flash + 实际镜像/图案校验 │
|
||
│ 4. 槽位选择:App slot / config slot / 失败策略 │
|
||
│ 5. 写入:esptool/espflash 分段 write;安全顺序;逐段 verify │
|
||
│ 6. 配置:生成 config record;写多副本;回读 CRC │
|
||
│ 7. 启动验收:串口日志 + UDP/perf probe │
|
||
│ 8. 报告:JSON + 日志 + SHA-256 + 坏区地图 │
|
||
└───────────────┬──────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────▼──────────────────────────────────────────────────┐
|
||
│ 固件(一次性模板) │
|
||
│ Bootloader 0x0 → 分区表 0x8000 → App(factory/ota) → config 分区 │
|
||
│ App 启动:读 config 多副本 → 校验 magic/version/CRC/seq → Wi-Fi │
|
||
│ 失败回退:编译期默认 SSID/PASS,并打印 config source │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 5.2 分区布局:从“单 App 躲字节”改为“统一布局 + 槽位”
|
||
|
||
**当前布局**
|
||
|
||
| 分区 | 偏移 | 大小 | 说明 |
|
||
| --- | ---: | ---: | --- |
|
||
| Bootloader | `0x0` | ~`0x5160` | 不随 App 移动 |
|
||
| Partition table | `0x8000` | `0xC00` | 自定义表 |
|
||
| NVS | `0x9000` | `0x6000` | Wi-Fi 驱动 |
|
||
| PHY | `0xF000` | `0x1000` | 射频校准 |
|
||
| Factory | `0x70000` | `0x120000` | 包含 `0xFADE7`,靠镜像字节恰好可写 |
|
||
|
||
**建议布局(OTA-ready;MVE 可先用 `0x120000` 槽位)**
|
||
|
||
| 分区 | 类型/子类型 | 偏移 | 大小 | 说明 |
|
||
| --- | --- | ---: | ---: | --- |
|
||
| Bootloader | — | `0x0` | ≤`0x8000` | 当前 0x5160,保持 |
|
||
| Partition table | — | `0x8000` | `0xC00` | 与 Bootloader/App 成套 |
|
||
| NVS | `data/nvs` | `0x9000` | `0x6000` | 保持现偏移,迁移时保留 |
|
||
| PHY | `data/phy` | `0xF000` | `0x1000` | 保持现偏移,保留校准 |
|
||
| *gap* | — | `0x10000` | `0x1000` | 含 `0x10863`,**不要分配** |
|
||
| `otadata` | `data/ota` | `0x11000` | `0x2000` | OTA 状态,两扇区 |
|
||
| `config` | `data/0x99`(或 `undefined`) | `0x13000` | `0x10000` | 4 个 4 KiB 副本槽 |
|
||
| *gap* | — | `0x23000` | `0xDD000` | 含 `0xFADE7`,**不要分配** |
|
||
| `factory` | `app/factory` | `0x100000` | `0x140000`(建议) | 恢复槽;当前 App 约 1.09 MiB(1,143,504 B) |
|
||
| `ota_0` | `app/ota_0` | `0x240000` | `0x140000` | OTA 槽 A |
|
||
| `ota_1` | `app/ota_1` | `0x380000` | `0x140000` | OTA 槽 B |
|
||
| 空闲/数据 | — | `0x4C0000` | 至 `0x1000000` | 约 11.25 MiB,可放日志/资源 |
|
||
|
||
> 若 MVE 先用 `0x120000` 槽位:`factory=0x100000`、`ota_0=0x220000`、`ota_1=0x340000`、空闲从 `0x460000` 开始;`otadata`/`config` 不变。模板定稿建议 `0x140000`,避免 App 增长后再次迁移。
|
||
|
||
**为什么这样排**
|
||
|
||
- `0x10863` 落在 `0x10000–0x11000` 的空隙;`0xFADE7` 落在 `0x23000–0x100000` 的空隙;两者都不在任何被写入的分区内。
|
||
- App 从 `0x100000` 开始后,不再需要“镜像字节恰好可写”的检查;模板可以稳定重建,不会因为一次编译移动字节而踩雷。
|
||
- `factory` + `ota_0` + `ota_1` 让 OTA 有回滚余地;`factory` 作为恢复镜像,OTA 失败不会失去可启动 App。
|
||
- `config` 独立于 NVS,不与 Wi-Fi 驱动抢分区;4 个副本可跨坏扇区。
|
||
- 统一布局对好板和坏板一致,避免“好板一套、坏板一套”的 provisioning 分叉。
|
||
|
||
**槽位大小决策**
|
||
|
||
- 当前 App 1,143,504 B / `0x120000` = **96.94%**,只剩 36,144 B;
|
||
- 新增 config 读取、日志、OTA rollback 代码后,36 KB 余量偏紧;
|
||
- 建议模板槽位用 `0x140000`(1.25 MiB),留约 163 KiB(167,216 B);三个槽共 3.75 MiB,16 MiB Flash 足够;
|
||
- 若坚持 `0x120000`,当前 96.94% 已经超过“App ≤ 分区 95%”门禁,实际上只剩 35.3 KiB;只能把门禁改为 98%(仍无余量)或扩到 `0x140000`。建议扩。
|
||
|
||
**迁移**
|
||
|
||
- 现网旧布局(`0x70000`)板卡需要一次全量布局迁移:写新分区表 + 新 factory App(+ 可选 Bootloader);
|
||
- NVS/PHY 偏移保持不变,迁移时不要擦除,保留 Wi-Fi 校准;
|
||
- 旧 App 区域 `0x70000` 可保留(不被新表引用),确认稳定后再擦除;
|
||
- 工具检测到旧布局时,应提示“需要迁移”,不要直接把新 App 写到旧偏移。
|
||
|
||
### 5.3 Config 记录格式与 patch 流程
|
||
|
||
**目标**:App 不重编、不改字节;每台只写 256 B 配置记录;至少一个副本可读;掉电不丢配置。
|
||
|
||
**记录格式(256 B,小端)**
|
||
|
||
| 偏移 | 长度 | 字段 | 规则 |
|
||
| ---: | ---: | --- | --- |
|
||
| 0 | 4 | magic | `S3CF`(ASCII 字节 `0x53 0x33 0x43 0x46`) |
|
||
| 4 | 2 | version | 当前 1;不兼容时固件回退默认 |
|
||
| 6 | 2 | flags | bit0=密码存在;其余保留 0 |
|
||
| 8 | 8 | seq | 单调递增;固件选最大 seq |
|
||
| 16 | 1 | ssid_len | 0–32,UTF-8 字节数 |
|
||
| 17 | 1 | pass_len | 0–64,UTF-8 字节数 |
|
||
| 18 | 2 | reserved | 0 |
|
||
| 20 | 32 | ssid | 右补 0;禁止内嵌 `\0` |
|
||
| 52 | 64 | password | 右补 0;**不写日志** |
|
||
| 116 | 4 | crc32 | 对 0–115 字节做 IEEE CRC32 |
|
||
| 120 | 136 | reserved | `0xFF`;未来可放 HMAC/标签 |
|
||
|
||
**副本布局**
|
||
|
||
- `config` 分区 64 KiB = 16 × 4 KiB;
|
||
- 使用前 4 个扇区:`+0x0000`、`+0x1000`、`+0x2000`、`+0x3000`;
|
||
- 每条记录独占一个扇区;写入会擦整个扇区,这是预期行为;
|
||
- 工具选择“当前不是最高 seq 的有效副本”写入,写入后回读校验;至少保留一个旧有效副本;
|
||
- 固件启动时扫描 4 个槽,选 `magic/version/length/CRC` 全部通过且 seq 最大的记录;
|
||
- 若全部无效:回退到编译期 `WIFI_SSID`/`WIFI_PASS`,打印 `config source=default`;
|
||
- 若使用分区成功:打印 `config source=partition seq=N ssid=...`(不打印密码)。
|
||
|
||
**Python 参考实现(可直接用于 P0 工具)**
|
||
|
||
```python
|
||
import struct, zlib
|
||
|
||
MAGIC = b"S3CF"
|
||
VERSION = 1
|
||
RECORD_SIZE = 256
|
||
CRC_OFFSET = 116
|
||
CRC_SPAN = 116
|
||
|
||
def pack_config(ssid: str, password: str, seq: int) -> bytes:
|
||
ssid_b = ssid.encode("utf-8")
|
||
pass_b = password.encode("utf-8")
|
||
assert 0 <= len(ssid_b) <= 32 and 0 <= len(pass_b) <= 64
|
||
assert b"\x00" not in ssid_b and b"\x00" not in pass_b
|
||
header = struct.pack(
|
||
"<4sHHQBBH",
|
||
MAGIC, VERSION, 1 if pass_b else 0, seq,
|
||
len(ssid_b), len(pass_b), 0,
|
||
)
|
||
body = header + ssid_b.ljust(32, b"\x00") + pass_b.ljust(64, b"\x00")
|
||
assert len(body) == CRC_SPAN
|
||
crc = zlib.crc32(body) & 0xFFFFFFFF
|
||
return body + struct.pack("<I", crc) + b"\xFF" * (RECORD_SIZE - CRC_OFFSET - 4)
|
||
|
||
def parse_config(buf: bytes):
|
||
if len(buf) != RECORD_SIZE or buf[:4] != MAGIC:
|
||
return None
|
||
version, flags, seq, ssid_len, pass_len, _ = struct.unpack_from("<HHQBBH", buf, 4)
|
||
if version != VERSION or not (0 <= ssid_len <= 32 and 0 <= pass_len <= 64):
|
||
return None
|
||
if zlib.crc32(buf[:CRC_SPAN]) & 0xFFFFFFFF != struct.unpack_from("<I", buf, CRC_OFFSET)[0]:
|
||
return None
|
||
ssid = buf[20:20 + ssid_len].decode("utf-8")
|
||
password = buf[52:52 + pass_len].decode("utf-8")
|
||
return {"version": version, "flags": flags, "seq": seq,
|
||
"ssid": ssid, "password": password}
|
||
```
|
||
|
||
**固件侧建议**
|
||
|
||
- 把解析逻辑放到可 host test 的共享 crate(类似 `protocol`),用 `cargo +stable test` 覆盖:magic/version/长度/CRC/seq 选择/回退;
|
||
- 读取 `config` 分区用 `esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "config")`,按 label 查找,避免 subtype 依赖;
|
||
- 在 `connect()` 之前加载;`ClientConfiguration` 的 SSID/password 用运行时字符串 `try_into()`;
|
||
- 现有 `SSID` 常量还在 `connect()`、`advertised_auth()`、`report_scan()` 与 `client_config()` 中使用;改为运行时配置后,把这些函数的比较/日志改为传入的 `&str`,否则扫描诊断仍指向编译期默认 SSID;
|
||
- 失败时回退到 `env!` 常量,保持现有行为;
|
||
- 日志只打印 `ssid`、`seq`、`source`,绝不打印密码。
|
||
|
||
**为什么不用 NVS**
|
||
|
||
- NVS 当前由 `EspWifi` 使用;`nvs_partition_gen.py` 生成整分区镜像,容易覆盖 Wi-Fi 驱动的键;
|
||
- NVS 的磨损均衡/CRC 对“整条记录”友好,但对“每台写一次、保留多副本”的 provisioning 不如 raw 分区直观;
|
||
- raw config 可以用 `parttool.py`/`esptool` 按偏移读写,回读/比对简单;
|
||
- 若未来配置项变多,可把 config 分区扩到 128 KiB,仍不与 NVS 冲突。
|
||
|
||
**安全说明**
|
||
|
||
- CRC32 只保证完整性,不保证真实性;能接触 Flash 的人可以改 SSID/密码;
|
||
- 若威胁模型包含物理篡改,增加 HMAC(设备唯一密钥放 eFuse/NVS)或启用 Flash Encryption;
|
||
- provisioning 文件、日志、报告中的密码必须脱敏;报告只记录 `ssid`、`config_sha256`、`seq`;
|
||
- 当前仓库把 Wi-Fi 凭据编译进 App 并随包分发;config 分区至少把凭据从 App 中分离出来,但仍需访问控制。
|
||
|
||
### 5.4 坏区扫描与“自动屏蔽”
|
||
|
||
**核心原则**
|
||
|
||
1. **App 槽位:实际镜像写读校验 > 通用图案扫描**。最终判据是“这个 App 镜像写进去、读回来、逐字节一致”;通用图案只用于评估余量和发现硬故障。
|
||
2. **Config 槽位:实际记录写读校验 + 多副本**。每个槽写真实 256 B 记录,回读/CRC 通过即可用;失败换下一个槽。
|
||
3. **不跳过 App 镜像内部的扇区**。没有文件系统/FTL 能改变 Bootloader 的线性读取;不要尝试“把坏扇区绕过去”。
|
||
4. **扫描目标按分区裁剪**。常规 provisioning 只对 Bootloader/分区表/otadata/config/App 槽位做写入校验;NVS/PHY 含 Wi-Fi/射频校准,默认只读或跳过,除非有已验证备份且确有必要。跳过 `0xFADE7`、`0x10863` 所在的空隙,避免无意义的写入和“恢复失败”。
|
||
5. **先备份,后破坏性测试**。任何写图案/擦除前,先 `read_flash` 备份目标区间;能恢复才做。
|
||
|
||
**扫描算法(分区感知)**
|
||
|
||
```text
|
||
# 图案扫描用于非 App 区;NVS/PHY 默认只读或跳过(除非有已验证备份)
|
||
# App 槽位用实际镜像写读校验(见下)
|
||
for partition in [bootloader, partition_table, otadata, config, data]:
|
||
if partition is known_bad_gap: skip
|
||
backup = read_flash(partition.offset, partition.size)
|
||
for chunk in 64 KiB chunks:
|
||
for pattern in (0x00, 0x55, 0xAA):
|
||
write_flash(chunk.offset, pattern * chunk.size) # 默认校验若失败,继续回读
|
||
readback = read_flash(chunk.offset, chunk.size)
|
||
for each byte: if readback != pattern: mark_bad(offset, expected, actual)
|
||
restore chunk from backup
|
||
verify_flash(chunk.offset, backup_chunk)
|
||
|
||
# App 槽位:写实际镜像 → 读回 → 全字节比较
|
||
for slot in app_slots:
|
||
write_flash(slot.offset, app_image)
|
||
readback = read_flash(slot.offset, len(app_image))
|
||
if readback != app_image: mark_slot_bad(slot, first_mismatch)
|
||
```
|
||
|
||
**为什么用 64 KiB 分块**
|
||
|
||
- 单次全片图案写入如果中途失败,定位困难;64 KiB 能局部化,又比 4 KiB 逐扇区快;
|
||
- `write_flash` 只擦覆盖扇区;分块不会碰其他分区;
|
||
- 对已知坏板,先扫“模板将写入的区间”;对疑似板,再做全片。
|
||
|
||
**图案选择**
|
||
|
||
| 图案 | 能发现 | 说明 |
|
||
| --- | --- | --- |
|
||
| `0x00` | 位 stuck-at-1(无法清 0) | 例如 `0x10863` 的 bit 6 若固定为 1,写 0 会读回 `0x40` 等 |
|
||
| `0x55` | 交替位、相邻位干扰;可发现“无法置 1”的位 | `0xFADE7` 的 bit 5–7 若无法置 1,写 `0x55` 会失败 |
|
||
| `0xAA` | 交替位(反相);可发现“无法置 1”的位 | `1010_1010` |
|
||
| `0xFF` | 位 stuck-at-0(无法置 1) | 擦除态;写 `0xFF` 是无操作,通常通过“擦除后读回”检查 |
|
||
| 可选 `0xA5` | 更复杂的位组合 | 增加覆盖,但时间上升 |
|
||
|
||
**判定与分级**
|
||
|
||
| 结果 | 定义 | 处理 |
|
||
| --- | --- | --- |
|
||
| GOOD | 所有图案写读一致 | 可用于 App/config/data |
|
||
| SUSPECT | 首次失败、重试通过;或仅个别位不稳定 | 隔离;重复扫描;优先不用 |
|
||
| BAD | 多次重试仍失败 | 不用于 App/config;记录绝对地址/位 |
|
||
| UNRECOVERABLE | 恢复备份后仍无法读回原数据 | 停止,标记返修;不要继续写 |
|
||
|
||
**“自动屏蔽”的真实含义**
|
||
|
||
| 区域 | 能否扇区级跳过 | 正确做法 |
|
||
| --- | --- | --- |
|
||
| Bootloader | 否 | 固定 `0x0`;坏则整板不可用/返修 |
|
||
| 分区表 | 否 | 固定 `0x8000`;坏则整板不可用/返修 |
|
||
| App(factory/ota) | 否 | 选一个实际镜像写读校验通过的槽位;否则拒绝/返修 |
|
||
| Config | 是(逻辑层) | 多副本;写实际记录;固件选有效副本 |
|
||
| 数据/日志 | 是(需自研/LittleFS) | 按扇区 CRC + 跳过表;P0 可不做 |
|
||
|
||
**对当前 `0xFADE7` 的正确理解**
|
||
|
||
- 当前 App 在 `0x70000`,镜像字节 `0x00` 恰好可写,所以 `verify OK`;
|
||
- 如果用 `0x55`/`0xAA` 做通用图案扫描,扫描到 `0xFADE7` 会失败;但**这不代表当前 App 不可用**;
|
||
- 因此:App 槽位以“实际镜像 verify”为准;通用扫描结果只作为“该槽位对未来镜像的余量”参考;
|
||
- 新布局把 `0xFADE7` 放进空隙,问题消失;仍保留扫描用于发现其他未知坏字节。
|
||
|
||
**扫描时间**
|
||
|
||
- 16 MB 全片、3 图案、460800 baud:约 10–30 分钟(取决于写入/读取/擦除与重试);
|
||
- 模板写入区间约 4.5 MB:约 3–8 分钟;
|
||
- 生产建议分层:
|
||
- Tier 0(每台,<10 s):`flash_id`/`read_mac` + 读已知坏地址所在扇区 + 实际 App 写读校验;
|
||
- Tier 1(每台或抽检,几分钟):扫描模板写入区间;
|
||
- Tier 2(疑似/RMA,十几分钟):全片扫描。
|
||
|
||
### 5.5 Provisioning 状态机
|
||
|
||
```text
|
||
IDLE
|
||
→ DETECTED USB 插入/枚举到候选串口
|
||
→ IDENTIFIED chip_id + read_mac + flash_id + 读分区表
|
||
→ MANIFEST_OK 固件清单匹配 chip/flash/layout/app_sha256
|
||
→ BACKED_UP 读回关键分区/全片备份(按策略)
|
||
→ SCANNED 坏区地图/实际镜像校验结果
|
||
→ SLOT_SELECTED App slot / config slot 选择
|
||
→ WRITTEN 分段写入(安全顺序)
|
||
→ VERIFIED 逐段 verify + 回读 SHA-256
|
||
→ CONFIGURED config 多副本写入 + 回读 CRC
|
||
→ BOOT_VALIDATED 串口日志 + UDP/perf probe
|
||
→ REPORTED JSON/日志/地图/退出码
|
||
```
|
||
|
||
**安全写入顺序(迁移/首次烧录)**
|
||
|
||
1. 备份(至少 NVS/PHY/旧分区表/旧 App 的关键区);
|
||
2. 写新 App 到新槽位(旧分区表尚未引用它,旧 App 仍可启动);
|
||
3. 写 config 副本(旧 App 不读它,无影响);
|
||
4. 写新分区表(切换引用;这是单点故障,尽量最后写);
|
||
5. 如需更新 Bootloader,最后写;
|
||
6. 复位,串口/UDP 验收。
|
||
|
||
> 注意:当前 `flash.sh` 的顺序是 Bootloader → 分区表 → App;迁移时若在分区表写入后掉电,设备可能无法启动。建议按“先 App、再表、最后 Bootloader”的顺序降低风险。首次空白板可不受此限,但统一顺序更安全。
|
||
|
||
**幂等与失败处理**
|
||
|
||
- 重复执行同一 manifest:允许;先读回/比对,已正确的段可跳过写入;
|
||
- 写入失败:最多重试 3 次;仍失败则标记该槽位 BAD,换槽位;
|
||
- 校验失败:不要重试到“碰巧通过”;先读回定位差异字节,再决定重写/换槽;
|
||
- 断电/拔出:记录最后完成阶段;重新插入后从该阶段恢复;不要报告成功;
|
||
- 退出码建议:0 成功;2 未发现设备;3 清单不兼容;4 扫描失败;5 写入失败;6 校验失败;7 config 失败;8 启动验收失败;9 中断;10 安全预检失败。
|
||
|
||
### 5.6 桌面端/CLI 集成
|
||
|
||
**P0:Python CLI(推荐先做)**
|
||
|
||
```text
|
||
python tools/provision.py discover
|
||
python tools/provision.py identify --port COM5
|
||
python tools/provision.py scan --port COM5 --profile tier1 --out reports/COM5-scan.json
|
||
python tools/provision.py flash --port COM5 --manifest firmware/0.2.0/manifest.json
|
||
python tools/provision.py patch-config --port COM5 --ssid "..." --password-stdin --slot auto
|
||
python tools/provision.py verify --port COM5 --manifest ... --seconds 15
|
||
python tools/provision.py report --out reports/...
|
||
```
|
||
|
||
- 纯 CLI 便于 CI、便于复现;Tauri 后续调用同一套逻辑;
|
||
- 所有命令输出 JSON(`--json`),人类可读日志走 stderr;
|
||
- 密码通过 stdin/环境变量/受限权限临时文件传入,避免命令行历史泄漏;示例中的 `--password-stdin` 是占位,实际参数以工具实现为准;
|
||
- 报告目录按 `<timestamp>-<mac>/` 组织,包含 `device.json`、`scan.json`、`commands.log`、`serial.log`、`udp.json`、`result.json`。
|
||
|
||
**P1:Tauri 集成**
|
||
|
||
- Rust 后端新增 `serialport`(发现/打开/读写)与 provisioning 模块;
|
||
- 侧车:优先 `espflash.exe`(Apache-2.0、单文件);esptool 作为独立校验器(若允许 Python/GPL),或把扫描/校验逻辑移植到 Rust + espflash;
|
||
- Tauri `bundle.externalBin`/`resources` 打包侧车;Windows NSIS 安装包需要代码签名;
|
||
- UI:
|
||
- 设备列表:COM、VID/PID、chip、MAC、Flash、当前布局;
|
||
- 固件选择:manifest 列表、版本/日期/SHA-256/兼容性;
|
||
- 扫描结果:坏区地图(地址、扇区、位、图案、状态);
|
||
- 配置表单:SSID/密码、校验、显示“只写 config、不改 App”;
|
||
- 进度/日志/报告导出/失败恢复指引;
|
||
- 权限:Tauri v2 capabilities 只开必要命令;侧车路径固定;不执行任意用户命令;
|
||
- 单实例/端口锁:同一串口只允许一个 provisioning 进程;避免与 `espflash monitor`/串口助手冲突。
|
||
|
||
**P1 的两种实现路径**
|
||
|
||
| 路径 | 写入 | 校验/扫描 | 优点 | 风险 |
|
||
| --- | --- | --- | --- | --- |
|
||
| P1-A | espflash 侧车 | esptool 侧车(Python) | 复用现有工具、快 | 双运行时、GPL/Python 打包 |
|
||
| P1-B | espflash crate/侧车 | Rust 自研(espflash 读写) | 单运行时、易签名 | 需补 read/erase/verify parity;开发量大 |
|
||
|
||
建议:先用 P1-A 做内部试点;若产品要求单文件/无 Python,再投入 P1-B。
|
||
|
||
### 5.7 与现有脚本/CI 的衔接
|
||
|
||
| 现有资产 | 复用方式 | 需要改动 |
|
||
| --- | --- | --- |
|
||
| `scripts/build.ps1` | 模板构建仍用它;`.env` 只提供默认 SSID | 不再要求每台改 `.env`;构建产物加 manifest |
|
||
| `target/export-firmware.ps1` | 导出 App/Bootloader/分区表 | 去掉硬编码 `C:\s3vt`;生成 `manifest.json` + `SHA256SUMS`;merged 仅归档 |
|
||
| `scripts/flash.sh` | 保留为故障板应急 | 不作为产品入口;标注 macOS-only/会重编 |
|
||
| `target/verify-firmware.py` | 扩展为布局不变量检查 | 检查坏地址不在写入分区;检查 App 大小/分区余量 |
|
||
| `target/refresh-deliverables.py` | 扩展为 manifest/报告打包 | 加入 config schema、布局 hash、工具版本 |
|
||
| `scripts/serial_soak.py` | provisioning 后串口验收 | 增加超时/失败退出码;可被 pytest 调用 |
|
||
| `scripts/udp_probe.py` / `perf_probe.py` | 启动后网络验收 | pytest 解析 JSON;断言帧率/丢帧阈值 |
|
||
| `.github/workflows/build-client.yml` | 复用 CI 结构 | 新增 host tests job;硬件 job 用独立 runner |
|
||
|
||
**建议新增的 CI 分层**
|
||
|
||
1. **PR 门禁(无硬件)**:Python lint/单元测试、config pack/parse、partition table 解析、manifest schema、坏区选择逻辑、命令构造 golden tests;
|
||
2. **Nightly(硬件在环)**:1 好板 + 1 坏板;跑 N/B/H/P 子集;产出 JSON/JUnit;失败保留日志;
|
||
3. **Release 门禁**:模板构建 + 三段 verify + config patch + 启动验收;人工签字。
|
||
|
||
> 现有 CI 的 Windows runner 处于低完整性沙箱(见 workflow 注释),不适合 USB 硬件在环;硬件 job 应使用独立、正常完整性、可访问 USB 的 runner,不要复用 `windows-runner-01`。
|
||
|
||
---
|
||
|
||
## 6. 最小可行性验证实验(MVE)
|
||
|
||
> 目的:用最小成本证明/证伪关键假设,再决定是否投入产品化。
|
||
> 所有命令中的 `COM5`、路径、IP 均需替换。执行前确认板子已备份、供电稳定、没有其他串口程序占用。
|
||
>
|
||
> **最小闭环(Stop/Go)**:E0 → E1 → E2/E3 → E4 → E5。若这五步不能全部通过,不要投入 P1;其中 E3(实际镜像 vs 图案)、E4(分段 vs merged)、E5(config patch 不改 App)是三个关键证伪点。
|
||
|
||
### E0. 工具与设备基线(0.5 天)
|
||
|
||
**目标**:锁定版本、确认 chip/Flash/分区表/镜像信息。
|
||
|
||
```powershell
|
||
python -m esptool version
|
||
.\target\tools\espflash\espflash.exe --version
|
||
python --version
|
||
|
||
python -m esptool --chip esp32s3 --port COM5 flash_id
|
||
python -m esptool --chip esp32s3 --port COM5 read_mac
|
||
python -m esptool --chip esp32s3 --port COM5 read_flash 0x8000 0xC00 partition-table-readback.bin
|
||
python -m esptool --chip esp32s3 image_info target\firmware\esp32s3-app.bin
|
||
```
|
||
|
||
**通过标准**
|
||
|
||
- `esptool` 4.12.0、`espflash` 4.4.0(或明确锁定的替代版本);
|
||
- `flash_id` 显示 16 MB、厂商/型号与 manifest 一致;
|
||
- `read_mac` 得到稳定 MAC;
|
||
- `image_info` 显示 `Checksum valid`、`Validation Hash valid`;
|
||
- 读回分区表可解析,`factory` 偏移与 manifest 一致。
|
||
|
||
**产物**:`reports/E0/versions.txt`、`device.json`、`image-info.txt`。
|
||
|
||
### E1. 全片/关键区备份与读稳定性(0.5 天)
|
||
|
||
**目标**:证明读通道稳定;为后续破坏性实验留后路。
|
||
|
||
```powershell
|
||
python -m esptool --chip esp32s3 --port COM5 --baud 460800 read_flash 0 0x1000000 backup-1.bin
|
||
python -m esptool --chip esp32s3 --port COM5 --baud 460800 read_flash 0 0x1000000 backup-2.bin
|
||
Get-FileHash backup-1.bin, backup-2.bin -Algorithm SHA256
|
||
```
|
||
|
||
**通过标准**
|
||
|
||
- 两次 SHA-256 完全一致;若不一致,先排查 USB/供电/波特率,不进入后续实验;
|
||
- 备份文件 ≥16 MB,SHA-256 记录在报告;
|
||
- 关键区(`0x0–0x23000`、`0x100000–0x4C0000`)单独备份一份,减小恢复粒度。
|
||
|
||
**风险**:460800 下 16 MB 读取约 6–10 分钟;不要用 921600 作为首次备份速率,除非 E0/E1 已证明稳定。
|
||
|
||
### E2. 已知坏字节表征(0.5–1 天)
|
||
|
||
**目标**:复现 `0xFADE7`、`0x10863`,确认是永久还是偶发;得到“期望/实际”位图。
|
||
|
||
**只在一台可恢复的故障板/牺牲板上做;先完成 E1 备份。**
|
||
|
||
```powershell
|
||
# 读周边扇区,先看原始值
|
||
python -m esptool --chip esp32s3 --port COM5 read_flash 0xFA000 0x1000 fade7-before.bin
|
||
python -m esptool --chip esp32s3 --port COM5 read_flash 0x10000 0x1000 s10863-before.bin
|
||
|
||
# 生成 4 KiB 图案文件(0x00/0x55/0xAA 同理)
|
||
python -c "open('pattern-00-4k.bin','wb').write(b'\x00'*4096)"
|
||
|
||
# 擦除 + 写 0x00 图案,读回
|
||
python -m esptool --chip esp32s3 --port COM5 erase_region 0xFA000 0x1000
|
||
python -m esptool --chip esp32s3 --port COM5 write_flash 0xFA000 pattern-00-4k.bin
|
||
# 若 write_flash 因坏字节返回校验失败,不要中断:数据通常已写入,
|
||
# 继续用 read_flash 回读并记录差异;不要依赖 --no-verify(版本支持情况不一)。
|
||
python -m esptool --chip esp32s3 --port COM5 read_flash 0xFA000 0x1000 fade7-00.bin
|
||
# 再写 0x55 / 0xAA / 0xFF,重复读回
|
||
```
|
||
|
||
> 扫描实验的目标是“拿到读回差异”,不是“让 write_flash 返回 0”。最终烧录必须保留默认校验;不要关闭校验。若所用版本确实提供 `--no-verify`,也只可用于图案扫描,并在报告中注明。
|
||
|
||
**通过标准**
|
||
|
||
- `0xFADE7` 在 `0x00` 或 `0x55/0xAA` 图案下复现固定偏差(例如 bit 5–7 保持 1 或 0);
|
||
- `0x10863` 若在多次擦写后仍读回 `0x6E`(期望 `0x2E`),标记为永久;若某次恢复 `0x2E`,标记为偶发/待观察;
|
||
- 恢复原始扇区后 `verify_flash` 通过;若无法恢复,停止并标记该板;
|
||
- 记录:地址、期望字节、实际字节、异或位图、图案、重试次数、温度/供电。
|
||
|
||
**风险**:此实验会擦除目标扇区;必须确认备份可恢复。不要在全片备份未验证前做。
|
||
|
||
### E3. “坏扇区” vs “当前镜像可写”(1 天)
|
||
|
||
**目标**:证明通用图案扫描会误报、实际镜像校验才是 App 槽位的正确判据。
|
||
|
||
**步骤**
|
||
|
||
1. 对当前 `factory` 分区(`0x70000`,`0x120000`)做通用图案扫描,记录 `0xFADE7` 是否被标记 BAD;
|
||
2. 用 `esptool write_flash 0x70000 target\firmware\esp32s3-app.bin` 写实际镜像,`verify_flash`;
|
||
3. 对比:
|
||
- 通用扫描:`0xFADE7` 失败(预期);
|
||
- 实际镜像:`verify OK`(当前镜像字节 `0x00`,预期);
|
||
4. 若可能,构造一个“镜像字节在 `0xFADE7` 为非 0 值”的测试镜像(可用 Python 改一个字节并重算 checksum/hash,仅用于实验),写入同一槽位,预期 `verify` 失败。**不要把这个测试镜像用于启动。**
|
||
|
||
**通过标准**
|
||
|
||
- 通用扫描标记 BAD、实际镜像 verify OK 同时成立;
|
||
- 测试镜像 verify 失败,且失败地址定位到 `0xFADE7`;
|
||
- 结论写入报告:App 槽位必须以实际镜像为准;通用扫描只做余量评估。
|
||
|
||
### E4. 分段写入 vs 合并镜像(0.5 天)
|
||
|
||
**目标**:证明坏板上分段写入可用、合并镜像高风险/失败。
|
||
|
||
```powershell
|
||
# 分段写入(推荐)
|
||
python -m esptool --chip esp32s3 --port COM5 --baud 460800 write_flash `
|
||
--flash_mode dio --flash_freq 40m --flash_size 16mb `
|
||
0x0 target\firmware\bootloader.bin `
|
||
0x8000 target\firmware\partition-table.bin `
|
||
0x70000 target\firmware\esp32s3-app.bin
|
||
|
||
# 逐段校验
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x0 target\firmware\bootloader.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x8000 target\firmware\partition-table.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x70000 target\firmware\esp32s3-app.bin
|
||
```
|
||
|
||
```powershell
|
||
# 对照:合并镜像(仅实验,确认失败/风险)
|
||
python -m esptool --chip esp32s3 --port COM5 write_flash 0x0 target\firmware\esp32s3-merged.bin
|
||
```
|
||
|
||
**通过标准**
|
||
|
||
- 分段写入 3 段均 `verify OK`;串口启动;
|
||
- 合并镜像在坏板上若失败,记录失败地址/校验错误;若通过,也不能据此认为安全(填充区可能未被覆盖/未被校验);
|
||
- 报告明确:坏板/迁移路径只用分段。
|
||
|
||
**注意**:E4 用当前 `0x70000` 布局做对照;模板切到 `0x100000` 后,把三条偏移替换为新布局。`write_flash` 的 `--flash_size 16mb` 语法按所用版本确认;与构建的 DIO/40 MHz 保持一致。
|
||
|
||
### E5. Config 分区 patch(1 天)
|
||
|
||
**目标**:证明不改 App、不重编即可切换 SSID;App SHA-256 不变。
|
||
|
||
**前置**:模板固件已加入 config 读取;`partitions.csv` 已有 `config` 分区;工具已能生成 256 B 记录。`provision.py` 完成前,可直接用 5.3 的 `pack_config` 片段生成 `cfg-a.bin`/`cfg-b.bin`。
|
||
|
||
```powershell
|
||
# 生成两个记录(seq 1/2),写入不同槽
|
||
python tools\provision.py make-config --ssid "LAB-A" --password-stdin --seq 1 --out cfg-a.bin
|
||
python tools\provision.py make-config --ssid "LAB-B" --password-stdin --seq 2 --out cfg-b.bin
|
||
|
||
# 写槽 0 / 槽 1(偏移按实际分区表)
|
||
python -m esptool --chip esp32s3 --port COM5 write_flash 0x13000 cfg-a.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x13000 cfg-a.bin
|
||
python -m esptool --chip esp32s3 --port COM5 write_flash 0x14000 cfg-b.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x14000 cfg-b.bin
|
||
|
||
# 读回 App,确认未变;$appSize 以 manifest 的 app.size 为准(当前 1,143,504)
|
||
$appSize = 1143504
|
||
python -m esptool --chip esp32s3 --port COM5 read_flash 0x100000 $appSize app-readback.bin
|
||
Get-FileHash app-readback.bin -Algorithm SHA256
|
||
```
|
||
|
||
**通过标准**
|
||
|
||
- 串口日志出现 `config source=partition seq=2 ssid=LAB-B`;
|
||
- `app-readback.bin` 的 SHA-256 与 manifest 中的 App SHA-256 一致;
|
||
- 设备联网,`perf_probe.py <IP> --seconds 15 --json` 有完整帧;
|
||
- 把槽 1 的 CRC 改坏/擦除,重启后回退到槽 0,日志 `seq=1`;
|
||
- 全部槽无效时回退到编译期默认,日志 `config source=default`。
|
||
|
||
### E6. 配置副本与坏扇区规避(0.5 天)
|
||
|
||
**目标**:证明 config 多副本能跨坏扇区。
|
||
|
||
**步骤**
|
||
|
||
1. 在故障板上选一个已知坏扇区(或人为把某个 config 槽写成无效 CRC);
|
||
2. 工具按策略跳过该槽,写下一个好槽;
|
||
3. 设备启动,读取有效副本;
|
||
4. 报告记录 `config_slot=N, skipped=[...]`。
|
||
|
||
**通过标准**
|
||
|
||
- 至少一个副本有效;设备联网;
|
||
- 工具不因单个槽失败而整体失败;
|
||
- 不把无效副本当成有效(CRC/seq 检查);
|
||
- 重启 3 次结果一致。
|
||
|
||
### E7. 热插拔与自动发现(0.5 天)
|
||
|
||
**目标**:验证枚举/探测/去抖/端口释放。
|
||
|
||
**步骤**
|
||
|
||
1. 插拔 20 次,记录从插入到设备出现在列表的时间;
|
||
2. 同时插两块板,按 MAC 选择,确认不会交叉写入;
|
||
3. 打开串口监视器再执行 provisioning,确认报“端口占用”而不是挂死;
|
||
4. 写入过程中拔掉 USB,确认报错、设备可重新进入下载模式、没有“成功”报告。
|
||
|
||
**通过标准**
|
||
|
||
- 20/20 次发现;P95 发现时间 ≤ 2 s(Windows 驱动枚举可能更慢,按实测设阈值);
|
||
- 0 次错板写入;
|
||
- 端口占用/拔出有明确错误码;
|
||
- 重新插入后可恢复。
|
||
|
||
### E8. OTA(条件:仅当 OTA 纳入范围,1–2 天)
|
||
|
||
**目标**:验证 OTA 成功、失败回滚、断电不砖。
|
||
|
||
**前置**:OTA-ready 分区表;Bootloader 支持 rollback;App 在启动成功后调用 `esp_ota_mark_app_valid_cancel_rollback()`。
|
||
|
||
**步骤**
|
||
|
||
1. 用 `otatool.py` 或 ESP-IDF OTA 把新 App 写入 `ota_0`,切换启动分区,重启;
|
||
2. 写入损坏镜像/错误 SHA,确认 Bootloader 拒绝,仍启动旧槽;
|
||
3. 在 OTA 写入 25%/50%/75% 时断电,恢复后确认旧/新槽至少一个可启动;
|
||
4. 把 `ota_0` 设为已知坏槽,确认 OTA 安全失败,旧槽不受影响;
|
||
5. 若启用 anti-rollback,测试降级被拒绝。
|
||
|
||
**通过标准**
|
||
|
||
- 成功 OTA 后版本/分区日志正确;
|
||
- 失败 OTA 不改变当前可启动槽;
|
||
- 断电后设备可启动,无需整片擦除;
|
||
- 坏槽 OTA 不导致 brick;
|
||
- 回滚在 1 次复位内生效。
|
||
|
||
### E9. 断电/异常拔插(0.5–1 天)
|
||
|
||
**目标**:验证写入/配置/分区表阶段的恢复能力。
|
||
|
||
**步骤**
|
||
|
||
1. App 写入 50% 时断电;重新上电,确认可进下载模式,重刷后启动;
|
||
2. config 写入时断电;确认旧副本仍有效;
|
||
3. 分区表写入时断电;确认可进下载模式,重刷分区表/App;
|
||
4. Bootloader 写入时断电;确认 BOOT strap 可恢复(最高风险,最后做);
|
||
5. 扫描/恢复时拔掉 USB;确认备份可恢复,不把半写状态当成功。
|
||
|
||
**通过标准**
|
||
|
||
- 每种场景都能通过 BOOT + 重刷恢复;
|
||
- 不出现“必须换板”的 brick;
|
||
- 报告记录最后完成阶段/失败阶段;
|
||
- 恢复后 App/Config 校验通过。
|
||
|
||
### E10. 回归基线(0.5 天)
|
||
|
||
**目标**:把上述实验固化为可重复的 pytest/脚本。
|
||
|
||
- host tests:config pack/parse、CRC、seq 选择、分区表解析、manifest 校验、坏区选择;
|
||
- hardware tests:E0/E1/E3/E4/E5/E7 的最小集;
|
||
- 报告 schema 校验;
|
||
- 同一输入重复 3 次,结果一致(允许时间差异)。
|
||
|
||
**通过标准**
|
||
|
||
- host tests 100% 通过;
|
||
- hardware 最小集 3/3 通过;
|
||
- 报告 JSON 可被 schema 校验;
|
||
- 命令/版本/哈希可追溯。
|
||
|
||
---
|
||
|
||
## 7. 测试矩阵与判定标准
|
||
|
||
> 完整机器可读版本见 [`firmware-provisioning-test-matrix.csv`](firmware-provisioning-test-matrix.csv)。下表是评审用的摘要。
|
||
|
||
### 7.1 测试矩阵(摘要)
|
||
|
||
| ID | 类别 | 前置 | 操作 | 期望/判定 | 证据 | 优先级 |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| N-01 | 正常 | 健康板,新模板 | 分段写 Bootloader/表/App,逐段 verify,启动 | 3 段 verify OK;串口 live;UDP 15 s 有完整帧 | verify 日志、serial、perf JSON | P0 |
|
||
| N-02 | 正常 | 健康板,已 provision | 只更新 App(同布局) | App hash 更新;启动;配置保留 | serial、manifest、perf | P0 |
|
||
| N-03 | 正常 | 健康板,已 provision | 只 patch config(换 SSID) | App SHA-256 不变;新 SSID 生效;不重编 | app hash、serial、perf | P0 |
|
||
| N-04 | 正常 | 健康板 | 重复 provisioning 3 次 | 幂等;无 brick;报告一致 | 3 份报告 | P1 |
|
||
| N-05 | 正常 | 健康板 | 写 merged image(对照) | 记录是否通过;不用于坏板 | verify 日志 | P1 |
|
||
| B-01 | 坏板 | 故障板,已备份 | 牺牲板 E2 写读表征;生产板定向读已知地址 + 扫描模板写入区 | E2 复现缺陷;生产板地图正确;恢复 verify | scan JSON、verify | P0 |
|
||
| B-02 | 坏板 | 故障板,新布局 | 分段写新 App/表,verify,启动 | 写入区不含坏地址;verify OK;启动 | verify、serial | P0 |
|
||
| B-03 | 坏板 | 强制 App 槽含坏字节 | 写实际镜像 | 若镜像字节不可写,verify 失败;工具换槽/拒绝;不 brick | verify、report | P0 |
|
||
| B-04 | 坏板 | config 槽含坏扇区 | 写 config 多副本 | 跳过坏槽;至少 1 个有效;启动 | scan、serial | P0 |
|
||
| B-05 | 坏板 | 故障板 | 写 merged image(对照) | 预期失败/高风险;工具默认禁止 | verify 日志 | P1 |
|
||
| B-06 | 坏板 | 间歇性坏字节 | 同一扇区扫描 3 次 | 标记 SUSPECT;隔离;不用于 App | scan JSON | P1 |
|
||
| B-07 | 坏板 | 故障板 | 全片扫描 + 恢复 | 时间/地图/恢复验证;不丢关键区 | scan JSON、backup | P2 |
|
||
| O-01 | OTA | OTA-ready | 写 `ota_0`,切换,重启 | 新版本启动;分区日志正确 | serial、otatool | P1 |
|
||
| O-02 | OTA | OTA-ready | 写损坏镜像 | Bootloader 拒绝;旧槽仍启动 | serial | P1 |
|
||
| O-03 | OTA | OTA-ready | 25/50/75% 断电 | 至少一个槽可启动;无需整片擦 | serial、relay log | P1 |
|
||
| O-04 | OTA | 坏槽 | OTA 到坏槽 | 安全失败;旧槽不受影响 | serial、scan | P1 |
|
||
| O-05 | OTA | anti-rollback | 降级 | 按策略拒绝/接受;不 brick | serial | P2 |
|
||
| H-01 | 热插拔 | 1 板 | 插拔 20 次 | 20/20 发现;P95 ≤2 s | discovery JSON | P0 |
|
||
| H-02 | 热插拔 | 2 板 | 同时插入,按 MAC 选择 | 0 次错板;端口/MAC 正确 | report | P0 |
|
||
| H-03 | 热插拔 | 写入中 | 拔 USB | 报错;可恢复;不报成功 | log、report | P0 |
|
||
| H-04 | 热插拔 | 扫描中 | 拔 USB | 报错;备份可恢复;不半写成功 | log、report | P1 |
|
||
| H-05 | 热插拔 | 串口被占用 | 执行 provisioning | 明确“端口占用”;不挂死 | log | P1 |
|
||
| H-06 | 热插拔 | 错固件/错芯片 | 选择不兼容 manifest | 写入前拒绝;设备不变 | report | P0 |
|
||
| P-01 | 断电 | App 写入 50% | 断电/上电 | 可进下载模式;重刷启动;不 brick | serial、verify | P0 |
|
||
| P-02 | 断电 | config 写入 | 断电/上电 | 旧副本有效;启动 | serial | P0 |
|
||
| P-03 | 断电 | 分区表写入 | 断电/上电 | BOOT 恢复;重刷表/App;不 brick | serial | P1 |
|
||
| P-04 | 断电 | Bootloader 写入 | 断电/上电 | BOOT 恢复;重刷;不 brick | serial | P2 |
|
||
| P-05 | 断电 | 扫描/恢复 | 拔 USB/断电 | 备份恢复;不丢关键区 | backup hash | P2 |
|
||
| S-01 | 安全 | 正常 provision | 检查日志/报告 | 无密码;只有 SSID/hash/seq | grep | P0 |
|
||
| S-02 | 安全 | Secure Boot/加密板 | 运行预检 | 检测 eFuse;拒绝未签名 patch | espefuse 输出 | P1 |
|
||
| S-03 | 安全 | 篡改 manifest | 校验 SHA-256 | 拒绝烧录 | report | P1 |
|
||
| S-04 | 安全 | 伪造 VID/PID | 主动探测 chip/MAC | 识别不匹配;拒绝 | report | P2 |
|
||
| R-01 | 回归 | host | 单元测试 | 100% 通过 | pytest/JUnit | P0 |
|
||
| R-02 | 回归 | 硬件 | 20 次循环 | 首次通过率 ≥95%;0 brick;0 错板 | 报告汇总 | P1 |
|
||
| R-03 | 回归 | 工具版本 | 固定版本/哈希 | 同输入同命令/同结果 | versions.txt | P0 |
|
||
| M-01 | 迁移 | 旧布局 `0x70000` | 全量迁移到新布局 | 保留 NVS/PHY;启动;配置可用 | serial、report | P1 |
|
||
| M-02 | 迁移 | 旧布局 | 只 patch config | 工具拒绝并提示迁移 | report | P0 |
|
||
|
||
### 7.2 全局判定标准
|
||
|
||
**一次 provisioning 判定为 PASS,必须同时满足:**
|
||
|
||
1. 设备身份匹配:chip = ESP32-S3、Flash 容量/型号匹配、MAC 与报告一致;
|
||
2. manifest 匹配:布局 hash、App SHA-256、config schema 匹配;
|
||
3. 写入的每一段 `verify_flash` 通过,且独立回读 SHA-256 与源文件一致;
|
||
4. config 至少一个副本有效,回读 CRC 通过;
|
||
5. 启动日志出现预期标记(`ESP32 camera is live`、`config source=...`),无 panic/brownout/watchdog 循环;
|
||
6. `perf_probe.py` 在 15 s 内收到完整帧,帧率 ≥ 15 fps、丢帧 ≤ 5%(按实验室网络设定;现场可放宽);
|
||
7. 报告包含工具版本、命令、时间、设备信息、坏区地图、结果;
|
||
8. 不出现密码明文。
|
||
|
||
**失败判定(任一即 FAIL)**
|
||
|
||
- 命令返回 0 但未做独立校验;
|
||
- 写入/校验失败后仍报告成功;
|
||
- 设备无法再次进入下载模式;
|
||
- 错板写入;
|
||
- 日志/报告出现密码;
|
||
- 坏区地图缺失或与实际不符。
|
||
|
||
**统计验收(P1 试点)**
|
||
|
||
- 同一批次 ≥20 台/20 次循环;
|
||
- 首次通过率 ≥95%;
|
||
- 0 brick、0 错板;
|
||
- 失败可复现且报告可定位;
|
||
- 连续 3 个 nightly 无 flaky。
|
||
|
||
### 7.3 测试环境与物料
|
||
|
||
| 物料 | 用途 | 备注 |
|
||
| --- | --- | --- |
|
||
| ESP32-S3 好板 ×2 | 正常路径/对照 | 与目标模组同型号 |
|
||
| ESP32-S3 故障板 ×2 | 坏区/迁移 | 至少 1 块有 `0xFADE7`,1 块 `0x10863` 可疑 |
|
||
| USB 带供电 Hub | 多板/掉电 | 每口可独立开关更好 |
|
||
| USB 继电器/可控电源 | 断电实验 | 2 通道即可;注意 USB-JTAG 会断开 |
|
||
| 串口适配器(可选) | 断电后仍能看日志 | 与原生 USB 分离 |
|
||
| 2.4 GHz AP | 联网验收 | 关闭 AP 隔离;准备备用热点 |
|
||
| Windows 11 测试机 | 主平台 | 关闭其他串口助手;装 CP210x/CH340 驱动 |
|
||
| Python 3.11 + pytest | P0 自动化 | 锁版本;venv/容器 |
|
||
| esptool 4.12 / espflash 4.4 | 写入/校验 | 版本写入报告 |
|
||
| 逻辑分析仪(可选) | 电源/复位时序 | 排查 brownout/复位 |
|
||
|
||
---
|
||
|
||
## 8. 风险登记与待确认项
|
||
|
||
### 8.1 风险登记
|
||
|
||
| ID | 风险 | 概率 | 影响 | 缓解 |
|
||
| --- | --- | --- | --- | --- |
|
||
| R-01 | 不允许一次性模板变更 | 中 | 高 | 先确认;若禁止,只做 Plan C App patch,并明确风险/限制 |
|
||
| R-02 | 坏区扫描漏检/误检 | 中 | 高 | 实际镜像校验 + 多图案 + 重试 + 重复扫描 + 温度/供电应力;SUSPECT 隔离 |
|
||
| R-03 | 扫描/恢复破坏数据 | 中 | 高 | 先备份并验证;分区感知;跳过已知空隙;牺牲板先行 |
|
||
| R-04 | 坏板写 merged image | 高 | 高 | 工具默认禁止;只允许分段;manifest 标记 `bad_board` |
|
||
| R-05 | App 字节 patch 破坏 checksum/hash | 高 | 高 | 不做;走 config 分区;Plan C 必须重算并 `image_info` 验证 |
|
||
| R-06 | espflash verify 假失败/真失败 | 中 | 高 | esptool 独立 verify;parity 测试;不关闭校验 |
|
||
| R-07 | OTA 未实现/回滚不可用 | 高 | 中 | 本次采用 OTA-ready 布局;Bootloader rollback + App 标记有效;或明确 N/A |
|
||
| R-08 | Secure Boot/Flash Encryption 未知 | 中 | 高 | `espefuse.py summary` 只读预检;启用则签名/加密;无密钥拒绝 |
|
||
| R-09 | 错板/多板混淆 | 中 | 高 | VID/PID + chip/MAC + 用户确认;同一串口加锁 |
|
||
| R-10 | 写入中拔出/断电 | 中 | 高 | 状态机 + 安全顺序 + 重试 + BOOT 恢复;未校验不报成功 |
|
||
| R-11 | 分区表/Bootloader 写坏 | 低 | 高 | 最后写;备份;BOOT strap 恢复;golden image |
|
||
| R-12 | 凭据泄漏 | 中 | 高 | 日志脱敏;文件 ACL;可选 HMAC/加密;报告不含密码 |
|
||
| R-13 | 工具供应链 | 低 | 高 | 锁版本/哈希;签名 manifest;SBOM;不从网络临时下载 |
|
||
| R-14 | USB 驱动/线材兼容 | 中 | 中 | CP210x/CH340/原生 USB 分别测;带供电 Hub;波特率回退 |
|
||
| R-15 | CI 硬件 flaky | 高 | 中 | 独立 runner;重试/隔离;nightly;报告留档 |
|
||
| R-16 | App 分区余量不足 | 高 | 中 | 扩到 `0x140000`;构建门禁 ≤95%;监控 app size |
|
||
| R-17 | Config schema/固件版本错配 | 中 | 中 | version + 回退;OTA 回滚后测试旧固件读新 config |
|
||
| R-18 | GPL/Python 打包 | 中 | 中 | P1 优先 espflash;esptool 仅独立进程/合规评估 |
|
||
| R-19 | 旧布局迁移失败 | 中 | 中 | 先 App 后表;保留 NVS/PHY;备份;试点 5 台 |
|
||
| R-20 | 扫描时间过长 | 中 | 低 | 分层扫描;只扫写入区;生产抽检 |
|
||
|
||
### 8.2 待确认项(按重要性)
|
||
|
||
| ID | 问题 | 为什么重要 | 默认假设(若未确认) |
|
||
| --- | --- | --- | --- |
|
||
| Q-01 | 是否允许一次性模板固件变更(config 读取;可选 OTA)? | 决定方案是否成立 | 允许,一次构建,所有设备共用 |
|
||
| Q-02 | OTA 是否在本次范围? | 决定分区布局与测试矩阵 | 在范围内;采用 OTA-ready 布局 |
|
||
| Q-03 | App 槽位是否可从 `0x120000` 扩到 `0x140000`? | 当前只剩 36 KB,新增代码可能溢出 | 扩到 `0x140000` |
|
||
| Q-04 | 是否启用/计划 Secure Boot、Flash Encryption、NVS 加密? | 决定 config 能否明文、App 能否重签 | 当前未启用;工具做只读预检 |
|
||
| Q-05 | 生产环境是否 Windows 单平台?桌面端是否必须内置烧录器? | 决定 Tauri/sidecar/Python 选型 | Windows 主平台;桌面端内置 P1 |
|
||
| Q-06 | Python 运行时/GPL 是否可接受? | 决定 P1 用 esptool 还是 espflash | P0 可接受;P1 优先 espflash |
|
||
| Q-07 | 每板可接受的 provisioning 时间? | 决定扫描层级/全片扫描 | Tier1 ≤10 min;全片仅疑似板 |
|
||
| Q-08 | 配置项只有 SSID/密码,还是还有 IP/摄像头/队伍绑定? | 决定 config 大小/格式 | MVP 只 SSID/密码;schema 可扩展 |
|
||
| Q-09 | 配置文件保密等级?是否允许明文 Wi-Fi 密码落盘? | 决定加密/HMAC/访问控制 | MVP 明文 + 脱敏 + ACL;后续加密 |
|
||
| Q-10 | 现网旧布局(`0x70000`)板卡如何迁移? | 决定迁移流程/停机 | 允许一次性全量迁移;保留 NVS/PHY |
|
||
| Q-11 | 目标产量/坏板比例? | 决定 ROI 与抽检策略 | 50–200 台/年;坏板少量 |
|
||
| Q-12 | 是否有坏板样品、USB 继电器、独立测试机? | 决定 MVE 能否执行 | 有 2 好 2 坏;继电器可采购 |
|
||
| Q-13 | “自动扫描识别”是否包括网络发现? | 决定是否要 UDP 发现 UI | 仅 USB 插入 + 主动探测 |
|
||
| Q-14 | 失败策略:坏板报废、返修还是降级? | 决定 SUSPECT/坏槽策略 | 坏槽拒绝;SUSPECT 隔离;不降级启动 |
|
||
| Q-15 | ESP32-C3 装甲板是否也要纳入同一工具? | 决定 chip/profile 抽象 | 暂不;后续按 board profile 扩展 |
|
||
| Q-16 | Flash 型号/批次是否固定? | 坏区行为/容量/ID 可能变化 | 固定 N16R8;`flash_id` 门禁 |
|
||
| Q-17 | OTA 是否要求签名/anti-rollback? | 决定安全设计 | 若 OTA,启用签名与 rollback |
|
||
| Q-18 | 报告/日志保留多久?是否含敏感信息? | 决定存储/脱敏 | 90 天;不含密码 |
|
||
|
||
### 8.3 决策记录(建议)
|
||
|
||
| ID | 决策 | 状态 |
|
||
| --- | --- | --- |
|
||
| D-01 | 用独立 config 分区,不 patch App 字节 | 建议,待 Q-01 |
|
||
| D-02 | 坏板只用分段写入,默认禁止 merged | 建议 |
|
||
| D-03 | esptool 做独立校验的权威;espflash 做写入,parity 后放行 | 建议 |
|
||
| D-04 | 本次直接采用 OTA-ready 布局 | 建议,待 Q-02 |
|
||
| D-05 | P0 用 Python CLI,P1 再进 Tauri | 建议 |
|
||
| D-06 | App 槽位扩到 `0x140000` | 建议,待 Q-03 |
|
||
| D-07 | 统一好板/坏板布局,废弃 per-board 偏移搜索 | 建议 |
|
||
|
||
---
|
||
|
||
## 9. 成本、ROI 与安全
|
||
|
||
### 9.1 成本估算
|
||
|
||
| 阶段 | 项目 | 人日 | 说明 |
|
||
| --- | --- | ---: | --- |
|
||
| P0 | CLI + config 格式 + 单测 | 5–8 | 发现/扫描/写入/patch/报告 |
|
||
| P0 | 模板固件(config + 日志 + 回退) | 3–5 | 含 host tests;不含 OTA |
|
||
| P0 | 坏区扫描硬化 | 3–5 | 分区感知、重试、恢复、报告 |
|
||
| **P0 小计** | | **11–18** | 约 2.5–4 周 |
|
||
| P1 | Tauri(发现/UI/侧车/报告) | 8–12 | 含权限/打包/签名 |
|
||
| P1 | 测试矩阵 + 硬件在环 | 5–8 | pytest + runner + 报告 |
|
||
| P1 | 文档/培训 | 2–3 | 操作手册/故障处理 |
|
||
| **P1 小计** | | **15–23** | 约 3–5 周 |
|
||
| 可选 | OTA | 5–10 | 布局/rollback/签名/测试 |
|
||
| **合计** | **P0+P1** | **26–41** | 约 5–8 周 |
|
||
| **合计** | **P0+P1+OTA** | **31–51** | 约 6–10 周 |
|
||
|
||
**硬件**:好板/坏板(若已有则 0)、USB 继电器 ¥200–500、带供电 Hub ¥150–350、串口适配器 ¥50–150、备用 Flash 模组 ¥100–300;合计约 ¥500–1,300(不含板卡)。
|
||
|
||
**软件**:esptool/espflash/ESP-IDF 均为开源;GPL 合规评估 1–2 人日;Windows 代码签名证书另计。
|
||
|
||
### 9.2 ROI 敏感性(说明性,不是承诺)
|
||
|
||
假设:
|
||
|
||
- 当前每台改 SSID 需要“改 `.env` → 重编 → 烧录 → 校验 → 串口检查”;
|
||
- 新流程每台只写 config + 校验;
|
||
- 节省时间 `t` 分钟/台;年产量 `N`;工程师成本 `c` 元/小时;坏板返修/报废成本 `b` 元/台,避免率 `r`。
|
||
|
||
年收益 ≈ `N × (t/60) × c + N × r × b`。
|
||
|
||
| 场景 | N | t | c | 工时收益 | 避免返修(r=1%,b=¥200) | 合计/年 |
|
||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||
| 小批量 | 50 | 10 min | ¥300/h | ¥2,500 | ¥100 | ¥2,600 |
|
||
| 中批量 | 200 | 10 min | ¥300/h | ¥10,000 | ¥400 | ¥10,400 |
|
||
| 较大批量 | 500 | 15 min | ¥300/h | ¥37,500 | ¥1,000 | ¥38,500 |
|
||
|
||
**解读**:小批量下,ROI 不是主要理由;主要价值是降低 brick 风险、让配置可追溯、让回归自动化。中大批量下,工时收益才明显。若只做 P0(约 2.5–4 周),回收期更短;P1 的 UI 投入应按“操作员不是工程师”这一场景来论证。
|
||
|
||
### 9.3 安全评估
|
||
|
||
| 威胁 | 影响 | 当前状态 | 建议 |
|
||
| --- | --- | --- | --- |
|
||
| Wi-Fi 凭据在固件/配置中 | 高 | 编译进 App;随包分发 | 移到 config 分区;日志/报告脱敏;可选加密 |
|
||
| 物理读取 Flash | 高 | 仓库配置未启用;以 eFuse 读数为准 | 若威胁模型需要,启用 Flash Encryption;否则接受物理访问风险 |
|
||
| 篡改 App/OTA | 高 | 仓库配置未启用;以 eFuse 读数为准 | 若 OTA/现场更新,启用签名与 rollback/anti-rollback |
|
||
| 伪造设备 | 中 | 仅 VID/PID 可能被伪造 | 主动读 chip/MAC/Flash ID;与 manifest/工单匹配 |
|
||
| 工具供应链 | 高 | 依赖本地二进制/Python 包 | 锁版本/哈希;签名 manifest;SBOM;不从网络临时下载 |
|
||
| 日志泄漏 | 中 | 串口可能打印失败 SSID | 统一日志脱敏;报告只存 SSID/hash/seq |
|
||
| GPL 合规 | 中 | 若打包 esptool | P1 优先 espflash;若用 esptool,提供许可证/源码获取方式 |
|
||
|
||
**Secure Boot/Flash Encryption 预检命令(只读)**
|
||
|
||
```powershell
|
||
python -m espefuse --chip esp32s3 --port COM5 summary
|
||
python -m espsecure --help # 确认签名/加密工具可用
|
||
```
|
||
|
||
- 若 `Secure Boot` 已启用:工具必须拒绝未签名 App;config 分区若被加密,写入需走加密路径;
|
||
- 若 `Flash Encryption` 已启用:不能直接写明文 config;需要 `espsecure.py encrypt_flash_data` 或通过设备端 provisioning;
|
||
- 当前仓库没有相关配置;不要假设“肯定没开”,以 eFuse 读数为准。
|
||
|
||
---
|
||
|
||
## 10. 结论与下一步
|
||
|
||
### 10.1 推荐实施顺序
|
||
|
||
**第 1 周:确认与基线**
|
||
|
||
- 确认 Q-01/Q-02/Q-03/Q-04;
|
||
- 跑 E0/E1,锁定工具版本、备份、设备身份;
|
||
- 固化“分段写入 + 逐段 verify”的命令模板。
|
||
|
||
**第 2 周:证明坏区策略**
|
||
|
||
- 跑 E2/E3/E4;
|
||
- 产出“坏字节地图 + 实际镜像校验”报告;
|
||
- 决定 App 槽位大小/布局。
|
||
|
||
**第 3 周:模板固件 + config patch**
|
||
|
||
- 加 config 分区/解析/回退/日志;
|
||
- 跑 E5/E6;
|
||
- 证明 App SHA-256 不变、SSID 可切换、坏副本可回退。
|
||
|
||
**第 4 周:自动化与试点**
|
||
|
||
- E7/E9/E10;
|
||
- 5–10 台试点;
|
||
- 评审是否进入 P1/Tauri 与 OTA。
|
||
|
||
### 10.2 需要立即做的三件事
|
||
|
||
1. **不要在坏板上继续使用 merged image / 单次整包写入**;先改为分段写入 + 逐段 verify。
|
||
2. **不要直接改 App 字节做 SSID patch**;先设计 config 分区(本报告 5.3 的格式可直接用)。
|
||
3. **把 `flash.sh` 从产品流程移除**,保留为故障板应急;用 manifest + 分段写入 + 报告替代。
|
||
|
||
### 10.3 结论
|
||
|
||
- **工具选型**:P0 用 Python + esptool;P1 用 Tauri + serialport + espflash 侧车 + esptool 独立校验;OTA 用 `otatool.py`/ESP-IDF OTA。
|
||
- **坏区策略**:没有“坏块感知烧录器”;正确做法是“分区感知扫描 + 实际镜像写读校验 + 槽位/副本规避”。App 不能扇区级跳过;config 可以多副本屏蔽。
|
||
- **SSID patch**:独立 config 分区 + 256 B 记录 + CRC + 4 副本 + 回退;App 不重编、不改字节。
|
||
- **验证路径**:E0–E10 的 MVE 先证明“坏字节可复现、实际镜像校验正确、分段写入可用、config patch 不改 App、断电可恢复”,再投入产品化。
|
||
- **最大风险**:不是工具能力,而是“是否允许一次性模板变更”和“OTA/安全范围未定”。这两项不确认,后续设计会反复。
|
||
|
||
---
|
||
|
||
## 附录 A:命令速查
|
||
|
||
```powershell
|
||
# 版本/设备
|
||
python -m esptool version
|
||
python -m esptool --chip esp32s3 --port COM5 flash_id
|
||
python -m esptool --chip esp32s3 --port COM5 read_mac
|
||
python -m esptool --chip esp32s3 --port COM5 chip_id
|
||
|
||
# 备份/读
|
||
python -m esptool --chip esp32s3 --port COM5 --baud 460800 read_flash 0 0x1000000 backup.bin
|
||
python -m esptool --chip esp32s3 --port COM5 read_flash 0x8000 0xC00 partition-table.bin
|
||
|
||
# 擦除(谨慎)
|
||
python -m esptool --chip esp32s3 --port COM5 erase_region 0x13000 0x1000
|
||
|
||
# 分段写 + 逐段校验
|
||
python -m esptool --chip esp32s3 --port COM5 --baud 460800 write_flash `
|
||
--flash_mode dio --flash_freq 40m --flash_size 16mb `
|
||
0x0 bootloader.bin 0x8000 partition-table.bin 0x100000 app.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x0 bootloader.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x8000 partition-table.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x100000 app.bin
|
||
|
||
# Config patch
|
||
python -m esptool --chip esp32s3 --port COM5 write_flash 0x13000 cfg.bin
|
||
python -m esptool --chip esp32s3 --port COM5 verify_flash 0x13000 cfg.bin
|
||
|
||
# 镜像信息
|
||
python -m esptool --chip esp32s3 image_info app.bin
|
||
|
||
# 安全预检
|
||
python -m espefuse --chip esp32s3 --port COM5 summary
|
||
|
||
# 启动后验收
|
||
python scripts\perf_probe.py 192.168.1.139 --seconds 15 --json perf.json
|
||
python scripts\serial_soak.py --port COM5 --seconds 20 --out reports\serial
|
||
```
|
||
|
||
> 命令名/参数按锁定的 esptool 版本为准;v4 与 v5 的下划线/连字符不同。不要把 `--no-verify` 用在最终烧录上,只可用于“写图案然后自己回读”的扫描实验。
|
||
|
||
## 附录 B:Manifest 示例
|
||
|
||
```json
|
||
{
|
||
"schema": 1,
|
||
"version": "0.2.0",
|
||
"chip": "esp32s3",
|
||
"flash_size": "16MB",
|
||
"flash_mode": "dio",
|
||
"flash_freq": "40m",
|
||
"partition_table": {
|
||
"offset": "0x8000",
|
||
"file": "partition-table.bin",
|
||
"sha256": "<sha256>"
|
||
},
|
||
"bootloader": {
|
||
"offset": "0x0",
|
||
"file": "bootloader.bin",
|
||
"sha256": "<sha256>"
|
||
},
|
||
"app": {
|
||
"offset": "0x100000",
|
||
"file": "esp32s3-app.bin",
|
||
"sha256": "<sha256>",
|
||
"size": 1143504
|
||
},
|
||
"config": {
|
||
"partition": "config",
|
||
"offset": "0x13000",
|
||
"size": "0x10000",
|
||
"schema": 1,
|
||
"record_size": 256,
|
||
"slots": 4
|
||
},
|
||
"ota": {
|
||
"enabled": true,
|
||
"factory": "0x100000",
|
||
"ota_0": "0x240000",
|
||
"ota_1": "0x380000",
|
||
"slot_size": "0x140000"
|
||
},
|
||
"bad_addresses": ["0xFADE7", "0x10863"],
|
||
"build": {
|
||
"source_revision": "<git-sha>",
|
||
"built_at": "2026-10-19T00:00:00+08:00",
|
||
"toolchain": "esp-1.97.0.0",
|
||
"esp_idf": "v5.5.5",
|
||
"espflash": "4.4.0",
|
||
"esptool": "4.12.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
## 附录 C:证据索引
|
||
|
||
| 证据 | 位置 | 用途 |
|
||
| --- | --- | --- |
|
||
| 坏字节/偏移策略 | [`HARDWARE.md`](../HARDWARE.md#损坏-flash-模组的兼容处理)、[`scripts/flash.sh`](../scripts/flash.sh) | 现状与风险 |
|
||
| 当前分区表 | [`partitions.csv`](../partitions.csv) | 偏移/大小/无 OTA |
|
||
| App 大小/偏移/坏字节 | [`target/firmware/BUILD-INFO.json`](../target/firmware/BUILD-INFO.json) | 96.94% 占用、`0x70000`、坏字节 |
|
||
| 镜像 checksum/hash | [`target/firmware/IMAGE-INFO.txt`](../target/firmware/IMAGE-INFO.txt) | App 字节 patch 风险 |
|
||
| 三段 verify | [`target/latency/tx15-verify.log`](../target/latency/tx15-verify.log) | esptool 逐段校验证据 |
|
||
| espflash 失败 | [`target/fw-budget40-flash.log`](../target/fw-budget40-flash.log)、[`target/soak/vga30-fb3-flash.log`](../target/soak/vga30-fb3-flash.log) | 不能只信写入成功 |
|
||
| SSID 编译期 | [`src/main.rs`](../src/main.rs) 36–37、582–599 | 需要 config 分区 |
|
||
| 构建脚本 | [`scripts/build.ps1`](../scripts/build.ps1) | `.env`/重编 |
|
||
| 桌面端无串口 | [`desktop/src-tauri/Cargo.toml`](../desktop/src-tauri/Cargo.toml) | 需新增发现/烧录 |
|
||
| CI 仅桌面 | [`.github/workflows/build-client.yml`](../.github/workflows/build-client.yml) | 需新增 host/hardware job |
|
||
| 串口/UDP 验证 | [`scripts/serial_soak.py`](../scripts/serial_soak.py)、[`scripts/perf_probe.py`](../scripts/perf_probe.py) | 复用为验收 |
|
||
|
||
---
|
||
|
||
**评估员**:工具评估师(DSH)
|
||
**评估日期**:2026-10-19
|
||
**置信度**:中高(静态证据充分;硬件实验待执行)
|
||
**下次评审**:MVE E2–E5 完成后;或模板/分区/安全配置变更时
|