Files
pig-farm-controller/design/ota-upgrade-and-log-monitoring/ota_upgrade_solution.md
2025-12-02 16:40:10 +08:00

260 lines
15 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. **分析与生成清单**: 平台遍历解压后的所有文件,为每个文件计算:
* 文件名 (`name`)
* 在设备上的目标路径 (`path`)
* MD5 校验和 (`md5`)
* 文件大小 (`size`)
* **排除配置文件**: 平台会识别配置文件(例如通过文件名约定),并**排除**这些文件,不将其包含在清单文件中,也不通过 OTA 传输。
4. **生成清单文件**: 平台根据上述信息,生成一个 JSON 格式的清单文件。
5. **数字签名 (未来扩展)**: 平台使用其私钥对**清单文件**的内容进行数字签名,并将签名添加到清单文件中。此步骤目前可跳过,但为未来安全性预留。
### 2.3. 清单文件 (Manifest File) 结构
清单文件是一个 JSON 对象,包含新固件的元数据和所有文件的详细信息。
```json
{
"version": "1.0.1", // 新固件版本号
"signature": "...", // 清单文件内容的数字签名 (未来扩展)
"files": [
{
"name": "manifest.json", // 清单文件本身
"path": "/manifest.json",
"md5": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"size": 1024
},
{
"name": "main.py",
"path": "/main.py",
"md5": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1",
"size": 10240
},
{
"name": "lib/sensor.py",
"path": "/lib/sensor.py",
"md5": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2",
"size": 5120
}
// ... 更多文件 (不包含配置文件)
]
}
```
## 3. 通信协议定义 (Protobuf Messages)
以下是 OTA 过程中平台与区域主控之间通信所需的 Protobuf 消息定义。
```protobuf
// OTA 升级指令和状态消息
// PrepareUpdateReq: 平台发送给设备,通知设备准备开始 OTA 升级
message PrepareUpdateReq {
string version = 1; // 新固件版本号
string task_id = 2; // 升级任务唯一ID
}
// RequestFile: 设备向平台请求特定文件 (包括清单文件和固件文件)
message RequestFile {
string task_id = 1; // 升级任务ID
string filename = 2; // 请求的文件名 (例如 "manifest.json" 或 "main.py")
string filepath = 3; // 请求的文件路径 (例如 "/manifest.json" 或 "/main.py")
uint32 retry_count = 4; // 设备请求该文件的重试次数
}
// FileResponse: 平台响应设备请求,发送单个文件的完整内容
// LoRa 传输层会自动处理分片和重组,因此应用层可以直接发送完整的单个文件内容
message FileResponse {
string task_id = 1; // 升级任务ID
string filename = 2; // 文件名 (例如 "manifest.json" 或 "main.py")
string filepath = 3; // 设备上的目标路径 (例如 "/manifest.json" 或 "/main.py")
bytes content = 4; // 文件的完整内容
// MD5 字段已从此处移除,设备将根据清单文件中的 MD5 进行统一校验
}
// UpdateStatusReport: 设备向平台报告升级状态
message UpdateStatusReport {
string device_id = 1; // 设备ID
string task_id = 2; // 升级任务ID
string current_version = 3; // 设备当前运行的固件版本
enum Status {
UNKNOWN = 0;
SUCCESS = 1; // 升级成功,新固件已运行
FAILED_PREPARE = 2; // 准备阶段失败 (如清空分区失败)
FAILED_FILE_REQUEST = 3; // 文件请求失败 (如平台未找到文件)
FAILED_FILE_RECEIVE = 4; // 文件接收失败 (如LoRa传输层错误)
FAILED_FILE_VERIFY = 5; // 文件MD5校验失败 (单个文件校验失败)
FAILED_MANIFEST_VERIFY = 6; // 清单文件验证失败 (如签名或格式错误)
FAILED_ACTIVATE = 7; // 激活失败 (如设置启动分区失败)
ROLLED_BACK = 8; // 新固件启动失败,已回滚到旧版本
IN_PROGRESS = 9; // 升级进行中 (可用于报告阶段性进度)
}
Status status = 4; // 升级状态
string error_message = 5; // 错误信息 (可选,用于详细说明失败原因)
string failed_file = 6; // 如果是文件相关失败,可包含文件名
}
```
## 4. 平台侧操作流程
### 4.1. 准备升级任务
1. 接收开发者提供的 MicroPython 项目压缩包。
2. 解压压缩包。
3. 遍历解压后的文件,计算每个文件的 MD5、大小并确定目标路径。
4. **排除配置文件**: 平台会识别配置文件(例如通过文件名约定),并**排除**这些文件,不将其包含在清单文件中,也不通过 OTA 传输。
5. 生成清单文件 (Manifest File)。**注意:清单文件本身也应作为 OTA 的一部分其元数据文件名、路径、MD5、大小应包含在清单文件自身的 `files` 列表中。Manifest文件生成后将被放在解压后的文件夹的根目录下, 方便后续主控设备获取**
6. (未来扩展)对清单文件进行数字签名。
7. 将清单文件和所有固件文件存储在平台内部,等待分发。
8. **记录 OTA 升级任务**: 在数据库中创建一条新的 OTA 升级任务记录(模型名为 `OTATask`,位于 `internal/infra/models/ota.go`),包含任务 ID、目标设备、新固件版本、状态例如“待开始”
### 4.2. 发送“准备更新”指令
1. 平台向目标区域主控发送 `PrepareUpdateReq` 消息。
2. 此消息通知设备即将进行 OTA 升级,并要求设备清空其非活动 OTA 分区。主控收到此指令并开始索要文件即表示准备完毕开始更新,平台记录此状态即可。
3. 更新任务记录: 平台根据设备开始索要清单文件的动作,更新 OTA 任务记录的状态。
### 4.3. 响应设备文件请求 (统一处理清单文件和固件文件)
1. 平台接收区域主控发送的 `RequestFile` 消息。
2. 平台根据 `task_id``filename``filepath` 在内部存储中找到对应的文件内容。
3. 平台构建 `FileResponse` 消息,将文件的完整内容、文件名和路径放入其中。
4. 平台通过 LoRa 传输层发送 `FileResponse` 消息。设备自己发现接收失败或超时会自行重发请求,多次失败设备会直接上报 `UpdateStatusReport` 结束更新。如果需要控制重试次数,可在平台发送的准备更新消息中带上重试次数。
5. 更新任务记录: 平台根据设备请求文件的动作,更新 OTA 任务记录中该文件的传输状态。
### 4.4. 处理设备状态上报
1. 平台接收区域主控发送的 `UpdateStatusReport` 消息。
2. 根据报告的状态,更新设备在平台上的固件版本和 OTA 任务记录的最终状态。
3. 如果报告失败或回滚,平台应记录错误信息,并可能触发告警或人工干预。
## 5. 区域主控侧操作流程 (MicroPython)
### 5.1. 接收“准备更新”指令
1. 区域主控接收 `PrepareUpdateReq` 消息。
2. 清空非活动分区: 使用 MicroPython 的文件系统操作(例如 `os.remove()``os.rmdir()`),递归删除非活动 OTA 分区(例如 `/ota_b`)下的所有文件和目录,为新固件腾出空间。
3. 设备准备就绪后,将直接开始请求清单文件,平台将通过设备请求清单文件的动作来判断设备已准备就绪。
### 5.2. 请求并验证清单文件
1. 设备完成准备后,向平台发送 `RequestFile` 消息,请求清单文件(例如 `filename: "manifest.json", filepath: "/manifest.json"`)。
2. 区域主控接收平台响应的 `FileResponse` 消息。
3. **写入非活动分区**: 将清单文件内容写入非活动分区(例如 `/ota_b/manifest.json`)。
4. **MD5 校验**: 计算接收到的清单文件的 MD5并与预期的 MD5如果设备有预置的清单文件 MD5 或通过其他安全方式获取)进行比对。**注意:由于清单文件本身也是通过 `RequestFile` 获取,其 MD5 校验的来源需要明确。最简单的方式是设备硬编码一个已知安全的清单文件 MD5或者依赖数字签名。**
5. **解析 JSON**: 解析清单文件内容,将其转换为 MicroPython 字典对象。
6. **数字签名验证 (未来扩展)**: 使用预置在设备中的平台公钥,验证清单文件的数字签名。如果签名验证失败,立即中止升级并报告错误。
7. 向平台发送 `UpdateStatusReport` 报告清单文件接收和验证结果。如果校验失败,设备应再次请求清单文件(并设置重试次数)。
### 5.3. 请求与存储固件文件 (逐文件校验)
1. 设备成功接收并验证清单文件后,根据清单文件中的文件列表,**逐个文件**地向平台发送 `RequestFile` 消息。
2. 对于每个请求的文件:
* 设备接收平台响应的 `FileResponse` 消息。
* **写入非活动分区**: 根据 `filepath` 字段,将 `content` 写入到 ESP32 的非活动 OTA 分区。需要确保目标目录存在,如果不存在则创建。
* 示例 (伪代码):
```python
import os
# 假设非活动分区挂载在 /ota_b
target_path = "/ota_b" + file_response.filepath
target_dir = os.path.dirname(target_path)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
with open(target_path, "wb") as f:
f.write(file_response.content)
```
* **MD5 校验**: 在文件写入完成后,计算该文件的 MD5 校验和。将计算出的 MD5 与清单文件中记录的 MD5 进行比对。
* MicroPython 的 `hashlib` 模块通常提供 MD5 算法。
* 如果校验失败或接收超时,设备应再次发送 `RequestFile` 消息请求该文件(并设置重试次数,例如连续三次失败则报告 `FAILED_FILE_VERIFY` 并中止升级)。平台不需等待每个文件的接收和校验状态报告。
### 5.4. 自激活与重启
1. **所有文件接收并校验成功后**,设备将自行执行以下操作:
* **配置 OTA 分区**: 使用 MicroPython 提供的 ESP-IDF OTA API通常通过 `esp` 模块或特定 OTA 模块),设置下一个启动分区为刚刚写入新固件的非活动分区。
* **自触发重启**: 在成功配置 OTA 分区后,区域主控自行触发重启。
### 5.5. 新版本启动与验证
1. 设备重启后,启动加载器会从新的 OTA 分区加载 MicroPython 固件。
2. **自检**: 新固件启动后,应执行必要的自检和健康检查,确保核心功能正常。
3. **标记有效**: 新固件在成功启动并完成自检后,必须调用相应的 MicroPython API例如 `esp.ota_mark_app_valid_cancel_rollback()`)来标记自身为有效,以防止自动回滚。
4. **版本上报**: 向平台发送 `UpdateStatusReport` 报告当前运行的版本号和升级成功状态。
5. **看门狗与回滚**:
* ESP-IDF 的 OTA 机制通常包含一个“启动计数器”或“验证机制”。如果新固件在一定次数的尝试后仍未标记自身为有效,启动加载器会自动回滚到上一个有效固件。
* 在 MicroPython 应用层,如果自检失败,不标记有效,以触发回滚。
### 5.6. 报告最终状态
1. 无论是成功升级到新版本还是回滚到旧版本,区域主控都应向平台发送 `UpdateStatusReport` 报告最终的升级状态。
## 6. 关键技术点与注意事项
### 6.1. LoRa 传输层
* 确保 `internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go` 实现的 LoRa 传输层能够稳定、可靠地处理大尺寸 Protobuf 消息的分片和重组。
* 注意 LoRa 传输的速率和可靠性,合理设置超时和重试机制。
### 6.2. 文件系统操作 (MicroPython)
* MicroPython 在 ESP32 上通常使用 LittleFS 或 FATFS。确保文件系统操作创建目录、写入文件、删除文件的正确性和鲁棒性。
* 清空非活动分区时,需要递归删除文件和目录。
* 注意文件系统空间管理,确保非活动分区有足够的空间接收新固件。
### 6.3. MD5 校验 (MicroPython)
* MicroPython 的 `hashlib` 模块通常提供 MD5 算法。确保在设备上计算 MD5 的效率和准确性。
### 6.4. OTA 分区管理 (MicroPython)
* 熟悉 ESP-IDF 的 OTA 机制在 MicroPython 中的绑定和使用方法。
* 正确调用 API 来设置下一个启动分区和标记当前应用为有效。
### 6.5. 回滚机制
* 依赖 ESP-IDF 提供的 OTA 回滚机制。新固件必须在启动后标记自身为有效,否则在多次重启后会自动回滚。
* 在 MicroPython 应用层,如果自检失败,不标记有效,以触发回滚。
### 6.6. 错误处理与重试
* 在平台和设备两侧,都需要实现完善的错误处理逻辑。
* 设备在请求文件时应包含重试次数,平台可以根据重试次数决定是否继续响应。
* 设备应能向平台准确报告错误类型和原因。
### 6.7. 安全性 (未来扩展)
* **数字签名**: 尽管目前暂时忽略密钥管理,但强烈建议在未来实现清单文件的数字签名。这将有效防止恶意固件注入和篡改。平台使用私钥签名,设备使用硬编码的公钥验证。
* **LoRaWAN 安全**: 确保 LoRaWAN 的网络层和应用层密钥管理得当,防止未经授权的设备加入网络或窃听数据。