Files
pig-farm-controller/design/ota-upgrade-and-log-monitoring/ota_upgrade_solution.md
2025-12-03 16:23:33 +08:00

305 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 区域主控 MicroPython OTA 升级方案
## 1. 概述
### 1.1. 目标
实现区域主控 (ESP32-S3-N16R8, MicroPython 固件) 的安全、可靠的远程固件升级 (OTA)。
### 1.2. 核心思想
* **AB 分区模式**: 区域主控采用 AB 分区模式,允许在设备运行时更新非活动分区,升级失败时可回滚到上一个已知的工作版本。
* **平台主导**: 升级过程由平台完全控制,包括固件准备、文件分发和升级指令下发。
* **LoRa 传输层自动分片**: 充分利用 LoRa 传输层自动分片和重组的能力,简化应用层协议设计。
* **逐文件校验**: 设备在接收每个文件后立即进行 MD5 校验,确保文件完整性,并处理重试。
* **清单文件**: 使用清单文件管理所有待更新文件的元数据和校验信息。
* **设备自驱动**: 设备主动请求清单文件和固件文件,并在所有文件校验成功后自行激活新固件并重启。
* **平台记录升级任务**: 平台将记录 OTA 升级任务的创建、进度和最终状态。
* **配置文件独立管理**: OTA 升级过程将不涉及配置文件的更新,配置文件由平台提供独立的远程修改功能。
### 1.3. 涉及组件
* **平台**: 负责固件包管理、清单文件生成、数字签名(未来)、文件分发、指令下发、状态接收和**升级任务记录**。
* **LoRa 传输层**: 负责应用层数据的分片、传输和重组。
* **区域主控 (ESP32-S3-N16R8)**: 负责接收文件、存储到非活动分区、文件校验、分区切换、新固件启动验证和状态上报。
## 2. 固件包结构与准备
### 2.1. 原始固件包 (由开发者提供给平台)
* 一个标准的压缩包(例如 `.zip`),其中包含所有 MicroPython `.py` 文件、资源文件等。
* 压缩包内的文件结构应与期望在设备上部署的路径结构一致。
### 2.2. 平台处理流程
1. **接收**: 平台接收开发者上传的 MicroPython 项目压缩包。
2. **解压**: 平台将该压缩包解压到内部的一个临时目录。
3. **分析与生成清单**: 平台遍历解压后的所有文件,为每个文件计算:
* 在设备上的目标路径 (`path`)
* MD5 校验和 (`md5`)
* 文件大小 (`size`)
* **排除配置文件**: 平台会识别配置文件(例如通过文件名约定,如 `/config/` 目录下的所有文件),并**排除**
这些文件,不将其包含在清单文件中,也不通过 OTA 传输。
4. **生成清单文件**: 平台根据上述信息,生成一个 JSON 格式的清单文件。
5. **数字签名 (未来扩展)**: 平台使用其私钥对**清单文件**的内容进行数字签名,并将签名添加到清单文件中。
### 2.3. 清单文件 (Manifest File) 结构
清单文件是一个 JSON 对象,包含新固件的元数据和所有文件的详细信息。
```json
{
"version": "1.0.1",
// 新固件版本号
"signature": "...",
// 清单文件内容的数字签名 (未来扩展)
"files": [
{
"path": "/manifest.json",
// 清单文件本身也作为文件列表的一部分
"md5": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"size": 1024
},
{
"path": "/main.py",
"md5": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1",
"size": 10240
},
{
"path": "/lib/sensor.py",
"md5": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2",
"size": 5120
}
// ... 更多文件 (不包含配置文件)
]
}
```
## 3. 通信协议定义 (Protobuf Messages)
以下是 OTA 过程中平台与区域主控之间通信所需的 Protobuf 消息定义。
```protobuf
// OTA 升级指令和状态消息
// PrepareUpdateReq: 平台发送给设备,通知设备准备开始 OTA 升级
message PrepareUpdateReq {
string version = 1; // 新固件版本号
uint32 task_id = 2; // 升级任务唯一ID
string manifest_md5 = 3; // 清单文件的 MD5 校验和,用于设备初步校验清单文件完整性
}
// RequestFile: 设备向平台请求特定文件 (包括清单文件和固件文件)
message RequestFile {
uint32 task_id = 1; // 升级任务ID
string filepath = 2; // 请求的文件路径 (例如 "/manifest.json" 或 "/main.py")
}
// FileResponse: 平台响应设备请求,发送单个文件的完整内容
// LoRa 传输层会自动处理分片和重组,因此应用层可以直接发送完整的单个文件内容
message FileResponse {
uint32 task_id = 1; // 升级任务ID
string filepath = 2; // 设备上的目标路径 (例如 "/manifest.json" 或 "/main.py")
bytes content = 3; // 文件的完整内容
}
// UpdateStatusReport: 设备向平台报告升级状态
message UpdateStatusReport {
uint32 task_id = 1; // 升级任务ID
string current_version = 2; // 操作完成后的当前版本
enum Status {
STATUS_UNKNOWN = 0;
// --- 设备主动上报的状态 ---
SUCCESS = 1; // 升级成功,新固件已运行 (由设备在自检成功后主动上报)
SUCCESS_ALREADY_UP_TO_DATE = 2; // 版本已是最新,未执行升级 (由设备在版本检查后主动上报)
FAILED_PRE_CHECK = 3; // 升级前检查失败 (例如拒绝降级、准备分区失败等,由设备主动上报)
FAILED_DOWNLOAD = 4; // 文件下载或校验失败 (由设备在下载过程中主动上报)
// --- 平台推断的状态 (数据库记录用) ---
FAILED_TIMEOUT = 5; // 平台在超时后仍未收到SUCCESS报告将任务标记为此状态
}
Status status = 3; // 升级的最终状态
string error_message = 4; // 人类可读的详细错误信息
string failed_file = 5; // 失败时关联的文件路径 (可选)
}
```
## 4. 平台侧操作流程
### 4.1. 准备升级任务
1. 接收开发者提供的 MicroPython 项目压缩包。
2. 解压压缩包。
3. 遍历解压后的文件,计算每个文件的 MD5、大小并确定目标路径。
4. **排除配置文件**: 平台会识别配置文件(例如通过文件名约定,如 `/config/` 目录下的所有文件),并**排除**这些文件。
5. 生成清单文件 (Manifest File)。**注意:清单文件本身也应作为 OTA 的一部分,其元数据应包含在清单文件自身的 `files`
列表中。Manifest文件生成后将被放在解压后的文件夹的根目录下, 方便后续主控设备获取**
6. (未来扩展)对清单文件进行数字签名。
7. 将清单文件和所有固件文件存储在平台内部,等待分发。
8. **记录 OTA 升级任务**: 在数据库中创建一条新的 OTA 升级任务记录(模型名为 `OTATask`,位于 `internal/infra/models/ota.go`
),包含任务 ID、目标设备、新固件版本、状态例如“待开始”
### 4.2. 发送“准备更新”指令
1. 平台向目标区域主控发送 `PrepareUpdateReq` 消息。
2. **更新任务记录**: 平台发送指令后,更新 OTA 任务记录的状态为“进行中”。
### 4.3. 响应设备文件请求
1. 平台接收区域主控发送的 `RequestFile` 消息。
2. 平台根据 `task_id``filepath` 在内部存储中找到对应的文件内容。
3. 平台构建 `FileResponse` 消息,将文件的完整内容和路径放入其中。
4. 平台通过 LoRa 传输层发送 `FileResponse` 消息。
### 4.4. 处理设备状态上报
1. 平台接收区域主控发送的 `UpdateStatusReport` 消息。
2. 根据报告的 `status` (`SUCCESS``FAILED`),更新 OTA 任务记录的最终状态,并记录 `error_code``error_message`
3. 如果状态为 `SUCCESS`,平台应更新该设备在系统中的固件版本记录。
4. **总超时管理**: 平台为每个 OTA 任务设置一个总的超时时间(例如 2 小时)。如果在总超时时间内未能收到设备的最终状态报告,平台应自动将该任务标记为
`FAILED``error_code` 设为 `ERR_TIMEOUT`
5. **处理重复报告**: 平台在收到最终状态报告后,即使后续再次收到相同的报告,也只需更新一次任务记录,无需重复处理。
## 5. 区域主控侧操作流程 (MicroPython)
### 5.1. 接收“准备更新”指令与版本检查
1. 区域主控接收 `PrepareUpdateReq` 消息。
2. **版本检查**: 设备将 `PrepareUpdateReq` 中的 `version` 与自身当前运行的固件版本进行比较。
* **降级场景**: 如果 `新版本 < 当前版本`,设备立即中止升级,并向平台发送 `UpdateStatusReport` (status: `FAILED`,
error_code: `ERR_VERSION_ROLLBACK`, error_message: "拒绝版本回滚,目标版本低于当前版本")。
* **同版本场景**: 如果 `新版本 == 当前版本`,设备立即中止升级,并向平台发送 `UpdateStatusReport` (status: `SUCCESS`,
error_code: `SUCCESS_ALREADY_UP_TO_DATE`, error_message: "版本已是最新,无需升级")。
* **正常升级场景**: 如果 `新版本 > 当前版本`,继续执行下一步。
3. **清空非活动分区**: 使用 MicroPython 的文件系统操作(例如 `os.remove()``os.rmdir()`),递归删除非活动 OTA 分区(例如
`/ota_b`)下的所有文件和目录。
* **错误处理**: 如果清空分区失败,设备应立即中止,并向平台发送 `UpdateStatusReport` (status: `FAILED`, error_code:
`ERR_PREPARE`, error_message: "清空非活动分区失败: [具体错误]").
4. 设备准备就绪后,将直接开始请求清单文件。
### 5.2. 请求并验证清单文件
1. 设备完成准备后,向平台发送 `RequestFile` 消息,请求清单文件 (`filepath: "/manifest.json"`)。
2. 设备接收平台响应的 `FileResponse` 消息,并将其写入非活动分区(例如 `/ota_b/manifest.json`)。
3. **MD5 校验**: 计算写入的清单文件的 MD5并与 `PrepareUpdateReq` 消息中提供的 `manifest_md5` 进行比对。
4. **解析 JSON**: 解析清单文件内容。
5. **数字签名验证 (未来扩展)**: 使用预置的平台公钥,验证清单文件的数字签名。
6. 如果上述任何步骤失败,设备应向平台发送 `UpdateStatusReport` (status: `FAILED`, error_code: `ERR_MANIFEST_VERIFY`,
error_message: "[具体失败原因]"), 然后中止升级。
### 5.3. 请求与存储固件文件 (逐文件校验)
1. 设备成功接收并验证清单文件后,根据清单文件中的文件列表,**逐个文件**地向平台发送 `RequestFile` 消息。
2. 对于每个请求的文件:
* **请求、接收与写入**: 设备请求文件,接收响应,并根据 `filepath` 将内容写入到非活动 OTA 分区。需要确保目标目录存在,如果不存在则创建。
* **MD5 校验**: 在文件写入完成后,计算该文件的 MD5 校验和,并与清单文件中记录的 MD5 进行比对。
* **错误处理与重试**:
* 如果文件下载超时、写入失败或 MD5 校验失败,设备将进行重试(例如最多 3 次)。
* 如果达到最大重试次数仍失败,设备应立即中止整个 OTA 任务,并向平台发送 `UpdateStatusReport` (status: `FAILED`,
error_code: `ERR_DOWNLOAD``ERR_VERIFY`, error_message: "[具体失败原因]", failed_file: "[失败的文件路径]")。
### 5.4. 自激活与重启
1. **所有文件接收并校验成功后**,设备将自行执行以下操作:
* **配置 OTA 分区**: 使用 MicroPython 提供的 ESP-IDF OTA API设置下一个启动分区为刚刚写入新固件的非活动分区。
* **自触发重启**: 在成功配置 OTA 分区后,区域主控自行触发重启。
### 5.5. 新版本启动与验证
1. 设备重启后,启动加载器会从新的 OTA 分区加载 MicroPython 固件。
2. **自检**: 新固件启动后,应执行必要的自检(如 LoRa 初始化、网络连接等)。
3. **标记有效**: 只有当所有自检项都成功通过后,新固件才必须调用相应的 API例如 `esp.ota_mark_app_valid_cancel_rollback()`
)来标记自身为有效。
4. **看门狗与回滚**:
* 如果新固件在一定次数的尝试后仍未标记自身为有效,启动加载器会自动回滚到上一个有效固件。
* 在 MicroPython 应用层,如果自检失败,**绝不能**标记自身为有效,并应等待底层机制自动触发回滚。
### 5.6. 报告最终状态
1. **成功场景**: 新固件自检成功并标记有效后,向平台发送 `UpdateStatusReport` (status: `SUCCESS`, current_version:
新版本号)。
2. **回滚场景**: 设备回滚到旧版本后,向平台发送 `UpdateStatusReport` (status: `FAILED`, error_code: `ERR_ROLLED_BACK`,
error_message: "新固件启动失败,已自动回滚", current_version: 旧版本号)。
3. **重复发送**: 为了提高在单向 LoRa 通信中的可靠性,设备在发送最终状态报告时,应在短时间内重复发送多次(例如 3-5 次)。
## 6. 关键技术点与注意事项
### 6.1. LoRa 传输层
* 确保 `internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go` 能稳定处理大尺寸 Protobuf 消息的分片和重组。
### 6.2. 平台侧的请求处理
* `internal/app/listener/lora_listener.go` 在接收 `RequestFile` 消息时,需要高效处理,避免阻塞监听器。
### 6.3. 文件系统操作 (MicroPython)
* 确保文件系统操作(创建目录、写入文件、删除文件)的正确性和鲁棒性,并对错误进行捕获和报告。
### 6.4. MD5 校验 (MicroPython)
* MicroPython 的 `hashlib` 模块提供 MD5 算法。确保计算的效率和准确性。
### 6.5. OTA 分区管理 (MicroPython)
* 熟悉 ESP-IDF 的 OTA 机制在 MicroPython 中的绑定和使用方法。正确调用 API 来设置启动分区和标记应用有效。
### 6.6. 回滚机制
* 完全依赖 ESP-IDF 提供的 OTA 回滚机制。新固件必须在启动后标记自身为有效,否则会自动回滚。
### 6.7. 错误处理与重试
* **设备侧**: 实现文件级别的下载和校验重试。对于无法恢复的错误,立即上报 `FAILED` 状态并中止任务。
* **平台侧**: 实现任务级别的总超时管理。这是处理设备意外断电、失联等情况的关键机制。设备重启后无需保留升级状态,简化了设备端逻辑。
### 6.8. 安全性
* **数字签名**: 强烈建议尽快实现清单文件的数字签名。**没有数字签名OTA 过程将面临严重的安全风险(如中间人攻击)**
,攻击者可能下发恶意固件。平台的公钥需要被硬编码到设备固件中,作为信任的根基。
* **LoRaWAN 安全**: 确保 LoRaWAN 的网络层和应用层密钥管理得当, 防止未经授权的设备加入网络或窃听数据。
---
## 7. 固件 OTA 升级流程描述
### 阶段一:任务准备与下发
1. **上传与准备 (Developer -> Platform)**: 开发者上传固件包平台解压、计算MD5、生成清单文件、创建升级任务。
2. **下发更新通知 (Platform -> Device)**: 平台向设备发送 `PrepareUpdateReq`
### 阶段二:设备版本检查与准备
1. **版本检查 (Device)**:
* **失败分支 (降级/同版本)**: 设备拒绝升级,上报 `FAILED` (ERR_VERSION_ROLLBACK) 或 `SUCCESS` (
SUCCESS_ALREADY_UP_TO_DATE),流程结束。
* **成功分支**: 版本检查通过,设备继续。
2. **设备准备 (Device)**:
* 设备清空非活动分区。
* **失败分支**: 上报 `FAILED` (ERR_PREPARE),流程结束。
* **成功分支**: 设备发送 `RequestFile` 请求清单文件。
### 阶段三:文件循环下载和校验
1. **清单文件传输与校验 (Platform <-> Device)**:
* 平台发送清单文件,设备接收并校验。
* **失败分支**: 上报 `FAILED` (ERR_MANIFEST_VERIFY),流程结束。
2. **固件文件循环 (Device <-> Platform)**:
* 设备逐个请求、下载、校验清单中的所有文件。
* **失败分支 (重试耗尽)**: 上报 `FAILED` (ERR_DOWNLOAD / ERR_VERIFY),流程结束。
### 阶段四:激活与最终状态
1. **激活重启 (Device)**: 所有文件成功下载后,设备配置启动分区并重启。
2. **新固件自检 (Device)**:
* **成功分支**:
* 设备标记自身为有效。
* 设备上报 `SUCCESS`
* 平台更新任务状态为 `SUCCESS`
* **失败分支 (自检失败/未标记)**:
* 设备等待底层机制自动回滚。
* 设备回滚后,上报 `FAILED` (ERR_ROLLED_BACK)。
* 平台更新任务状态为 `FAILED`
3. **总超时检查 (Platform)**: 如果在规定时间内未收到任何最终报告,平台将任务标记为 `FAILED` (ERR_TIMEOUT)。