docs: 按 humanizer 重写项目文档并同步 RMIC::2026 现状
This commit is contained in:
+21
-20
@@ -1,6 +1,7 @@
|
||||
# 烧录 / 维护客户端架构设计(ESP32-S3 图传板)
|
||||
|
||||
> 状态:**提议中(Proposed)**,等待第 9 节的问题清单答复后冻结。
|
||||
> 落地情况:实际实现走的是 desktop 维护页 + `.zip` bundle,见 [docs/firmware-maintenance.md](docs/firmware-maintenance.md)。
|
||||
> 输入约束:现场存在 SPI Flash 坏块;需要兼顾 OTA 与现场排障;**不改固件源码、不重新编译**;
|
||||
> 优先“模板固件 + 动态 patch”。
|
||||
> 本文所有结论都在仓库里逐条对过代码/脚本/分区表/文档,引用位置见各节链接。
|
||||
@@ -12,22 +13,22 @@
|
||||
1. **“不改固件源码”可以做到;但“真 OTA”做不到。** 当前固件没有 `ota_0/ota_1/otadata` 分区,
|
||||
没有 `esp_ota` 调用,UDP 协议里也没有任何命令通道(只有 Hello/Subscribe/视频/血量)。
|
||||
设备自己没有能力接收固件。所谓“兼顾 OTA”,在现约束下只能是**现场 USB 串口更新**,
|
||||
或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。**这是必须先决的问题**。
|
||||
2. **坏块规避其实根本不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回
|
||||
或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。这是必须先决的问题。
|
||||
2. **坏块规避其实不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回
|
||||
`partitions.csv`(构建输入)再触发重建;而 **App 镜像不包含自己的分区偏移**,偏移只存在于
|
||||
`0x8000` 的 `partition-table.bin`(0xC00 字节 + 末尾 16 字节 MD5)。
|
||||
→ 对同一份已构建镜像,**搜索偏移 + 重写分区表 + 原样写 App**,全程零编译。
|
||||
这是本方案最大的收益点,也是“模板 + patch”的第一块拼图。
|
||||
3. **SSID/密码不是 NVS,是 `.rodata` 里的字符串字面量**(`env!` 编译期注入)。
|
||||
二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch **App 镜像**,
|
||||
且必须连带处理**字符串长度常量**与**镜像 checksum/SHA256**。
|
||||
二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch App 镜像,
|
||||
且必须连带处理字符串长度常量与镜像 checksum/SHA256。
|
||||
长度常量能否可靠定位是这个方案真正的技术风险(见 7.2)。
|
||||
4. **坏块信息不能“读”出来,只能“写进去再读回来”差分出来。** `flash_id` 只给芯片型号与容量,
|
||||
SFDP 只给参数,启动日志只给症状。仓库里 `0x10863` 的 bit6 挂在 1,就是整段回读比对发现的
|
||||
(HARDWARE.md)。因此客户端必须自带「写入图案 → 回读 → 差分」与「写后整段校验差分」两条链路。
|
||||
5. **推荐形态:纯逻辑核心库 `flash-core` + 优先 CLI + 桌面端加一页**。
|
||||
把最危险的逻辑(镜像格式、分区表、偏移搜索、补丁)做成不碰 I/O 的纯函数,
|
||||
在主机上用**真实 1.1 MB 镜像做往返测试**——这与仓库已有的 `protocol/`、`stream_control`、
|
||||
在主机上用真实 1.1 MB 镜像做往返测试——这与仓库已有的 `protocol/`、`stream_control`、
|
||||
`link_control`「能在主机跑测试的策略代码」风格一致。
|
||||
|
||||
---
|
||||
@@ -59,7 +60,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
|
||||
```
|
||||
|
||||
- 每次启动都会用编译期值覆盖驱动配置:`wifi.set_configuration(&client_config(auth)?)`
|
||||
([src/main.rs:502](src/main.rs#L502))。所以 **NVS 里即使存着旧配置也不会被采用**。
|
||||
([src/main.rs:502](src/main.rs#L502))。所以 NVS 里即使存着旧配置也不会被采用。
|
||||
- `EspDefaultNvsPartition` 只是被交给 `EspWifi::new`([src/main.rs:178](src/main.rs#L178)),
|
||||
用于驱动自身的存储,不参与凭据解析。
|
||||
- 对比:[armor/armor-firmware/src/settings.rs](armor/armor-firmware/src/settings.rs) 的 C3 装甲板
|
||||
@@ -90,13 +91,13 @@ const PASSWORD: &str = env!("WIFI_PASS");
|
||||
关键推论(对架构有决定性影响):
|
||||
|
||||
- `0x10863` 位于**未分配空洞** `0x10000–0x70000`(`phy_init` 结束到 `factory` 开始之间,
|
||||
380 KB 全是空闲 flash)。→ 这块空洞就是天然的**坏块探测试验场**,写坏也不要紧。
|
||||
380 KB 全是空闲 flash)。→ 这块空洞就是天然的坏块探测试验场,写坏也不要紧。
|
||||
- `0xFADE7` 落在 App 镜像的**代码段**内(镜像布局见 1.5),所以“把那个字节改成别的安全值”
|
||||
会改坏指令;**唯一安全的动作是挪分区**。
|
||||
会改坏指令;唯一安全的动作是挪分区。
|
||||
- 现行偏移已经漂过至少三次:`0x10000` → `0x50000`([LATENCY.md:88](LATENCY.md#L88))→ `0x70000`
|
||||
(当前 [partitions.csv](partitions.csv))。每次重编都要重新核对,这正是要消灭的脆弱点。
|
||||
- 缺陷有两种**方向相反**的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF”
|
||||
的检查里是**隐形**的,必须写 0 才暴露。探测逻辑必须两种都打图案。
|
||||
- 缺陷有两种方向相反的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF”
|
||||
的检查里是隐形的,必须写 0 才暴露。探测逻辑必须两种都打图案。
|
||||
|
||||
### 1.5 已实测出来的镜像布局(`fps-final-raw-serial.log` 逐段对齐算出)
|
||||
|
||||
@@ -116,7 +117,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
|
||||
几个直接可用的副产品:
|
||||
|
||||
- `esp_app_desc_t` 在 **App 分区 + 0x20**,内含 `version` / `project_name` / `idf_ver` / 编译时间。
|
||||
**不需要让板子跑起来**,读 256 字节就能回答“这块板上到底是哪版固件”。
|
||||
不需要让板子跑起来,读 256 字节就能回答“这块板上到底是哪版固件”。
|
||||
- 分区表就是 0x8000 起的 0xC00 字节(`verify 0xc00 @ 0x8000` 佐证),末尾 16 字节 MD5。
|
||||
改写偏移只要重算这 16 字节。
|
||||
|
||||
@@ -142,14 +143,14 @@ const PASSWORD: &str = env!("WIFI_PASS");
|
||||
- ✅ 现场只有一个程序;已有 NSIS 打包、CI、配置与密码闸门可复用。
|
||||
- ✅ 视频端本来就要连板子,烧完顺手进监控很自然。
|
||||
- ❌ WebView 里跑长任务 + 进度 + 日志尾随,需要额外的事件/取消/背压设计。
|
||||
- ❌ 烧录是**系统级、不可中断、会变砖风险**的操作,和“看画面”共享一个进程的失败域。
|
||||
- ❌ 烧录是系统级、不可中断、有变砖风险的操作,和“看画面”共享一个进程的失败域。
|
||||
- ❌ 无法在 CI / 无人值守 / 远程 SSH 场景复用。
|
||||
|
||||
### 方案 B(推荐):纯逻辑核心库 + 双前端(CLI 优先,GUI 复用)
|
||||
|
||||
`flash/core`(无 I/O,纯函数,主机可测)+ `flashctl` CLI + 桌面端后续加一页调用 Tauri 命令。
|
||||
|
||||
- ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)**全部可在主机测**,
|
||||
- ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)全部可在主机测,
|
||||
且可以用仓库里真实的 1.1 MB 镜像做往返验收。
|
||||
- ✅ CLI 先天可脚本化:CI 能跑、现场能远程跑、能出确定退出码与机器可读报告。
|
||||
- ✅ GUI 后置,做完 CLI 后界面只是薄壳;失败域分开(GUI 崩了不影响烧录工具)。
|
||||
@@ -160,7 +161,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
|
||||
|
||||
- ✅ 落地最快,协议风险最低;`target/tools/espflash/espflash.exe` 已经在了。
|
||||
- ✅ 与现有 `scripts/*.sh` 心智一致。
|
||||
- ❌ **拿不到回读差分**(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案
|
||||
- ❌ 拿不到回读差分(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案
|
||||
坏块检测的核心,只能再调一次 `read-flash` 自己比。
|
||||
- ❌ 解析人类可读输出、处理 `esptool.py` 的 Python 依赖,现场极易碎(现有 flash.sh 就是被
|
||||
“macOS 串口名 + termios + 本地 IDF venv”绑死的)。
|
||||
@@ -177,7 +178,7 @@ const PASSWORD: &str = env!("WIFI_PASS");
|
||||
| 交付成本 | 10% | 6 | 6 | **9** |
|
||||
| 加权 | | 6.45 | **8.10** | 5.65 |
|
||||
|
||||
**推荐 B**,并明确:GUI 不是不做,而是**放到 P2**——先用 CLI 把危险逻辑做对、做实测。
|
||||
**推荐 B**。GUI 不是不做,而是放到 P2:先用 CLI 把危险逻辑做对、做实测。
|
||||
|
||||
---
|
||||
|
||||
@@ -284,8 +285,8 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─
|
||||
| 串口日志诊断 | 症状:`invalid header` / checksum 失败 / rst 原因 | 秒级 | 板子已经在现场、只能看现象时 |
|
||||
|
||||
要点:
|
||||
- 图案探测必须**双向**:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形——
|
||||
`0x10863` 就是整段回读才发现就是这个原因。
|
||||
- 图案探测必须双向:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形,
|
||||
`0x10863` 就是这样在整段回读里才发现的。
|
||||
- 探测区选空洞而不是 NVS:**不要为了体检去毁 NVS**。若怀疑 NVS 区有坏点,单独提供
|
||||
“备份 NVS → 探测 → 还原”的危险模式,默认关闭。
|
||||
- probe 结束后必须 `erase` 干净,并**复核空洞回到 0xFF**,避免留下疑似数据。
|
||||
@@ -309,7 +310,7 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─
|
||||
- **对齐放宽到 4 KiB**:App 分区只要求 4 KiB 对齐,`flash.sh` 用 64 KiB 是自我限制。
|
||||
候选从 ~230 个变成 ~3700 个,命中概率高一个数量级(单坏点 1/8,双坏点也能稳过)。
|
||||
- **分区大小 ≠ 镜像大小**:`0x120000` 的分区里镜像只占 `0x10E700`,尾部 71 KiB 是空的。
|
||||
坏扇区落在尾部 slack 里**无害**,别把它算成阻塞。
|
||||
坏扇区落在尾部 slack 里无害,别把它算成阻塞。
|
||||
- **bootloader 与分区表固定偏移**:`0x0`(0x5160) 与 `0x8000`(0xC00) 挪不了。
|
||||
这里只能做**字节级吸收**:例如坏点落在分区表某条 `label` 字段里,就换一个 label 让那一位合法
|
||||
(label 是自由文本);落在 `magic/offset/size/MD5` 上则该板报废。这条路径要显式实现并明确其边界。
|
||||
@@ -317,7 +318,7 @@ Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)─
|
||||
### 4.3 执行(`flash-core::parttable` + `image`)
|
||||
|
||||
- 改写 `partition-table.bin` 中 `factory` 的 offset(和必要时 size)→ 重算末尾 16 字节 MD5。
|
||||
- **App 镜像原样写入新偏移**——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。
|
||||
- App 镜像原样写入新偏移——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。
|
||||
- 只有在“分区表/bootloader 必须字节吸收”这一层才动镜像,并且要重算 checksum/SHA256。
|
||||
|
||||
### 4.4 验证与回滚(`session` + `report`)
|
||||
@@ -346,7 +347,7 @@ checksum = 0xEF XOR (所有段的所有数据字节) // 1 字节,位于
|
||||
sha256 = SHA256(镜像中它之前的全部字节) // 32 字节,仅当 header.hash_appended=1
|
||||
```
|
||||
|
||||
任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在**真实镜像上做往返测试**
|
||||
任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在真实镜像上做往返测试
|
||||
(重算结果与文件里已有的字节必须完全相同)——这是整个方案最便宜、最有效的验收手段,
|
||||
在碰硬件之前就能证明实现是对的。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user