# 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(" 通用图案扫描**。最终判据是“这个 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` 是占位,实际参数以工具实现为准; - 报告目录按 `-/` 组织,包含 `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 --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": "" }, "bootloader": { "offset": "0x0", "file": "bootloader.bin", "sha256": "" }, "app": { "offset": "0x100000", "file": "esp32s3-app.bin", "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": "", "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 完成后;或模板/分区/安全配置变更时