存储、文件系统与数据保护

[English]

本章统一规定固件、Python 程序、Device Core、IDE/Agent 客户端和恢复工具的存储契约,目标是保护用户文件,并让破坏性恢复由用户决定。运行时诊断和恢复 API 见 storage – 存储诊断与确认恢复。

存储介质与文件系统所有权

存储职责

介质

当前用途

规范

内部 Flash ffat

FAT;Python 路径 / 及别名 /flash,IDF 内部路径 /flash;支持 USB device 的开发板通过 MSC LUN 0 暴露

ESP-IDF 唯一持有文件系统。Python VFS 与 MSC 访问相同数据,不是两份副本。这个命名的 data/FAT 分区可按已配置的自动初始化策略格式化,或由用户明确确认后执行恢复。

旧 Flash vfs

挂载已有 FAT;也可能包含旧固件的其他文件系统

自动启动和在线恢复都只允许挂载。未知格式或 LittleFS 卷不能被当成可丢弃的 FAT。

SD 卡

FAT,路径 /sdcard;支持 SD 的 USB device 开发板通过 MSC LUN 1 暴露

ESP-IDF 唯一持有文件系统。缺卡、坏卡不得阻止 Flash、VM 和通信工作。自动策略可以初始化无法识别的卡;显式 Python SD 格式化仍不支持。其他恢复应备份后在主机上明确选择目标卡处理。

NVS

网络配置、写入所有权和格式化事务记录等小型状态

与用户文件分离。NVS 错误不授权擦除 NVS 或格式化数据卷。Flash 文件备份不包含 NVS。

配有外部 NAND 的开发板

当前尚未暴露为 ESP-VISION 文件系统,包括 ESP-Mosaico 的板载 NAND

后端完成验收前应报告不支持,不能当作 SD,也不能直接套用原始 NOR ffat 驱动;后续支持必须处理 NAND 坏块、ECC 和恢复问题。

冻结模块/只读 RAM VFS

固件模块或临时模型资源

不是可写用户卷,也不是持久备份。RAM 内容重启即失,冻结内容属于固件镜像。

开发板分区表是偏移和大小的唯一依据。不能根据模组版本、旧示例或其他开发板上同名分区推断破坏性操作范围。Board Manager 驱动和相机、模型等使用方应复用公共存储管理器,不能在同一介质上再挂一套文件系统。

/.esp_vision_disk 只是创建默认文件的标记。它和已保存的 NVS 状态都不是备份,也不构成新的格式化授权。格式化授权来自下面的板级策略或用户明确作出的恢复决定。

当前 Flash 后端使用项目自有的原始块驱动:512 字节逻辑扇区,4096 字节擦除块读改写。它既不是日志文件系统,也不是 ESP-IDF wear levelling 卷。高频日志、图片和录像应使用合适的可移除介质并限制保留量,同时考虑剩余空间、擦写寿命和元数据开销。

可选卷与默认自动挂载

Flash 和 SD 是两个独立的可选卷。默认启动时尝试挂载 Flash,并在支持 SD 的开发板上挂载可用的卡。Flash 缺失/挂载失败不阻止 SD 挂载;SD 缺卡/挂载失败不阻止 Flash 挂载。初始化遵循下面的独立策略,普通挂载错误不触发初始化。两个卷均不可用时,VM 和控制通道仍可使用虚拟根目录与诊断功能,启动脚本不会自动改从 SD 执行。

各开发板可在 boards/<BOARD>/boardconfig.h 中分别覆盖默认值:

#define ESP_VISION_STORAGE_AUTO_MOUNT_FLASH (1)
#define ESP_VISION_STORAGE_AUTO_MOUNT_SD    (1)
#define ESP_VISION_STORAGE_FLASH_FORMAT_IF_NO_FILESYSTEM (1)
#define ESP_VISION_STORAGE_SD_FORMAT_IF_NO_FILESYSTEM    (0)

将任一 AUTO_MOUNT 值设为 0 可关闭该介质的自动挂载。这是相互独立的编译期策略,不是 Python 持久配置,也不改变硬件能力。SD 仍需要 MICROPY_HW_ENABLE_SDCARD 和支持的 SDMMC 后端。介质可用但关闭自动挂载时,报告 unmounted / STORAGE_NOT_MOUNTED;即使开启自动初始化,显式调用 storage.retry("flash") 或 storage.retry("sd") 也只挂载、绝不格式化。检查结果后,由用户选择软复位来发布新的 Python 挂载。启动时不可用的 SD 卡仅在开启自动 SD 挂载时于后续 VM 启动重试;已挂载的物理文件系统在软复位期间保留。

