Files
ESP32_COMM/main/relay/ota_relay.h
T

421 lines
14 KiB
C++
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.
/*
* ota_relay.h - OTA 中继核心状态机
* T2.3, T3.1 - T3.7
*
* OtaRelay 类封装 OTA 中继状态机、Flash 存储和元数据管理。
*/
#ifndef OTA_RELAY_H
#define OTA_RELAY_H
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "transport/protocol.h"
#include "transport/spi_master.h"
#include "esp_partition.h"
#define OTA_META_NVS_NAMESPACE "ota_meta"
typedef struct {
uint32_t total_size;
uint32_t stored_size;
uint32_t last_seq;
uint8_t checksum[32];
} ota_meta_t;
typedef enum {
RELAY_IDLE = 0,
RELAY_HANDSHAKE_SENT,
RELAY_CHALLENGE_SENT,
RELAY_CHALLENGE_RESPOND,
RELAY_SESSION_KEY_SENT,
RELAY_AUTH_COMPLETE,
RELAY_SIZE_SENT,
RELAY_SIZE_ACK,
RELAY_RELAY_ACTIVE,
RELAY_PACKET_SENT,
RELAY_PACKET_ACK_OK,
RELAY_PACKET_ACK_RETRY,
RELAY_RECEIVE_DONE,
RELAY_FINISH_SENT,
RELAY_DONE,
RELAY_APP_VERIFY,
RELAY_ERROR = 0xFF
} relay_state_t;
typedef enum {
OTA_RESULT_OK = 0,
OTA_RESULT_ERR_TIMEOUT = -1,
OTA_RESULT_ERR_CRC = -2,
OTA_RESULT_ERR_AUTH = -3,
OTA_RESULT_ERR_STORAGE = -4,
OTA_RESULT_ERR_GD32 = -5,
OTA_RESULT_ERR_HEALTH = -6,
OTA_RESULT_ERR_USER_ABORT = -7
} ota_result_t;
class OtaRelay {
public:
/**
* @brief 获取OtaRelay单例引用.
* @return OtaRelay单例引用.
*/
static OtaRelay& instance();
/**
* @brief OTA中继FreeRTOS任务入口.
* @param pvParameters FreeRTOS任务参数.
*/
static void taskEntry(void *pvParameters);
/**
* @brief 从Flash向GD32转发固件(主流程).
* @return 操作结果码.
*/
ota_result_t forwardFirmware();
/* ---- 线程安全的状态访问接口 ----
* OTA 任务写、HTTP 任务读,通过 mutex 保证一致性。
* 状态机内部(OTA 任务)可直接写 static 成员,
* 跨任务读/写必须走这些 API。
*/
/**
* @brief 获取当前中继状态.
* @return 当前中继状态.
*/
static relay_state_t getState();
/**
* @brief 获取中继当前序号.
* @return 当前序号值.
*/
static uint32_t getRelaySeq();
/**
* @brief 获取中继总包数.
* @return 总包数.
*/
static uint32_t getRelayTotalPackets();
/**
* @brief 获取GD32 APP存活标志位.
* @return true 存活,false 未存活.
*/
static bool getGd32AppAlive();
/**
* @brief 获取GD32固件版本.
* @param out 输出参数,版本结构体指针.
*/
static void getGd32Version(gd32_version_t *out);
/**
* @brief 设置中继状态.
* @param s 目标状态值.
*/
static void setState(relay_state_t s);
/**
* @brief 设置中继当前序号.
* @param seq 序号值.
*/
static void setRelaySeq(uint32_t seq);
/**
* @brief 设置中继总包数.
* @param total 总包数.
*/
static void setRelayTotalPackets(uint32_t total);
/**
* @brief 设置GD32 APP存活标志位.
* @param alive 存活状态.
*/
static void setGd32AppAlive(bool alive);
/**
* @brief 获取GD32固件版本.
* @param ver 待设置的版本结构体引用.
*/
static void setGd32Version(const gd32_version_t &ver);
/**
* @brief 重置状态机.
*/
void reset();
/**
* @brief Flash写入接口(支持非4字节对齐写入).
* @param offset 分区内写入偏移.
* @param data 待写入的数据指针.
* @param len 数据长度(字节).
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
/**
* @brief 从OTA缓存分区读取数据块.
* @param offset 分区内读取偏移.
* @param data 读取缓冲区指针.
* @param len 读取长度(字节).
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
/**
* @brief 从Flash加载元数据.
* @param meta 输出参数,加载后的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaLoad(ota_meta_t *meta);
/**
* @brief 保存元数据到Flash.
* @param meta 待保存的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaSave(const ota_meta_t *meta);
/**
* @brief 传输过程中更新元数据.
* @param stored_size 已存储的固件大小(字节).
* @param last_seq 最后成功的分块序号.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
/**
* @brief OTA完成或中止时清除元数据.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaClear();
/**
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
* @param out_sha256 输出32字节SHA-256摘要.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
/**
* @brief 固件接收完毕后计算SHA-256并写入元数据.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaFinalizeChecksum();
/**
* @brief 校验固件完整性(SHA-256).
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败.
*/
static esp_err_t metaVerifyChecksum();
// 公开状态(状态机内部直接访问;跨任务请用 getter/setter)
static relay_state_t relayState;
static uint32_t relaySeq;
static uint32_t relayTotalPackets;
static volatile bool gd32AppAlive;
static gd32_version_t gd32Version;
static QueueHandle_t dispatchQueue;
static QueueHandle_t resultQueue;
// 线程安全锁
static SemaphoreHandle_t s_stateMutex;
/**
* @brief 初始化状态互斥锁.
*/
static void initMutex();
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50);
class StateLockGuard {
public:
/**
* @brief 构造时获取互斥锁.
* @param timeout 等待超时(时钟节拍),默认50ms.
*/
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
m_locked = (s_stateMutex && xSemaphoreTake(s_stateMutex, timeout) == pdTRUE);
}
/**
* @brief 析构时释放互斥锁.
*/
~StateLockGuard() {
if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); }
}
/**
* @brief 检查是否成功获取锁.
* @return true 已锁定,false 未锁定.
*/
bool isLocked() const { return m_locked; }
StateLockGuard(const StateLockGuard&) = delete;
StateLockGuard& operator=(const StateLockGuard&) = delete;
private:
bool m_locked = false;
};
// 实例状态(public,供状态机方法访问)
const esp_partition_t *m_gd32Partition = NULL;
session_ctx_t m_sessionCtx;
uint8_t m_nonceServer[16] = {0};
uint8_t m_kSession[16] = {0};
uint8_t m_gd32UidRuntime[12] = {0};
static constexpr size_t GD32_UID_LEN = 12;
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
/**
* @brief 请求GD32 APP进入Bootloader.
* @return 操作结果码.
*/
esp_err_t requestGd32AppEnterBootloader();
/**
* @brief 发送握手请求.
* @return 操作结果码.
*/
ota_result_t sendHandshakeReq();
/**
* @brief 发送随机数并与GD32相互认证.
* @return 操作结果码.
*/
ota_result_t sendNonceAndAuthenticate();
/**
* @brief 向GD32发送固件总大小.
* @param size 固件总字节数.
* @return 操作结果码.
*/
ota_result_t sendFirmwareSize(uint32_t size);
/**
* @brief 从Flash向GD32转发固件包.
* @param deadline_tick 截止时间戳(可选),0表示无超时.
* @return 操作结果码.
*/
ota_result_t relayPacketsFromFlash(TickType_t deadline_tick = 0);
/**
* @brief 完成OTA并发送结束指令.
* @return 操作结果码.
*/
ota_result_t finishOta();
/**
* @brief 使用AES-128-ECB加密单个16字节块.
* @param key 16字节密钥.
* @param in 16字节输入数据.
* @param out 16字节输出数据.
* @return 0 成功,-1 失败.
*/
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
/**
* @brief 记录响应载荷日志.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型.
* @param payload 载荷指针.
* @param len 载荷长度.
*/
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
/**
* @brief 循环等待直到响应不再是WAIT状态.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param timeout_ms 单次轮询超时毫秒.
* @param max_wait_rounds 最大WAIT轮次.
* @return ESP_OK 成功,其他失败.
*/
esp_err_t waitUntilNotWait(const char *stage, uint8_t *resp_type,
uint8_t *resp_payload, size_t resp_max,
size_t *resp_len, uint32_t timeout_ms,
uint32_t max_wait_rounds);
/**
* @brief 判断是否为带序号的结构化ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param expected_seq 期望的序号.
* @return true 匹配成功,false 不匹配.
*/
static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq);
/**
* @brief 解析结构化错误帧.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param seq_out 输出错误序号.
* @param err_out 输出错误码.
* @return true 解析成功,false 解析失败.
*/
static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out);
/**
* @brief 判断是否为握手回显ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是握手回显ACK,false 不是.
*/
static bool isHandshakeEchoAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为挑战应答ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是挑战应答ACK,false 不是.
*/
static bool isChallengePayloadAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为普通确认ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是确认ACK,false 不是.
*/
static bool isConfirmPlainAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为大小序号0的ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是大小序号0ACK,false 不是.
*/
static bool isSizeSeq0Ack(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为普通的结束ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是结束ACK,false 不是.
*/
static bool isFinishPlainAck(const uint8_t *payload, size_t len);
/**
* @brief 轮询直到收到预期的ACK形状.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 响应缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param poll_timeout_ms 轮询超时毫秒.
* @param wait_rounds WAIT状态最大轮询次数.
* @param maxExtraPolls 额外轮询次数.
* @param matcher 匹配回调函数.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t pollUntilExpectedAckShape(const char *stage,
uint8_t *resp_type,
uint8_t *resp_payload,
size_t resp_max,
size_t *resp_len,
uint32_t poll_timeout_ms,
uint32_t wait_rounds,
uint32_t maxExtraPolls,
bool (*matcher)(const uint8_t *, size_t));
/**
* @brief 解析并验证挑战应答载荷.
* @param resp_payload 响应载荷指针.
* @param resp_len 响应长度.
* @param out_k_session 输出会话密钥.
* @param out_uid 输出GD32 UID.
* @return true 验证通过,false 验证失败.
*/
static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
uint8_t *out_k_session, uint8_t *out_uid);
/**
* @brief 使用AES-128-ECB加密48字节数据.
* @param key 16字节密钥.
* @param in 48字节输入数据.
* @param out 48字节输出数据.
* @return true 加密成功,false 加密失败.
*/
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
/**
* @brief 使用密钥计算48字节数据的CRC32.
* @param key 16字节密钥.
* @param data48 48字节输入数据.
* @param output 输出4字节CRC32结果.
*/
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
private:
OtaRelay() = default;
~OtaRelay() = default;
OtaRelay(const OtaRelay&) = delete;
OtaRelay& operator=(const OtaRelay&) = delete;
};
#endif // OTA_RELAY_H