Files
simba-rmic-2026/docs/firmware-provisioning-evaluation.md
T
sloray e3acb37d32
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
docs: 按 humanizer 重写项目文档并同步 RMIC::2026 现状
2026-10-19 10:47:00 +08:00

81 KiB
Raw Blame History

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。文中的 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),直接改字节会破坏它们;若启用 Secure Boot,还会破坏签名。正确位置是独立 config 分区。
  • 当前 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/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 硬件表、sdkconfig.defaults 目标固定,工具可按 chip/flash 做兼容性门禁
Flash 模式 16 MB、DIO、40 MHz sdkconfig.defaults CONFIG_ESPTOOLPY_* 烧录参数必须与构建一致;QIO/80 MHz 在本板会 boot loop
Bootloader 0x0,当前 0x5160 字节 DEVELOPMENT.md 镜像导出、target/latency/tx15-verify.log Bootloader 未变时不要重写;写入是单点故障
分区表 0x8000,当前 0xC00 字节 sdkconfig.defaults CONFIG_PARTITION_TABLE_OFFSET=0x8000、verify 日志 分区表与 Bootloader/App 必须成套
NVS 0x9000,0x6000 partitions.csv 目前主要由 Wi-Fi 驱动使用,不应塞入业务配置
PHY init 0xF000,0x1000 同上 保存射频校准,不要整片擦除
Factory App 0x70000,0x120000(1,179,648 B) 同上、BUILD-INFO.json App 实际 1,143,504 B,占用 96.94%,余量仅 36,144 B
OTA 无 otadata / ota_0 / ota_1 partitions.csv、grep OTA 无结果 OTA 测试当前无法执行;需一次性布局变更
坏字节 1 0xFADE7,bit 5–7 写入异常 HARDWARE.md 当前 App 镜像在该地址的字节为 0x00,恰好可写
坏字节 2 0x10863,bit 6 读回为 1(期望 0x2e,读回 0x6e) 同上、BUILD-INFO.json 尚未确认永久故障;当前靠 App 偏移 ≥ 0x20000 避开
App 镜像完整性 Checksum: 9e (valid)、Validation Hash: ... (valid) target/firmware/IMAGE-INFO.txt 直接改 App 字节会破坏两者

当前坏板规避逻辑(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 用于 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 模板固件构建固定;不随 provisioning 变化
export-firmware.ps1 硬编码 C:\s3vt\... 导出 App/Bootloader/分区表/merged 需改为可移植、带 manifest 的导出
verify-firmware.py 仓库 target/ 解析分区表、检查 merged、检查 0xFADE7 可扩展为“布局不变量 + manifest 校验”
CI .github/workflows/build-client.yml 只构建桌面端、跑 protocol/core 测试 没有固件构建、没有硬件在环

2.3 客户端、配置与测试

项目 现状 证据 差距
SSID/密码 编译期 env!("WIFI_SSID") / env!("WIFI_PASS") src/main.rs 36–37、582–599 每换 SSID 必须重编;无法按台 patch
运行时配置 无;NVS 只传给 EspWifi src/main.rs 168–180 需新增 config 分区读取 + 回退
桌面端串口 无 serialport 依赖 desktop/src-tauri/Cargo.toml 需新增设备发现/烧录后端
桌面端打包 Tauri v2,bundle targets app/dmg,CI 覆盖 desktop/src-tauri/tauri.conf.json 需加 sidecar/资源与 Windows NSIS
串口测试 serial_soak.py、capture-serial.py scripts/serial_soak.py、target/capture-serial.py 可复用为 provisioning 后验证
UDP 验证 udp_probe.py、perf_probe.py scripts/udp_probe.py、scripts/perf_probe.py 可复用为启动后验收
主机测试文化 protocol/core/stream-control 有 host tests DEVELOPMENT.md 新 config 格式应放共享 crate,做 host tests
产物完整性 SHA256SUMS.txt、BUILD-INFO.json、verify 日志 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;
  • 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/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 总体架构

┌──────────────────────────────────────────────────────────────────┐
│ 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 工具)

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 备份目标区间;能恢复才做。

扫描算法(分区感知)

# 图案扫描用于非 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 状态机

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(推荐先做)

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/分区表/镜像信息。

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 天)

目标:证明读通道稳定;为后续破坏性实验留后路。

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 备份。

# 读周边扇区,先看原始值
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 天)

目标:证明坏板上分段写入可用、合并镜像高风险/失败。

# 分段写入(推荐)
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
# 对照:合并镜像(仅实验,确认失败/风险)
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。

# 生成两个记录(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。下表是评审用的摘要。

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 预检命令(只读)

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:命令速查

# 版本/设备
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 示例

{
  "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、scripts/flash.sh 现状与风险
当前分区表 partitions.csv 偏移/大小/无 OTA
App 大小/偏移/坏字节 target/firmware/BUILD-INFO.json 96.94% 占用、0x70000、坏字节
镜像 checksum/hash target/firmware/IMAGE-INFO.txt App 字节 patch 风险
三段 verify target/latency/tx15-verify.log esptool 逐段校验证据
espflash 失败 target/fw-budget40-flash.log、target/soak/vga30-fb3-flash.log 不能只信写入成功
SSID 编译期 src/main.rs 36–37、582–599 需要 config 分区
构建脚本 scripts/build.ps1 .env/重编
桌面端无串口 desktop/src-tauri/Cargo.toml 需新增发现/烧录
CI 仅桌面 .github/workflows/build-client.yml 需新增 host/hardware job
串口/UDP 验证 scripts/serial_soak.py、scripts/perf_probe.py 复用为验收

评估员:工具评估师(DSH) 评估日期:2026-10-19 置信度:中高(静态证据充分;硬件实验待执行) 下次评审:MVE E2–E5 完成后;或模板/分区/安全配置变更时