ESP_VISION_STORAGE_FLASH_FORMAT_IF_NO_FILESYSTEM 默认是 1;ESP_VISION_STORAGE_SD_FORMAT_IF_NO_FILESYSTEM 默认是 0。自动挂载时,只有对应介质的策略开启,FatFs 精确返回 FR_NO_FILESYSTEM 才授权格式化并重新挂载。因此内部 ffat 默认允许初始化;无法识别的 SD 卡默认保留,只有显式开启 SD 策略后才允许初始化。非空出厂镜像、其他文件系统或损坏的 FAT 都可能返回此结果,它不能证明区域内没有有价值的数据。开启任一策略都表示接受无需运行时确认便擦除这些内容;需要保留未知数据时,应将对应策略保持为 0。旧 vfs 无论这两个选项如何都保持只挂载。

已有可挂载 FAT 会保留。FR_INT_ERR、I/O/未就绪/写保护错误、缺少介质、未关闭句柄以及被占用/隔离的写入方均不授权自动格式化。关闭某介质的自动挂载后,不会自动初始化它。storage.retry() 和写入方交接时的文件系统刷新始终只挂载。破坏性写入前,日志会明确标识由策略触发的初始化;初始化失败返回错误,不重启、不导致 crash。

内部 Flash 在 NVS 中持久保存 PENDING/READY 格式化事务;即使开启 Flash 策略,中断的事务也阻止继续自动格式化,需要用户重新明确选择恢复。SD 当前没有持久格式化日志或同等的中断保护。若显式开启 SD 自动初始化,断电后下次挂载返回 FR_NO_FILESYSTEM 时可能再次初始化。无论策略开关如何,部分创建的 FAT 都可能看起来可读,这不能证明断电恢复成功。应保留经校验的设备外备份;默认 SD 策略会保留无法识别的内容,而非格式化它们。

Flash 上的 /main.py 和 /flash/main.py 指向同一文件。别名只是连接到已有 IDF 文件系统的第二个 Python VFS bridge,不会再挂一套 FAT、复制文件或增加 USB 磁盘。相对路径跟随当前选中 bridge 的工作目录。os.rename() 的两个参数应使用一致的前缀:MicroPython 拒绝不同 VFS 挂载点之间的重命名,包括 / 与 /flash。/flash 保留为挂载点;已有的物理 flash 目录会保留,可通过 /flash/flash 访问。/flash/sdcard 指向 Flash 上的目录,不是位于 /sdcard 的独立 SD 卷。

备份和恢复工具必须按卷识别数据,不能把 / 和 /flash 当作两份副本或递归进入虚拟挂载点。建议用 /flash 作为明确的 Flash 备份根目录,另行备份 /sdcard。

两个卷在线时的 USB 行为

默认卷标为 ESP_FLASH 和 ESP_SDCARD。自动挂载仅为未命名的卷设置默认名,保留自定义卷标;卷标错误不授权格式化。USB 配置、命名限制、接口字符串和主机盘符见 USB 接口、配置与磁盘命名。

开启原生 USB device MSC 的开发板/接口使用一个 MSC 接口暴露固定逻辑单元:LUN 0 是内部 Flash,支持 SD 的开发板再提供 LUN 1。两个卷都已挂载、健康且写入方准入允许主机访问时,主机可以枚举两个独立介质。盘符、主机挂载路径和空槽显示方式由操作系统决定。不支持 SD 的开发板只提供 Flash 槽位。USB Serial/JTAG 调试接口不提供这些 MSC 磁盘。

某介质缺失、未挂载或不可用时,只让对应槽位报告未就绪;Flash 不可用时,SD 不会改成 LUN 0。关闭自动挂载只推迟该槽位就绪,不影响另一卷或整个 USB 接口。/flash 不增加 LUN。弹出和写入方交接按卷进行:Python/IDE 写入前,应安全弹出所选操作系统卷并核对其所有权;看到两个磁盘不表示主机与 VM 可以同时写入任一共享文件系统。

故障原则:保留数据,保持控制通道可用

挂载、分配内存、NVS、写入、关闭、同步和卡就绪状态等失败,都应作为可恢复的存储条件处理。存储错误路径必须返回状态或错误,必要时关闭 I/O 准入,并保持 REPL、会话管理和网络/控制通信可用。不得通过致命断言、abort、crash 或重启掩盖失败。Python 参数异常和操作返回 ok: False 不等于设备 crash。

