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

15 KiB
Raw Blame History

区域主控 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 对象,包含新固件的元数据和所有文件的详细信息。

{
    "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 消息定义。

// 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_idfilenamefilepath 在内部存储中找到对应的文件内容。
  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 分区。需要确保目标目录存在,如果不存在则创建。
      • 示例 (伪代码):
        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 的网络层和应用层密钥管理得当,防止未经授权的设备加入网络或窃听数据。