81 KiB
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。
这不是“选一个万能工具”,而是三层组合:
- 参考/校验层:
esptool(当前 4.12.0)负责原始读、擦、写、逐段verify_flash、镜像信息检查; - 产品写入层:
espflash4.4.0(Apache-2.0、单文件 Rust)负责发现端口与快速写入;其校验结果必须与 esptool 做 parity(一致性)测试后才能单独作为门禁; - 业务编排层: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 件事
- 是否允许一次性修改模板固件(新增 config 分区读取;可选 OTA)?
- OTA 是否在本次范围内?当前分区表没有任何 OTA 分区,但测试矩阵已包含 OTA,建议本次直接采用 OTA-ready 布局,避免二次迁移。
- App 分区是否可从
0x120000扩到0x140000?当前 App 已占 96.94%(1,143,504 / 1,179,648),只剩 36,144 字节;加入 config 加载/OTA 代码后很可能不够。 - 是否启用/计划启用 Secure Boot、Flash Encryption、NVS 加密?这决定 config 分区能否明文写入、App 能否重签。
1. 结论先行:七个必须接受的技术事实
- 没有“坏块感知烧录器”。esptool/espflash 不会读取坏块表,也不会跳过坏扇区;它们只会在写入后做 digest 校验,失败就报错。仓库里已有的
espflash::verify_failed日志(target/fw-budget40-flash.log、target/soak/vga30-fb3-flash.log)说明“写入命令返回”不等于“内容正确”。 - SPI NOR 的故障粒度是位/字节,不是 NAND 的块。把
0xFADE7叫“坏块”只是工程口语;工具应按 4 KiB 扇区统计,但根因要记录到字节/位。 - Bootable App 只能整槽位规避。如果坏字节落在 App 镜像的某个字节上,不能“跳过 4 KiB 继续写”;只能换分区偏移、换 OTA 槽位,或证明当前镜像恰好能写入该字节。
- 实际镜像写读校验优先于通用图案扫描。通用
0x00/0x55/0xAA图案能发现“对任意数据都坏”的扇区,但会误报“仅对某些字节坏、当前镜像恰好可写”的槽位。App 槽位的最终判据应是“写入实际 App 镜像 → 读回 → 全字节比较通过”。 - 合并镜像不适合坏板。它把 Bootloader、分区表、App 和填充区从
0x0连续写入;坏字节在填充区也会被写入/校验,导致整包失败。分段写入能把故障局部化,也便于独立校验。 - App 字节 patch 不是配置方案。即使只改 SSID 字符串,也会改变 App 的
Checksum和Validation Hash;需要自定义工具重算,且 Secure Boot 下还要重签。config 分区 patch 不触碰 App,App SHA-256 保持不变,天然满足“模板固件 + patch”。 - 当前
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):
espflash save-image导出 App;- 从
0x20000开始按 64 KiB 步进,找最低的偏移,使 App 中落到0xFADE7的那个字节满足byte & 0xE0 == 0; - 把偏移写回
partitions.csv; - 删除
esp-idf-sys构建目录并重编译; - 再次导出 App,确认字节仍可写;
- 用
esptool write-flash写0x0Bootloader、0x8000分区表、offsetApp; - 串口观察。
这条链路的问题:
- 它只检查
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 等依赖各自许可证(需核对) |
注:
espflash4.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; - 不会在写入失败时自动换槽位/重试到好扇区;
- 默认校验只覆盖“本次写入的区间”,不覆盖全片;不要用“命令成功”推断整片健康。
关键限制
- 合并镜像的填充区:
merge_bin生成的镜像从0x0连续排布,分区间会填充;write_flash 0x0 merged.bin会把这些填充也写进去。坏板上的坏字节若落在填充区,校验会失败;即使不失败,也浪费写入并扩大故障面。 - 小文件写入会擦整个扇区:写 256 字节 config 也会擦除其所在的 4 KiB 扇区;因此 config 必须“一条记录占一个扇区”,不能把多条记录塞在同一扇区。
- 版本/命令名差异:v4 使用
write_flash等下划线形式;v5 改为write-flash等连字符形式,并可能改变输出。锁定 4.12.0 或 5.6.0 其中之一,不要在同一流水线混用。 - 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.pyCLI 完成发现/扫描/烧录/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 坏区扫描与“自动屏蔽”
核心原则
- App 槽位:实际镜像写读校验 > 通用图案扫描。最终判据是“这个 App 镜像写进去、读回来、逐字节一致”;通用图案只用于评估余量和发现硬故障。
- Config 槽位:实际记录写读校验 + 多副本。每个槽写真实 256 B 记录,回读/CRC 通过即可用;失败换下一个槽。
- 不跳过 App 镜像内部的扇区。没有文件系统/FTL 能改变 Bootloader 的线性读取;不要尝试“把坏扇区绕过去”。
- 扫描目标按分区裁剪。常规 provisioning 只对 Bootloader/分区表/otadata/config/App 槽位做写入校验;NVS/PHY 含 Wi-Fi/射频校准,默认只读或跳过,除非有已验证备份且确有必要。跳过
0xFADE7、0x10863所在的空隙,避免无意义的写入和“恢复失败”。 - 先备份,后破坏性测试。任何写图案/擦除前,先
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,十几分钟):全片扫描。
- Tier 0(每台,<10 s):
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/日志/地图/退出码
安全写入顺序(迁移/首次烧录)
- 备份(至少 NVS/PHY/旧分区表/旧 App 的关键区);
- 写新 App 到新槽位(旧分区表尚未引用它,旧 App 仍可启动);
- 写 config 副本(旧 App 不读它,无影响);
- 写新分区表(切换引用;这是单点故障,尽量最后写);
- 如需更新 Bootloader,最后写;
- 复位,串口/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 分层
- PR 门禁(无硬件):Python lint/单元测试、config pack/parse、partition table 解析、manifest schema、坏区选择逻辑、命令构造 golden tests;
- Nightly(硬件在环):1 好板 + 1 坏板;跑 N/B/H/P 子集;产出 JSON/JUnit;失败保留日志;
- 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
通过标准
esptool4.12.0、espflash4.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 槽位的正确判据。
步骤
- 对当前
factory分区(0x70000,0x120000)做通用图案扫描,记录0xFADE7是否被标记 BAD; - 用
esptool write_flash 0x70000 target\firmware\esp32s3-app.bin写实际镜像,verify_flash; - 对比:
- 通用扫描:
0xFADE7失败(预期); - 实际镜像:
verify OK(当前镜像字节0x00,预期);
- 通用扫描:
- 若可能,构造一个“镜像字节在
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 多副本能跨坏扇区。
步骤
- 在故障板上选一个已知坏扇区(或人为把某个 config 槽写成无效 CRC);
- 工具按策略跳过该槽,写下一个好槽;
- 设备启动,读取有效副本;
- 报告记录
config_slot=N, skipped=[...]。
通过标准
- 至少一个副本有效;设备联网;
- 工具不因单个槽失败而整体失败;
- 不把无效副本当成有效(CRC/seq 检查);
- 重启 3 次结果一致。
E7. 热插拔与自动发现(0.5 天)
目标:验证枚举/探测/去抖/端口释放。
步骤
- 插拔 20 次,记录从插入到设备出现在列表的时间;
- 同时插两块板,按 MAC 选择,确认不会交叉写入;
- 打开串口监视器再执行 provisioning,确认报“端口占用”而不是挂死;
- 写入过程中拔掉 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()。
步骤
- 用
otatool.py或 ESP-IDF OTA 把新 App 写入ota_0,切换启动分区,重启; - 写入损坏镜像/错误 SHA,确认 Bootloader 拒绝,仍启动旧槽;
- 在 OTA 写入 25%/50%/75% 时断电,恢复后确认旧/新槽至少一个可启动;
- 把
ota_0设为已知坏槽,确认 OTA 安全失败,旧槽不受影响; - 若启用 anti-rollback,测试降级被拒绝。
通过标准
- 成功 OTA 后版本/分区日志正确;
- 失败 OTA 不改变当前可启动槽;
- 断电后设备可启动,无需整片擦除;
- 坏槽 OTA 不导致 brick;
- 回滚在 1 次复位内生效。
E9. 断电/异常拔插(0.5–1 天)
目标:验证写入/配置/分区表阶段的恢复能力。
步骤
- App 写入 50% 时断电;重新上电,确认可进下载模式,重刷后启动;
- config 写入时断电;确认旧副本仍有效;
- 分区表写入时断电;确认可进下载模式,重刷分区表/App;
- Bootloader 写入时断电;确认 BOOT strap 可恢复(最高风险,最后做);
- 扫描/恢复时拔掉 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,必须同时满足:
- 设备身份匹配:chip = ESP32-S3、Flash 容量/型号匹配、MAC 与报告一致;
- manifest 匹配:布局 hash、App SHA-256、config schema 匹配;
- 写入的每一段
verify_flash通过,且独立回读 SHA-256 与源文件一致; - config 至少一个副本有效,回读 CRC 通过;
- 启动日志出现预期标记(
ESP32 camera is live、config source=...),无 panic/brownout/watchdog 循环; perf_probe.py在 15 s 内收到完整帧,帧率 ≥ 15 fps、丢帧 ≤ 5%(按实验室网络设定;现场可放宽);- 报告包含工具版本、命令、时间、设备信息、坏区地图、结果;
- 不出现密码明文。
失败判定(任一即 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 需要立即做的三件事
- 不要在坏板上继续使用 merged image / 单次整包写入;先改为分段写入 + 逐段 verify。
- 不要直接改 App 字节做 SSID patch;先设计 config 分区(本报告 5.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 完成后;或模板/分区/安全配置变更时