挂载失败本身不构成格式化授权,自动处理的例外是上述仅适用于精确 FR_NO_FILESYSTEM 的独立板级策略,默认 Flash 开启、SD 关闭。关闭某介质的策略后,其空白分区、出厂/未知数据和损坏卷都保持原样。NVS 记录缺失不会独立授权格式化,NVS 错误也不能绕过 Flash 事务保护。storage.retry() 始终不创建文件系统、不擦除区域、不复位设备。

诊断与用户选择

状态

含义

用户决定的下一步

ready

文件系统已挂载;写入权限仍需单独满足所有权规则

按规则访问文件,并维护经过验证的设备外备份。

blank

完整读取后确认分区全部已擦除

确认这就是预期的空卷后,选择初始化。

unmounted

可用介质的自动挂载被关闭

如需挂载,可使用 storage.retry();这不是格式化请求。

unrecognized

无法挂载 FAT;可能是出厂资源、其他文件系统,也可能是损坏的用户数据

先保留/导出所需数据、核对布局,再选择重试、离线恢复或确认格式化。

absent/unsupported

介质缺失或能力不可用

检查卡和开发板能力;不能向用户推荐格式化不存在的硬件。

io_error/unavailable

驱动、挂载或资源错误

查看错误码、供电、连接和可用资源,处理原因后再重试。

state_error

NVS 格式化事务元数据不可读或校验失败

保留 NVS 和数据卷用于诊断,不能通过清除历史绕过保护。

format_interrupted

格式化未能完成

Flash 会持久保存此保护并要求重新决定;SD 没有同等持久日志。重启前应检查介质和板级策略。

写入方占用/隔离

其他写入方持有介质,或交接结果不确定

完成经过验证的交接,或调查隔离原因;格式化请求不覆盖所有权限制。

storage.status() 返回状态、稳定错误码、可读提示、挂载/存在/能力标志、原生错误、已知的 FatFs 结果和 Flash 几何信息。Device Core 的 inspect({ fields: ['storage'] }) 保留原有写入方字段,并增加 fs_state、fs_error、fatfs_result 和 format_supported。这些字段是状态快照,不是销毁数据的授权。控制通信启动后会输出可执行的存储诊断;向未挂载卷上传文件时,原生上传返回 STORAGE_UNAVAILABLE 和恢复建议,而不是含糊的文件打开失败。

前端必须显示受影响卷、原因和数据影响,在显式破坏性操作前提供检查、重试和导出选项,并等待用户主动确认手动格式化。Agent 和无人值守任务不能把超时、重连、泛化的“修好错误”请求或出厂数据解释成新的运行时确认。固件已配置的自动初始化属于独立启动策略。本次改动提供状态、日志、错误和显式 REPL API,不代表所有既有 IDE/Web UI 已有备份向导或格式化按钮。

单写入方、文件发布与断电

EV-MUX 会话所有权不等于文件系统写入所有权。Python/IDE 写入、备份、恢复或卸载前,应按 交互式 Python、脚本与文件操作 中的存储写入方交接契约与操作系统的 MSC 访问协调:安全弹出主机卷,停止应用和后台写入,排空句柄和写入,再核对实际所有权。结果不确定的交接应跨重启保持隔离。旧版兼容模式不能作为已经验收的并发写入保证;仅连接 SDK 不会让操作系统和 Python 同时写入变安全。

未挂载 Flash 对 MSC 不就绪,格式化事务完成前不会发布给主机。如果后续 FAT 变得无法识别且自动初始化策略仍开启,事务保护不能保证保留后来写入的文件。SD 卡独立判断就绪状态,其格式化没有持久事务保护。不能忽略关闭失败、仍持有句柄的上传或写入隔离来强行完成格式化。

原生二进制上传使用临时文件、预期长度和 SHA-256、同步/关闭和最终发布;普通替换失败会尝试从旧文件回滚。这能防护传输错误和普通发布失败,不能保证 FAT 多次目录更新之间每一种断电情况都安全。同一介质上的临时文件和回滚文件不是独立备份。fsync 和成功的传输响应,不会把 FAT 或原始 Flash 擦除块更新变成有日志、断电原子的事务。最终响应丢失后,应先检查目标文件,再决定是否重试。

重要应用数据应写到新文件,校验并关闭/同步后,再发布且保留已验证的上一代,直到确认成功。限制日志大小,为新旧两代文件预留空间。写入或关闭失败后应停止后续写入并上报原始错误;反复重试或删除其他文件不等于恢复。

备份与恢复规范

