diff --git a/design/ota-upgrade-and-log-monitoring/ota_upgrade_solution.md b/design/ota-upgrade-and-log-monitoring/ota_upgrade_solution.md index 9874633..4183727 100644 --- a/design/ota-upgrade-and-log-monitoring/ota_upgrade_solution.md +++ b/design/ota-upgrade-and-log-monitoring/ota_upgrade_solution.md @@ -8,40 +8,41 @@ ### 1.2. 核心思想 -* **AB 分区模式**: 区域主控采用 AB 分区模式,允许在设备运行时更新非活动分区,升级失败时可回滚到上一个已知的工作版本。 -* **平台主导**: 升级过程由平台完全控制,包括固件准备、文件分发和升级指令下发。 -* **LoRa 传输层自动分片**: 充分利用 LoRa 传输层自动分片和重组的能力,简化应用层协议设计。 -* **逐文件校验**: 设备在接收每个文件后立即进行 MD5 校验,确保文件完整性,并处理重试。 -* **清单文件**: 使用清单文件管理所有待更新文件的元数据和校验信息。 -* **设备自驱动**: 设备主动请求清单文件和固件文件,并在所有文件校验成功后自行激活新固件并重启。 -* **平台记录升级任务**: 平台将记录 OTA 升级任务的创建、进度和最终状态。 -* **配置文件独立管理**: OTA 升级过程将不涉及配置文件的更新,配置文件由平台提供独立的远程修改功能。 +* **AB 分区模式**: 区域主控采用 AB 分区模式,允许在设备运行时更新非活动分区,升级失败时可回滚到上一个已知的工作版本。 +* **平台主导**: 升级过程由平台完全控制,包括固件准备、文件分发和升级指令下发。 +* **LoRa 传输层自动分片**: 充分利用 LoRa 传输层自动分片和重组的能力,简化应用层协议设计。 +* **逐文件校验**: 设备在接收每个文件后立即进行 MD5 校验,确保文件完整性,并处理重试。 +* **清单文件**: 使用清单文件管理所有待更新文件的元数据和校验信息。 +* **设备自驱动**: 设备主动请求清单文件和固件文件,并在所有文件校验成功后自行激活新固件并重启。 +* **平台记录升级任务**: 平台将记录 OTA 升级任务的创建、进度和最终状态。 +* **配置文件独立管理**: OTA 升级过程将不涉及配置文件的更新,配置文件由平台提供独立的远程修改功能。 ### 1.3. 涉及组件 -* **平台**: 负责固件包管理、清单文件生成、数字签名(未来)、文件分发、指令下发、状态接收和**升级任务记录**。 -* **LoRa 传输层**: 负责应用层数据的分片、传输和重组。 -* **区域主控 (ESP32-S3-N16R8)**: 负责接收文件、存储到非活动分区、文件校验、分区切换、新固件启动验证和状态上报。 +* **平台**: 负责固件包管理、清单文件生成、数字签名(未来)、文件分发、指令下发、状态接收和**升级任务记录**。 +* **LoRa 传输层**: 负责应用层数据的分片、传输和重组。 +* **区域主控 (ESP32-S3-N16R8)**: 负责接收文件、存储到非活动分区、文件校验、分区切换、新固件启动验证和状态上报。 ## 2. 固件包结构与准备 ### 2.1. 原始固件包 (由开发者提供给平台) -* 一个标准的压缩包(例如 `.zip`),其中包含所有 MicroPython `.py` 文件、资源文件等。 -* 压缩包内的文件结构应与期望在设备上部署的路径结构一致。 +* 一个标准的压缩包(例如 `.zip`),其中包含所有 MicroPython `.py` 文件、资源文件等。 +* 压缩包内的文件结构应与期望在设备上部署的路径结构一致。 ### 2.2. 平台处理流程 -1. **接收**: 平台接收开发者上传的 MicroPython 项目压缩包。 -2. **解压**: 平台将该压缩包解压到内部的一个临时目录。 -3. **分析与生成清单**: 平台遍历解压后的所有文件,为每个文件计算: - * 文件名 (`name`) - * 在设备上的目标路径 (`path`) - * MD5 校验和 (`md5`) - * 文件大小 (`size`) - * **排除配置文件**: 平台会识别配置文件(例如通过文件名约定,如 `/config/` 目录下的所有文件),并**排除**这些文件,不将其包含在清单文件中,也不通过 OTA 传输。 -4. **生成清单文件**: 平台根据上述信息,生成一个 JSON 格式的清单文件。 -5. **数字签名 (未来扩展)**: 平台使用其私钥对**清单文件**的内容进行数字签名,并将签名添加到清单文件中。此步骤目前可跳过,但为未来安全性预留。 +1. **接收**: 平台接收开发者上传的 MicroPython 项目压缩包。 +2. **解压**: 平台将该压缩包解压到内部的一个临时目录。 +3. **分析与生成清单**: 平台遍历解压后的所有文件,为每个文件计算: + * 文件名 (`name`) + * 在设备上的目标路径 (`path`) + * MD5 校验和 (`md5`) + * 文件大小 (`size`) + * **排除配置文件**: 平台会识别配置文件(例如通过文件名约定,如 `/config/` 目录下的所有文件),并**排除** + 这些文件,不将其包含在清单文件中,也不通过 OTA 传输。 +4. **生成清单文件**: 平台根据上述信息,生成一个 JSON 格式的清单文件。 +5. **数字签名 (未来扩展)**: 平台使用其私钥对**清单文件**的内容进行数字签名,并将签名添加到清单文件中。此步骤目前可跳过,但为未来安全性预留。 ### 2.3. 清单文件 (Manifest File) 结构 @@ -49,29 +50,32 @@ ```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 - } - // ... 更多文件 (不包含配置文件) - ] + "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 + } + // ... 更多文件 (不包含配置文件) + ] } ``` @@ -84,48 +88,48 @@ // PrepareUpdateReq: 平台发送给设备,通知设备准备开始 OTA 升级 message PrepareUpdateReq { - string version = 1; // 新固件版本号 - string task_id = 2; // 升级任务唯一ID - string manifest_md5 = 3; // 清单文件的 MD5 校验和,用于设备初步校验清单文件完整性 + string version = 1; // 新固件版本号 + string task_id = 2; // 升级任务唯一ID + string manifest_md5 = 3; // 清单文件的 MD5 校验和,用于设备初步校验清单文件完整性 } // 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; // 设备请求该文件的重试次数 + 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; // 文件的完整内容 + string task_id = 1; // 升级任务ID + string filename = 2; // 文件名 (例如 "manifest.json" 或 "main.py") + string filepath = 3; // 设备上的目标路径 (例如 "/manifest.json" 或 "/main.py") + bytes content = 4; // 文件的完整内容 } // 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; // 清单文件验证失败 (如MD5不匹配,或格式错误) - FAILED_ACTIVATE = 7; // 激活失败 (如设置启动分区失败,或新固件自检失败) - ROLLED_BACK = 8; // 新固件启动失败,已回滚到旧版本 - IN_PROGRESS = 9; // 升级进行中 (可用于报告阶段性进度) - } - Status status = 4; // 升级状态 - string error_message = 5; // 错误信息 (可选,用于详细说明失败原因) - string failed_file = 6; // 如果是文件相关失败,可包含文件名 + 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; // 清单文件验证失败 (如MD5不匹配,或格式错误) + FAILED_ACTIVATE = 7; // 激活失败 (如设置启动分区失败,或新固件自检失败) + ROLLED_BACK = 8; // 新固件启动失败,已回滚到旧版本 + IN_PROGRESS = 9; // 升级进行中 (可用于报告阶段性进度) + } + Status status = 4; // 升级状态 + string error_message = 5; // 错误信息 (可选,用于详细说明失败原因) + string failed_file = 6; // 如果是文件相关失败,可包含文件名 } ``` @@ -133,279 +137,236 @@ message UpdateStatusReport { ### 4.1. 准备升级任务 -1. 接收开发者提供的 MicroPython 项目压缩包。 -2. 解压压缩包。 -3. 遍历解压后的文件,计算每个文件的 MD5、大小,并确定目标路径。 -4. **排除配置文件**: 平台会识别配置文件(例如通过文件名约定,如 `/config/` 目录下的所有文件),并**排除**这些文件,不将其包含在清单文件中,也不通过 OTA 传输。 -5. 生成清单文件 (Manifest File)。**注意:清单文件本身也应作为 OTA 的一部分,其元数据(文件名、路径、MD5、大小)应包含在清单文件自身的 `files` 列表中。Manifest文件生成后将被放在解压后的文件夹的根目录下, 方便后续主控设备获取** -6. (未来扩展)对清单文件进行数字签名。 -7. 将清单文件和所有固件文件存储在平台内部,等待分发。 -8. **记录 OTA 升级任务**: 在数据库中创建一条新的 OTA 升级任务记录(模型名为 `OTATask`,位于 `internal/infra/models/ota.go`),包含任务 ID、目标设备、新固件版本、状态(例如“待开始”)。 +1. 接收开发者提供的 MicroPython 项目压缩包。 +2. 解压压缩包。 +3. 遍历解压后的文件,计算每个文件的 MD5、大小,并确定目标路径。 +4. **排除配置文件**: 平台会识别配置文件(例如通过文件名约定,如 `/config/` 目录下的所有文件),并**排除** + 这些文件,不将其包含在清单文件中,也不通过 OTA 传输。 +5. 生成清单文件 (Manifest File)。**注意:清单文件本身也应作为 OTA + 的一部分,其元数据(文件名、路径、MD5、大小)应包含在清单文件自身的 `files` 列表中。Manifest文件生成后将被放在解压后的文件夹的根目录下, + 方便后续主控设备获取** +6. (未来扩展)对清单文件进行数字签名。 +7. 将清单文件和所有固件文件存储在平台内部,等待分发。 +8. **记录 OTA 升级任务**: 在数据库中创建一条新的 OTA 升级任务记录(模型名为 `OTATask`,位于 `internal/infra/models/ota.go` + ),包含任务 ID、目标设备、新固件版本、状态(例如“待开始”)。 ### 4.2. 发送“准备更新”指令 -1. 平台向目标区域主控发送 `PrepareUpdateReq` 消息,其中包含清单文件的 MD5 校验和。 -2. 此消息通知设备即将进行 OTA 升级,并要求设备清空其非活动 OTA 分区。 -3. **确认指令送达**: 平台发送 `PrepareUpdateReq` 后,启动一个定时器。如果在预设的超时时间内(例如 30 秒)未收到设备请求清单文件的 `RequestFile` 消息,平台可以重试发送 `PrepareUpdateReq`,重试次数可配置。 -4. **更新任务记录**: 平台根据设备开始索要清单文件的动作,更新 OTA 任务记录的状态为“进行中”。 +1. 平台向目标区域主控发送 `PrepareUpdateReq` 消息,其中包含清单文件的 MD5 校验和。 +2. 此消息通知设备即将进行 OTA 升级,并要求设备清空其非活动 OTA 分区。 +3. **确认指令送达**: 平台发送 `PrepareUpdateReq` 后,启动一个定时器。如果在预设的超时时间内(例如 30 秒)未收到设备请求清单文件的 + `RequestFile` 消息,平台可以重试发送 `PrepareUpdateReq`,重试次数可配置。 +4. **更新任务记录**: 平台根据设备开始索要清单文件的动作,更新 OTA 任务记录的状态为“进行中”。 ### 4.3. 响应设备文件请求 (统一处理清单文件和固件文件) -1. 平台接收区域主控发送的 `RequestFile` 消息。 -2. 平台根据 `task_id`、`filename` 和 `filepath` 在内部存储中找到对应的文件内容。 -3. 平台构建 `FileResponse` 消息,将文件的完整内容、文件名和路径放入其中。 -4. 平台通过 LoRa 传输层发送 `FileResponse` 消息。设备自己发现接收失败或超时会自行重发请求,多次失败设备会直接上报 `UpdateStatusReport` 结束更新。 -5. 更新任务记录: 平台根据设备请求文件的动作,更新 OTA 任务记录中该文件的传输状态。 +1. 平台接收区域主控发送的 `RequestFile` 消息。 +2. 平台根据 `task_id`、`filename` 和 `filepath` 在内部存储中找到对应的文件内容。 +3. 平台构建 `FileResponse` 消息,将文件的完整内容、文件名和路径放入其中。 +4. 平台通过 LoRa 传输层发送 `FileResponse` 消息。设备自己发现接收失败或超时会自行重发请求,多次失败设备会直接上报 + `UpdateStatusReport` 结束更新。 +5. 更新任务记录: 平台根据设备请求文件的动作,更新 OTA 任务记录中该文件的传输状态。 ### 4.4. 处理设备状态上报 -1. 平台接收区域主控发送的 `UpdateStatusReport` 消息。 -2. **总超时管理**: 平台为每个 OTA 任务设置一个总的超时时间(例如 2 小时)。如果在总超时时间内未能收到设备的最终状态报告(`SUCCESS`、`FAILED_XXX` 或 `ROLLED_BACK`),平台应自动将该任务标记为 `FAILED_TIMEOUT`。 -3. 根据报告的状态,更新设备在平台上的固件版本和 OTA 任务记录的最终状态。 -4. 如果报告失败或回滚,平台应记录错误信息,并可能触发告警或人工干预。 -5. **处理重复报告**: 平台在收到设备的最终状态报告后,即使后续再次收到相同的最终状态报告,也只需更新一次任务记录,无需重复处理。 +1. 平台接收区域主控发送的 `UpdateStatusReport` 消息。 +2. **总超时管理**: 平台为每个 OTA 任务设置一个总的超时时间(例如 2 小时)。如果在总超时时间内未能收到设备的最终状态报告( + `SUCCESS`、`FAILED_XXX` 或 `ROLLED_BACK`),平台应自动将该任务标记为 `FAILED_TIMEOUT`。 +3. 根据报告的状态,更新设备在平台上的固件版本和 OTA 任务记录的最终状态。 +4. 如果报告失败或回滚,平台应记录错误信息,并可能触发告警或人工干预。 +5. **处理重复报告**: 平台在收到设备的最终状态报告后,即使后续再次收到相同的最终状态报告,也只需更新一次任务记录,无需重复处理。 ## 5. 区域主控侧操作流程 (MicroPython) ### 5.1. 接收“准备更新”指令 -1. 区域主控接收 `PrepareUpdateReq` 消息。 -2. 清空非活动分区: 使用 MicroPython 的文件系统操作(例如 `os.remove()` 和 `os.rmdir()`),递归删除非活动 OTA 分区(例如 `/ota_b`)下的所有文件和目录,为新固件腾出空间。 - * **错误处理**: 在清空分区过程中,如果遇到文件系统错误(例如文件被占用、目录无法删除),设备应立即中止准备,并向平台发送 `UpdateStatusReport`,状态为 `FAILED_PREPARE`,并在 `error_message` 中包含详细的错误信息。 -3. 设备准备就绪后,将直接开始请求清单文件,平台将通过设备请求清单文件的动作来判断设备已准备就绪。 +1. 区域主控接收 `PrepareUpdateReq` 消息。 +2. 清空非活动分区: 使用 MicroPython 的文件系统操作(例如 `os.remove()` 和 `os.rmdir()`),递归删除非活动 OTA 分区(例如 + `/ota_b`)下的所有文件和目录,为新固件腾出空间。 + * **错误处理**: 在清空分区过程中,如果遇到文件系统错误(例如文件被占用、目录无法删除),设备应立即中止准备,并向平台发送 + `UpdateStatusReport`,状态为 `FAILED_PREPARE`,并在 `error_message` 中包含详细的错误信息。 +3. 设备准备就绪后,将直接开始请求清单文件,平台将通过设备请求清单文件的动作来判断设备已准备就绪。 ### 5.2. 请求并验证清单文件 -1. 设备完成准备后,向平台发送 `RequestFile` 消息,请求清单文件(例如 `filename: "manifest.json", filepath: "/manifest.json"`)。 - * **请求超时与重试**: 设备发送 `RequestFile` 后,启动一个定时器。如果在预设的超时时间内(例如 30 秒)没有收到 `FileResponse`,则认为传输失败,并进行重试。设备应为清单文件请求设置最大重试次数(例如 5 次)。如果达到最大重试次数仍未成功,则上报 `FAILED_FILE_RECEIVE` 并中止 OTA 任务。 -2. 区域主控接收平台响应的 `FileResponse` 消息。 -3. **写入非活动分区**: 将清单文件内容写入非活动分区(例如 `/ota_b/manifest.json`)。 - * **错误处理**: 如果文件写入失败,设备应立即中止升级,并向平台发送 `UpdateStatusReport`,状态为 `FAILED_FILE_RECEIVE`,并在 `error_message` 中包含详细的错误信息。 -4. **MD5 校验**: 计算写入非活动分区的清单文件的 MD5,并与 `PrepareUpdateReq` 消息中提供的 `manifest_md5` 进行比对。 -5. **解析 JSON**: 解析清单文件内容,将其转换为 MicroPython 字典对象。 -6. **数字签名验证 (未来扩展)**: 使用预置在设备中的平台公钥,验证清单文件的数字签名。如果签名验证失败,立即中止升级并报告错误。 -7. 如果上述任何校验或解析失败,设备应向平台发送 `UpdateStatusReport` 报告 `FAILED_MANIFEST_VERIFY`,并在 `error_message` 中说明原因,然后中止升级。 +1. 设备完成准备后,向平台发送 `RequestFile` 消息,请求清单文件(例如 + `filename: "manifest.json", filepath: "/manifest.json"`)。 + * **请求超时与重试**: 设备发送 `RequestFile` 后,启动一个定时器。如果在预设的超时时间内(例如 30 秒)没有收到 + `FileResponse`,则认为传输失败,并进行重试。设备应为清单文件请求设置最大重试次数(例如 5 次)。如果达到最大重试次数仍未成功,则上报 + `FAILED_FILE_RECEIVE` 并中止 OTA 任务。 +2. 区域主控接收平台响应的 `FileResponse` 消息。 +3. **写入非活动分区**: 将清单文件内容写入非活动分区(例如 `/ota_b/manifest.json`)。 + * **错误处理**: 如果文件写入失败,设备应立即中止升级,并向平台发送 `UpdateStatusReport`,状态为 `FAILED_FILE_RECEIVE` + ,并在 `error_message` 中包含详细的错误信息。 +4. **MD5 校验**: 计算写入非活动分区的清单文件的 MD5,并与 `PrepareUpdateReq` 消息中提供的 `manifest_md5` 进行比对。 +5. **解析 JSON**: 解析清单文件内容,将其转换为 MicroPython 字典对象。 +6. **数字签名验证 (未来扩展)**: 使用预置在设备中的平台公钥,验证清单文件的数字签名。如果签名验证失败,立即中止升级并报告错误。 +7. 如果上述任何校验或解析失败,设备应向平台发送 `UpdateStatusReport` 报告 `FAILED_MANIFEST_VERIFY`,并在 `error_message` + 中说明原因,然后中止升级。 ### 5.3. 请求与存储固件文件 (逐文件校验) -1. 设备成功接收并验证清单文件后,根据清单文件中的文件列表,**逐个文件**地向平台发送 `RequestFile` 消息。 -2. 对于每个请求的文件: - * **请求超时与重试**: 设备发送 `RequestFile` 后,启动一个定时器。如果在预设的超时时间内(例如 30 秒)没有收到 `FileResponse`,则认为传输失败,并进行重试。设备应为每个文件的请求设置最大重试次数(例如 5 次)。如果达到最大重试次数仍未成功,则上报 `FAILED_FILE_RECEIVE` 并中止当前文件下载,进而中止整个 OTA 任务。 - * 设备接收平台响应的 `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) # 错误处理:如果创建目录失败,应上报 FAILED_FILE_RECEIVE - with open(target_path, "wb") as f: - f.write(file_response.content) # 错误处理:如果写入失败,应上报 FAILED_FILE_RECEIVE - ``` - * **错误处理**: 如果文件写入失败,设备应立即中止升级,并向平台发送 `UpdateStatusReport`,状态为 `FAILED_FILE_RECEIVE`,并在 `error_message` 中包含详细的错误信息。 - * **MD5 校验**: 在文件写入完成后,计算该文件的 MD5 校验和。将计算出的 MD5 与清单文件中记录的 MD5 进行比对。 - * MicroPython 的 `hashlib` 模块通常提供 MD5 算法。 - * 如果 MD5 校验失败,设备应再次发送 `RequestFile` 消息请求该文件(并设置重试次数,例如连续三次失败则报告 `FAILED_FILE_VERIFY` 并中止升级)。平台不需等待每个文件的接收和校验状态报告。 +1. 设备成功接收并验证清单文件后,根据清单文件中的文件列表,**逐个文件**地向平台发送 `RequestFile` 消息。 +2. 对于每个请求的文件: + * **请求超时与重试**: 设备发送 `RequestFile` 后,启动一个定时器。如果在预设的超时时间内(例如 30 秒)没有收到 + `FileResponse`,则认为传输失败,并进行重试。设备应为每个文件的请求设置最大重试次数(例如 5 次)。如果达到最大重试次数仍未成功,则上报 + `FAILED_FILE_RECEIVE` 并中止当前文件下载,进而中止整个 OTA 任务。 + * 设备接收平台响应的 `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) # 错误处理:如果创建目录失败,应上报 FAILED_FILE_RECEIVE + with open(target_path, "wb") as f: + f.write(file_response.content) # 错误处理:如果写入失败,应上报 FAILED_FILE_RECEIVE + ``` + * **错误处理**: 如果文件写入失败,设备应立即中止升级,并向平台发送 `UpdateStatusReport`,状态为 + `FAILED_FILE_RECEIVE`,并在 `error_message` 中包含详细的错误信息。 + * **MD5 校验**: 在文件写入完成后,计算该文件的 MD5 校验和。将计算出的 MD5 与清单文件中记录的 MD5 进行比对。 + * MicroPython 的 `hashlib` 模块通常提供 MD5 算法。 + * 如果 MD5 校验失败,设备应再次发送 `RequestFile` 消息请求该文件(并设置重试次数,例如连续三次失败则报告 + `FAILED_FILE_VERIFY` 并中止升级)。平台不需等待每个文件的接收和校验状态报告。 ### 5.4. 自激活与重启 -1. **所有文件接收并校验成功后**,设备将自行执行以下操作: - * **配置 OTA 分区**: 使用 MicroPython 提供的 ESP-IDF OTA API(通常通过 `esp` 模块或特定 OTA 模块),设置下一个启动分区为刚刚写入新固件的非活动分区。 - * **自触发重启**: 在成功配置 OTA 分区后,区域主控自行触发重启。 +1. **所有文件接收并校验成功后**,设备将自行执行以下操作: + * **配置 OTA 分区**: 使用 MicroPython 提供的 ESP-IDF OTA API(通常通过 `esp` 模块或特定 OTA + 模块),设置下一个启动分区为刚刚写入新固件的非活动分区。 + * **自触发重启**: 在成功配置 OTA 分区后,区域主控自行触发重启。 ### 5.5. 新版本启动与验证 -1. 设备重启后,启动加载器会从新的 OTA 分区加载 MicroPython 固件。 -2. **自检**: 新固件启动后,应执行必要的自检和健康检查,确保核心功能正常。这包括但不限于:LoRa 模块初始化、关键传感器读取、网络连接测试、核心业务逻辑初始化等。 -3. **标记有效**: 只有当所有自检项都成功通过后,新固件才必须调用相应的 MicroPython API(例如 `esp.ota_mark_app_valid_cancel_rollback()`)来标记自身为有效,以防止自动回滚。 -4. **版本上报**: 向平台发送 `UpdateStatusReport` 报告当前运行的版本号和升级成功状态。 -5. **看门狗与回滚**: - * ESP-IDF 的 OTA 机制通常包含一个“启动计数器”或“验证机制”。如果新固件在一定次数的尝试后仍未标记自身为有效,启动加载器会自动回滚到上一个有效固件。 - * 在 MicroPython 应用层,如果自检失败,**绝不能**标记自身为有效。设备应等待看门狗超时或系统自动重启,让 ESP-IDF 的底层 OTA 机制自动触发回滚到上一个有效固件。 +1. 设备重启后,启动加载器会从新的 OTA 分区加载 MicroPython 固件。 +2. **自检**: 新固件启动后,应执行必要的自检和健康检查,确保核心功能正常。这包括但不限于:LoRa + 模块初始化、关键传感器读取、网络连接测试、核心业务逻辑初始化等。 +3. **标记有效**: 只有当所有自检项都成功通过后,新固件才必须调用相应的 MicroPython API(例如 + `esp.ota_mark_app_valid_cancel_rollback()`)来标记自身为有效,以防止自动回滚。 +4. **版本上报**: 向平台发送 `UpdateStatusReport` 报告当前运行的版本号和升级成功状态。 +5. **看门狗与回滚**: + * ESP-IDF 的 OTA 机制通常包含一个“启动计数器”或“验证机制”。如果新固件在一定次数的尝试后仍未标记自身为有效,启动加载器会自动回滚到上一个有效固件。 + * 在 MicroPython 应用层,如果自检失败,**绝不能**标记自身为有效。设备应等待看门狗超时或系统自动重启,让 ESP-IDF 的底层 + OTA 机制自动触发回滚到上一个有效固件。 ### 5.6. 报告最终状态 -1. 无论是成功升级到新版本还是回滚到旧版本,区域主控都应向平台发送 `UpdateStatusReport` 报告最终的升级状态。 -2. **重复发送最终状态**: 为了提高在单向 LoRa 通信中平台接收到最终状态报告的可靠性,设备在发送最终的 `UpdateStatusReport` (无论是 `SUCCESS`、`FAILED_XXX` 还是 `ROLLED_BACK`) 时,应在短时间内(例如,间隔几秒)**重复发送该报告多次**(例如 3-5 次)。 +1. 无论是成功升级到新版本还是回滚到旧版本,区域主控都应向平台发送 `UpdateStatusReport` 报告最终的升级状态。 +2. **重复发送最终状态**: 为了提高在单向 LoRa 通信中平台接收到最终状态报告的可靠性,设备在发送最终的 + `UpdateStatusReport` (无论是 `SUCCESS`、`FAILED_XXX` 还是 `ROLLED_BACK`) 时,应在短时间内(例如,间隔几秒)**重复发送该报告多次 + **(例如 3-5 次)。 ## 6. 关键技术点与注意事项 ### 6.1. LoRa 传输层 -* 确保 `internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go` 实现的 LoRa 传输层能够稳定、可靠地处理大尺寸 Protobuf 消息的分片和重组。 -* 注意 LoRa 传输的速率和可靠性,合理设置超时和重试机制。 +* 确保 `internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go` 实现的 LoRa 传输层能够稳定、可靠地处理大尺寸 + Protobuf 消息的分片和重组。 +* 注意 LoRa 传输的速率和可靠性,合理设置超时和重试机制。 ### 6.2. 平台侧的请求处理 -* `internal/app/listener/lora_listener.go` 模块在接收到设备发来的 `RequestFile` 消息时,需要高效地处理。这可能涉及到快速查询数据库以获取文件内容,或者通过回调机制将请求转发给 OTA 任务管理器进行处理,以避免阻塞 LoRa 监听器并确保及时响应设备请求。 +* `internal/app/listener/lora_listener.go` 模块在接收到设备发来的 `RequestFile` + 消息时,需要高效地处理。这可能涉及到快速查询数据库以获取文件内容,或者通过回调机制将请求转发给 OTA 任务管理器进行处理,以避免阻塞 + LoRa 监听器并确保及时响应设备请求。 ### 6.3. 文件系统操作 (MicroPython) -* MicroPython 在 ESP32 上通常使用 LittleFS 或 FATFS。确保文件系统操作(创建目录、写入文件、删除文件)的正确性和鲁棒性。 -* 清空非活动分区时,需要递归删除文件和目录,并对可能出现的错误进行捕获和报告。 -* 在创建目录和写入文件时,也应进行错误捕获,并在失败时上报详细错误信息。 +* MicroPython 在 ESP32 上通常使用 LittleFS 或 FATFS。确保文件系统操作(创建目录、写入文件、删除文件)的正确性和鲁棒性。 +* 清空非活动分区时,需要递归删除文件和目录,并对可能出现的错误进行捕获和报告。 +* 在创建目录和写入文件时,也应进行错误捕获,并在失败时上报详细错误信息。 ### 6.4. MD5 校验 (MicroPython) -* MicroPython 的 `hashlib` 模块通常提供 MD5 算法。确保在设备上计算 MD5 的效率和准确性。 -* 设备将依赖 `PrepareUpdateReq` 中的 `manifest_md5` 对清单文件进行校验,并依赖清单文件中记录的 MD5 对所有固件文件进行校验。 +* MicroPython 的 `hashlib` 模块通常提供 MD5 算法。确保在设备上计算 MD5 的效率和准确性。 +* 设备将依赖 `PrepareUpdateReq` 中的 `manifest_md5` 对清单文件进行校验,并依赖清单文件中记录的 MD5 对所有固件文件进行校验。 ### 6.5. OTA 分区管理 (MicroPython) -* 熟悉 ESP-IDF 的 OTA 机制在 MicroPython 中的绑定和使用方法。 -* 正确调用 API 来设置下一个启动分区和标记当前应用为有效。 -* 确保在自检失败时,**不**调用标记有效的 API,以触发回滚机制。 +* 熟悉 ESP-IDF 的 OTA 机制在 MicroPython 中的绑定和使用方法。 +* 正确调用 API 来设置下一个启动分区和标记当前应用为有效。 +* 确保在自检失败时,**不**调用标记有效的 API,以触发回滚机制。 ### 6.6. 回滚机制 -* 依赖 ESP-IDF 提供的 OTA 回滚机制。新固件必须在启动后标记自身为有效,否则在多次重启后会自动回滚。 -* 在 MicroPython 应用层,如果自检失败,不标记有效,以触发回滚。 +* 依赖 ESP-IDF 提供的 OTA 回滚机制。新固件必须在启动后标记自身为有效,否则在多次重启后会自动回滚。 +* 在 MicroPython 应用层,如果自检失败,不标记有效,以触发回滚。 ### 6.7. 错误处理与重试 -* 在平台和设备两侧,都需要实现完善的错误处理逻辑。 -* 设备在请求文件时应包含重试次数,平台可以根据重试次数决定是否继续响应。 -* 设备应能向平台准确报告错误类型和原因,包括文件系统操作失败、MD5 校验失败等。 -* 平台应具备对 OTA 任务的总超时管理能力。 +* 在平台和设备两侧,都需要实现完善的错误处理逻辑。 +* 设备在请求文件时应包含重试次数,平台可以根据重试次数决定是否继续响应。 +* 设备应能向平台准确报告错误类型和原因,包括文件系统操作失败、MD5 校验失败等。 +* 平台应具备对 OTA 任务的总超时管理能力。 ### 6.8. 安全性 (未来扩展) -* **数字签名**: 尽管目前暂时忽略密钥管理,但强烈建议在未来实现清单文件的数字签名。这将有效防止恶意固件注入和篡改。平台使用私钥签名,设备使用硬编码的公钥验证。 -* **LoRaWAN 安全**: 确保 LoRaWAN 的网络层和应用层密钥管理得当, 防止未经授权的设备加入网络或窃听数据。 +* **数字签名**: 尽管目前暂时忽略密钥管理,但强烈建议在未来实现清单文件的数字签名。这将有效防止恶意固件注入和篡改。平台使用私钥签名,设备使用硬编码的公钥验证。 +* **LoRaWAN 安全**: 确保 LoRaWAN 的网络层和应用层密钥管理得当, 防止未经授权的设备加入网络或窃听数据。 --- -## 7. OTA 升级流程时序图 +## 7. 固件 OTA 升级流程描述 -```mermaid -sequenceDiagram - participant User as 开发者 - participant Platform as 平台 - participant Device as 区域主控 +整个固件 OTA(Over-The-Air)升级流程涉及三个主要参与者:**开发者 (User)**、**平台 (Platform)** 和 **区域主控设备 (Device)**。 - User->>Platform: 上传固件包 (.zip) - activate Platform - Platform->>Platform: 解压固件包,分析文件,排除配置文件 - Platform->>Platform: 计算文件MD5,生成清单文件 (manifest.json) - Platform->>Platform: 存储固件文件和清单文件 - Platform->>Platform: 记录OTA升级任务 (状态: 待开始) - deactivate Platform +### 阶段一:任务准备(开发者与平台) - Platform->>Device: PrepareUpdateReq (version, task_id, manifest_md5) - activate Platform - Platform->>Platform: 启动定时器 (等待设备请求清单文件) - activate Device - Device->>Device: 接收 PrepareUpdateReq - Device->>Device: 清空非活动OTA分区 (如 /ota_b) - alt 清空分区失败 - Device->>Platform: UpdateStatusReport (FAILED_PREPARE, error_message) (重复发送多次) - deactivate Device - Platform->>Platform: 更新任务状态为 FAILED_PREPARE - deactivate Platform - break - end - Device->>Platform: RequestFile (task_id, filename: "manifest.json", filepath: "/manifest.json", retry_count: 0) - deactivate Device +1. **上传固件包 (User -> Platform)**: + * 开发者上传固件包(.zip 文件)。 + * 平台接收固件包,解压,分析文件,排除配置文件。 + * 平台计算所有文件MD5,生成清单文件 (manifest.json)。 + * 平台存储固件文件和清单文件,并记录 OTA 升级任务 (状态: 待开始)。 - activate Platform - Platform->>Platform: 收到 RequestFile,更新任务状态 (状态: 进行中) - Platform->>Platform: 查找 manifest.json 内容 - Platform->>Device: FileResponse (task_id, filename: "manifest.json", filepath: "/manifest.json", content) - deactivate Platform +### 阶段二:设备接收并请求清单文件 - activate Device - Device->>Device: 接收 FileResponse (manifest.json) - Device->>Device: 将内容写入非活动分区 (/ota_b/manifest.json) - alt 写入失败 - Device->>Platform: UpdateStatusReport (FAILED_FILE_RECEIVE, error_message) (重复发送多次) - deactivate Device - Platform->>Platform: 更新任务状态为 FAILED_FILE_RECEIVE - deactivate Platform - break - end - Device->>Device: 计算写入文件的MD5 - Device->>Device: 比对 MD5 (与 PrepareUpdateReq.manifest_md5) - alt MD5不匹配 或 JSON解析失败 - Device->>Platform: UpdateStatusReport (FAILED_MANIFEST_VERIFY, error_message) (重复发送多次) - deactivate Device - Platform->>Platform: 更新任务状态为 FAILED_MANIFEST_VERIFY - deactivate Platform - break - end - Device->>Device: 解析 manifest.json,获取文件列表 - deactivate Device +1. **下发更新通知 (Platform -> Device)**: + * 平台向设备发送 `PrepareUpdateReq` (包含 version, task_id, manifest_md5)。 +2. **设备准备 (Device)**: + * 设备接收请求,并尝试清空**非活动 OTA 分区**(如 /ota_b)。 + * **失败分支:** 如果清空分区失败,设备报告 `UpdateStatusReport` (FAILED_PREPARE),平台更新任务状态为 FAILED_PREPARE。 + * **成功分支:** 设备向平台发送 `RequestFile` (filename: "manifest.json")。 +3. **清单文件传输 (Platform <-> Device)**: + * 平台收到请求,更新任务状态为进行中,并发送 `FileResponse` (manifest.json) 给设备。 + * 设备写入清单文件。 +4. **校验清单文件 (Device)**: + * **失败分支 1 (写入失败):** 报告 `UpdateStatusReport` (FAILED_FILE_RECEIVE)。 + * **失败分支 2 (校验失败):** 计算 MD5 与 `PrepareUpdateReq` 的 MD5 不匹配,或 JSON 解析失败,报告 + `UpdateStatusReport` (FAILED_MANIFEST_VERIFY)。 + * **成功分支:** 设备解析清单文件,获取文件列表。 - loop For each file in manifest - activate Device - Device->>Platform: RequestFile (task_id, filename: "file_X.py", filepath: "/path/to/file_X.py", retry_count) - Device->>Device: 启动定时器 (等待 FileResponse) - deactivate Device +### 阶段三:文件循环下载和校验(核心 OTA 过程) - activate Platform - Platform->>Platform: 收到 RequestFile,更新文件传输状态 - Platform->>Platform: 查找 file_X.py 内容 - Platform->>Device: FileResponse (task_id, filename: "file_X.py", filepath: "/path/to/file_X.py", content) - deactivate Platform +设备循环请求清单中的每一个固件文件: - activate Device - Device->>Device: 接收 FileResponse (file_X.py) - Device->>Device: 确保目标目录存在,将内容写入非活动分区 (/ota_b/path/to/file_X.py) - alt 写入失败 - Device->>Platform: UpdateStatusReport (FAILED_FILE_RECEIVE, error_message) (重复发送多次) - deactivate Device - Platform->>Platform: 更新任务状态为 FAILED_FILE_RECEIVE - deactivate Platform - break loop - end - Device->>Device: 计算写入文件的MD5 - Device->>Device: 比对 MD5 (与 manifest.files[X].md5) - alt MD5不匹配 或 超时未收到 FileResponse (达到最大重试次数) - Device->>Device: 增加 retry_count - alt 达到最大重试次数 - Device->>Platform: UpdateStatusReport (FAILED_FILE_VERIFY/FAILED_FILE_RECEIVE, error_message) (重复发送多次) - deactivate Device - Platform->>Platform: 更新任务状态为 FAILED_FILE_VERIFY/FAILED_FILE_RECEIVE - deactivate Platform - break loop - else - Device->>Platform: RequestFile (重试请求该文件) - end - end - deactivate Device - end +1. **文件请求与响应 (Device <-> Platform)**: + * **循环开始:** 设备发送 `RequestFile` (filename: "file_X.py")。 + * 平台响应 `FileResponse` (file_X.py)。 +2. **写入与校验 (Device)**: + * 设备接收文件,确保目录存在,写入非活动分区,并计算写入文件的 MD5。 + * **失败分支 1 (写入失败):** 报告 `FAILED_FILE_RECEIVE`,中断下载循环。 + * **失败分支 2 (校验失败/超时):** + * 设备增加 `retry_count`。 + * **达到最大重试次数:** 报告失败 (`FAILED_FILE_VERIFY`/`FAILED_FILE_RECEIVE`),中断下载循环。 + * **未达最大重试次数:** 重置定时器,重试发送 `RequestFile`。 - activate Device - Device->>Device: 所有文件下载并校验成功 - Device->>Device: 配置OTA分区为新固件分区 - Device->>Device: 自触发重启 - deactivate Device +### 阶段四:激活与最终状态(重启与回滚) - Device-->>Device: (设备重启,加载新固件) +1. **激活准备 (Device)**: + * 所有文件下载并校验成功后,设备配置 OTA 分区为新固件分区,并自触发重启。 +2. **新固件自检 (Device)**: + * 设备重启,加载新固件,执行自检。 + * **成功分支:** + * 设备标记自身为有效 (`esp.ota_mark_app_valid_cancel_rollback()`)。 + * 设备报告 `UpdateStatusReport` (SUCCESS, current_version)。 + * 平台更新任务状态为 SUCCESS,更新设备固件版本。 + * **失败分支:** + * 设备不标记自身为有效,报告 `UpdateStatusReport` (FAILED_ACTIVATE)。 + * 设备等待看门狗超时或系统自动回滚到旧固件。 + * 设备报告 `UpdateStatusReport` (ROLLED_BACK, current_version: 旧版本)。 + * 平台更新任务状态为 ROLLED_BACK,更新设备固件版本为旧版本。 - activate Device - Device->>Device: 新固件启动,执行自检 - alt 自检成功 - Device->>Device: 标记自身为有效 (esp.ota_mark_app_valid_cancel_rollback()) - Device->>Platform: UpdateStatusReport (SUCCESS, current_version) (重复发送多次) - deactivate Device - activate Platform - Platform->>Platform: 更新任务状态为 SUCCESS - Platform->>Platform: 更新设备固件版本 - deactivate Platform - else 自检失败 - Device->>Device: 不标记自身为有效 - Device->>Platform: UpdateStatusReport (FAILED_ACTIVATE, error_message) (重复发送多次) - Device-->>Device: (等待看门狗超时或系统自动回滚) - Device-->>Device: (回滚到旧固件) - Device->>Platform: UpdateStatusReport (ROLLED_BACK, current_version: 旧版本) (重复发送多次) - deactivate Device - - activate Platform - Platform->>Platform: 更新任务状态为 ROLLED_BACK - Platform->>Platform: 更新设备固件版本为旧版本 - deactivate Platform - end - Platform->>Platform: 总超时检查 (如果长时间未收到最终状态,则标记为 FAILED_TIMEOUT) -``` \ No newline at end of file +3. **总超时检查 (Platform)**: + * 如果平台长时间未收到最终状态,则标记任务状态为 FAILED_TIMEOUT。 \ No newline at end of file