备份必须位于不同的故障域,通常是主机电脑或独立保留的其他介质。同一 Flash/卡上的备份目录和旧文件重命名,不能抵御整个介质损坏、误格式化或介质故障。

  1. 确认物理设备、开发板、固件和选定卷。停止脚本及所有后台写入,安全弹出 MSC 并核对所有权后,再复制一致的文件系统快照。若无法停止写入,只能把结果标为未验证副本,不能称为一致备份。

  2. 导出用户脚本、隐藏配置、所需包、模型元数据和其他必要文件。在主机清单中记录路径、大小、SHA-256、设备身份和文件系统/布局信息。保留多个备份版本,不能覆盖最后一份已知可用备份。

  3. 根据清单验证下载的字节,并检查预期文件是否齐全。下载结束不等于备份可用。应定期在合适的备用介质/设备上验证恢复。

  4. 恢复前检查目标身份、布局兼容性和剩余空间。通过正常的校验上传及所有权流程导入,核对长度、哈希和回读;验证成功前保留备份。用户检查之前,不能自动执行恢复的启动脚本。

当前文件 API 支持通过 Device Core 导出/导入选定文件,也支持安全交接后的主机 MSC 复制。本次固件 API 未实现完整的清单式备份/恢复向导。主机实现此功能时,应遵守上述规范,区分成功、部分完成、失败和未验证副本,不能把部分复制标成已验证备份。

文件系统无法挂载时,可以导出指定原始分区镜像保留离线恢复证据。进入 ROM 模式会中断运行;用户明确请求后,可按已安装分区表中的实际偏移和长度读取:

esptool --chip <CHIP> --port <PORT> read-flash <OFFSET> <LENGTH> <HOST_BACKUP.bin>

原始镜像不是已经验证的文件级备份,恢复时可能需要匹配的布局和加密上下文。NVS/系统状态备份应单独明确范围,且可能包含凭据,应妥善保护文件。日常开发烧录不自动备份或恢复固件,也不自动读取整片 Flash;只有用户明确要求时才执行这些操作。

显式恢复未挂载的 Flash 卷

先进行不写盘的检查:

import storage
print(storage.status("flash"))
print(storage.status("sd"))
print(storage.retry("flash"))  # 只重试挂载,绝不格式化

如果 retry 返回 ok: True 和 soft_reset_required: True,用户可选择软复位以发布新的 Python 挂载。失败时保留运行环境和诊断状态。

只有在用户确认目标 Flash 卷、保留所需文件,并同意擦除后,才执行以下独立的破坏性操作:

result = storage.format("flash", confirm=True)
print(result)

必须传入布尔值 True;省略、假值或真值字符串都会被拒绝。恢复只格式化未挂载的 data/FAT ffat,并先检查打开句柄和写入准入。已挂载/可读取卷、旧 vfs、在线 SD 格式化以及不可用/隔离写入方都会被拒绝。硬件 I/O 错误不会触发格式化。范围来自设备分区表,不擦除 NVS、固件或 SD。

格式化前,会先提交与分区绑定的 NVS PENDING 记录。只有格式化、重新挂载和 READY 持久提交全部成功,才发布该卷。提交、格式化或挂载失败时保持不可用;中断的 PENDING 不会自动恢复,即使残留 FAT 头看起来可以读取,也必须由用户再次明确决定。清除 NVS 不是绕过该状态的合规恢复方式。

检查 ok 和 operation_error。成功且返回 soft_reset_required 时,用户可显式软复位,以发布 Python 挂载并创建缺失的默认文件:

import machine
machine.soft_reset()

复位是用户选择的后续步骤,不是存储故障的自动响应。恢复应用之前,应重新检查存储,并验证创建、上传、回读以及重启后的文件持久性。失败或响应丢失意味着完成状态可能不确定;再次执行破坏性操作前必须先检查状态。

固件安装与验收

普通固件升级保留用户文件和 NVS;完整安装、布局改变和整片擦除具有不同的数据影响,主机必须在用户选择前说明。不能因布局不一致而静默把升级改为安装,或格式化数据卷。升级与安装契约见 应用 API 完整参考。

验收应覆盖空白和残留数据介质、有效旧 FAT、损坏 FAT、缺卡/拔卡、NVS 错误、空间耗尽、写入/关闭/同步失败、格式化中断及所有权交接不确定等情况。必须验证已开启且精确匹配 FR_NO_FILESYSTEM 的初始化策略以外,挂载失败不会自动擦除/格式化/写入 NVS;对策略授权的初始化,验证各介质默认值及 Flash 的 PENDING/READY 事务保护。同时验证 REPL 和会话仍可用,提示可定位,显式确认保护有效,其他卷不受影响。主机故障注入不等于真实断电或特定模组实机验收,应报告准确提交、介质、开发板、接口及实际硬件证据。