feat(ota): 引入OTA协调器和事件驱动状态机重构

- 添加OtaCoordinator单例模式,提供9状态系统级OTA协调功能
- 重构OtaRelay为事件驱动状态机,从4阶段扩展为6阶段状态机
- 实现系统级状态管理:OTA_SYS_IDLE/GD32_STORING/GD32_RELAYING等9个状态
- 传输层通过OtaCoordinator统一入口管理ESP32/GD32 OTA操作序列化

fix(protocol): 优化协议层安全配置和参数管理

- Scramble常量改为运行时随机生成,提升安全性
- GD32芯片UID从握手包动态获取,移除编译期常量
- 添加运行时SCRAMBLE_CONST参数说明

refactor(esp32_ota): 优化ESP32 OTA分区擦除策略

- BLE OTA采用实际固件大小而非分区全尺寸进行擦除
- 避免全分区擦除导致的Task Watchdog超时问题
- 擦除大小对齐到64KB块边界确保尾部数据干净

feat(transport): 增强SPI传输层功能和可靠性

- 添加Flash元数据记录(total_size/stored_size/last_seq/checksum)
- 支持中继恢复判断和完整性校验
- 添加ACK错误码支持(0x04=CRC错误)

feat(ble): 实现BLE传输层乱序重排和预检功能

- 添加32槽reorder buffer处理窗口内乱序包
- 新增0xF9命令支持仅SHA校验不入库的预检模式
- 完善BLE OTA收尾流程和状态跟踪

docs(readme): 更新架构文档和API说明

- 详细描述OtaCoordinator系统级状态流转
- 更新OTA流程状态机图表和说明
- 补充新的控制命令和配置参数说明
This commit is contained in:
小龙虾
2026-06-17 02:18:42 +08:00
parent 5b2c5f8ff9
commit 98b5cc1ba1
11 changed files with 1535 additions and 919 deletions
+454 -500
View File
File diff suppressed because it is too large Load Diff
+157 -264
View File
@@ -1,8 +1,16 @@
/*
* ota_relay.h - OTA 中继核心状态机
* T2.3, T3.1 - T3.7
* ota_relay.h - OTA 中继事件驱动状态机
*
* OtaRelay 类封装 OTA 中继状态机、Flash 存储和元数据管理。
* OtaRelay 将 GD32 SPI OTA 协议封装为事件驱动的状态机:
* 1. 每个协议阶段对应一个状态(共 6 个阶段状态 + ERROR)
* 2. 状态通过 processEvent() 驱动转换,而非顺序赋值
* 3. 外部(OtaCoordinator / transport layer)通过 dispatchQueue 触发中继
* 4. forwardFirmware() 保留为便捷入口,内部驱动状态机循环
*
* 状态转换:
* IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
* 任意状态 ──(error)──→ ERROR
* ERROR/DONE ──(reset)──→ IDLE
*/
#ifndef OTA_RELAY_H
#define OTA_RELAY_H
@@ -24,26 +32,31 @@ typedef struct {
uint8_t checksum[32];
} ota_meta_t;
// ============================================================
// 阶段状态枚举(精简为 7 个状态,原 17 个"微状态"合并为阶段)
// ============================================================
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_IDLE = 0, // 空闲
RELAY_HANDSHAKING, // 阶段1: 握手 + 挑战认证
RELAY_SIZE_NEG, // 阶段2: 固件大小协商
RELAY_TRANSFERRING, // 阶段3: 数据包传输
RELAY_FINISHING, // 阶段4-5: 结束OTA + APP存活验证
RELAY_DONE, // 完成
RELAY_ERROR = 0xFF // 错误
} relay_state_t;
// ============================================================
// 事件类型(驱动状态转换)
// ============================================================
typedef enum {
RELAY_EVT_START = 0, // 启动中继流程
RELAY_EVT_RESP_OK, // 收到 GD32 有效响应
RELAY_EVT_RESP_ERROR, // 收到 GD32 错误响应
RELAY_EVT_TIMEOUT, // 超时
RELAY_EVT_HEALTH_CRIT, // 健康检查危险
RELAY_EVT_ABORT // 外部中止
} relay_event_t;
typedef enum {
OTA_RESULT_OK = 0,
OTA_RESULT_ERR_TIMEOUT = -1,
@@ -58,141 +71,117 @@ typedef enum {
class OtaRelay {
public:
/**
* @brief 获取OtaRelay单例引用.
* @return OtaRelay单例引用.
* @brief 获取OtaRelay单例引用.
*/
static OtaRelay& instance();
/**
* @brief OTA中继FreeRTOS任务入口.
* @param pvParameters FreeRTOS任务参数.
* @brief OTA中继FreeRTOS任务入口.
*/
static void taskEntry(void *pvParameters);
/**
* @brief 从Flash向GD32转发固件(主流程).
* @brief 从Flash向GD32转发固件(便捷入口,内部驱动状态机循环).
* @return 操作结果码.
*/
ota_result_t forwardFirmware();
/* ---- 线程安全的状态访问接口 ----
* OTA 任务写、HTTP 任务读,通过 mutex 保证一致性。
* 状态机内部(OTA 任务)可直接写 static 成员,
* 跨任务读/写必须走这些 API。
*/
// ── 事件驱动接口 ──
/**
* @brief 获取当前中继状态.
* @return 当前中继状态.
* @brief 向状态机投递事件,驱动状态转换.
* @param event 事件类型.
* @param payload 关联数据(响应载荷等),可为nullptr.
* @param len 数据长度.
* @return 当前操作结果码(OTA_RESULT_OK 表示事件已处理且可继续).
*/
ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len);
// ── 状态查询(线程安全)──
/**
* @brief 获取当前中继状态.
*/
static relay_state_t getState();
/**
* @brief 获取中继当前序号.
* @return 当前序号值.
* @brief 获取中继当前序号.
*/
static uint32_t getRelaySeq();
/**
* @brief 获取中继总包数.
* @return 总包数.
* @brief 获取中继总包数.
*/
static uint32_t getRelayTotalPackets();
/**
* @brief 获取GD32 APP存活标志位.
* @return true 存活,false 未存活.
* @brief 获取GD32 APP存活标志位.
*/
static bool getGd32AppAlive();
/**
* @brief 获取GD32固件版本.
* @param out 输出参数,版本结构体指针.
* @brief 获取GD32固件版本.
*/
static void getGd32Version(gd32_version_t *out);
/**
* @brief 设置中继状态.
* @param s 目标状态值.
* @brief 设置中继状态.
*/
static void setState(relay_state_t s);
/**
* @brief 设置中继当前序号.
* @param seq 序号值.
* @brief 设置中继当前序号.
*/
static void setRelaySeq(uint32_t seq);
/**
* @brief 设置中继总包数.
* @param total 总包数.
* @brief 设置中继总包数.
*/
static void setRelayTotalPackets(uint32_t total);
/**
* @brief 设置GD32 APP存活标志位.
* @param alive 存活状态.
* @brief 设置GD32 APP存活标志位.
*/
static void setGd32AppAlive(bool alive);
/**
* @brief 获取GD32固件版本.
* @param ver 待设置的版本结构体引用.
* @brief 设置GD32固件版本.
*/
static void setGd32Version(const gd32_version_t &ver);
/**
* @brief 重置状态机.
* @brief 重置状态机到 IDLE.
*/
void reset();
// ── Flash 操作 ──
/**
* @brief Flash写入接口(支持非4字节对齐写入).
* @param offset 分区内写入偏移.
* @param data 待写入的数据指针.
* @param len 数据长度(字节).
* @return ESP_OK 成功,其他失败.
* @brief Flash写入接口(支持非4字节对齐写入).
*/
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 成功,其他失败.
* @brief 从OTA缓存分区读取数据块.
*/
static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
/**
* @brief 从Flash加载元数据.
* @param meta 输出参数,加载后的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
* @brief 从Flash加载元数据.
*/
static esp_err_t metaLoad(ota_meta_t *meta);
/**
* @brief 保存元数据到Flash.
* @param meta 待保存的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
* @brief 保存元数据到Flash.
*/
static esp_err_t metaSave(const ota_meta_t *meta);
/**
* @brief 传输过程中更新元数据.
* @param stored_size 已存储的固件大小(字节).
* @param last_seq 最后成功的分块序号.
* @return ESP_OK 成功,其他失败.
* @brief 传输过程中更新元数据.
*/
static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
/**
* @brief OTA完成或中止时清除元数据.
* @return ESP_OK 成功,其他失败.
* @brief OTA完成或中止时清除元数据.
*/
static esp_err_t metaClear();
/**
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
* @param out_sha256 输出32字节SHA-256摘要.
* @return ESP_OK 成功,其他失败.
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
*/
static esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
/**
* @brief 固件接收完毕后计算SHA-256并写入元数据.
* @return ESP_OK 成功,其他失败.
* @brief 固件接收完毕后计算SHA-256并写入元数据.
*/
static esp_err_t metaFinalizeChecksum();
/**
* @brief 校验固件完整性(SHA-256).
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败.
* @brief 校验固件完整性(SHA-256).
*/
static esp_err_t metaVerifyChecksum();
// 公开状态(状态机内部直接访问;跨任务请用 getter/setter)
// ── 公开状态成员 ──
static relay_state_t relayState;
static uint32_t relaySeq;
static uint32_t relayTotalPackets;
@@ -201,33 +190,19 @@ public:
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;
@@ -235,7 +210,7 @@ public:
bool m_locked = false;
};
// 实例状态(public,供状态机方法访问)
// ── 实例状态 ──
const esp_partition_t *m_gd32Partition = NULL;
session_ctx_t m_sessionCtx;
uint8_t m_nonceServer[16] = {0};
@@ -245,177 +220,95 @@ public:
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;
// ── 状态机内部类型 ──
/**
* @brief 状态处理函数类型:接收事件 + 载荷,返回结果码.
* 返回 OTA_RESULT_OK 表示事件已成功处理且状态机可继续;
* 返回负值表示错误,状态机转入 RELAY_ERROR.
*/
typedef ota_result_t (OtaRelay::*StateHandler)(relay_event_t event,
const uint8_t *payload, size_t len);
struct RelayStateDef {
const char *name; // 状态名称(日志用)
StateHandler handler; // 事件处理器
};
// ── 状态表(const,编译期确定)──
static const RelayStateDef s_stateTable[];
// ── 各状态的事件处理器 ──
ota_result_t handleIdle(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleHandshaking(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleSizeNeg(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleTransferring(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleFinishing(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleDone(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleError(relay_event_t event, const uint8_t *payload, size_t len);
// ── 协议子步骤(由状态 handler 内部调用)──
/**
* @brief 请求GD32 APP进入Bootloader.
*/
esp_err_t requestGd32AppEnterBootloader();
/**
* @brief 执行握手请求(发送 HANDSHAKE_REQ).
*/
ota_result_t doHandshakeReq();
/**
* @brief 执行挑战认证.
*/
ota_result_t doChallengeAndAuth();
/**
* @brief 发送固件总大小.
*/
ota_result_t doSendFirmwareSize(uint32_t size);
/**
* @brief 转发下一批数据包(由 TRANSFERRING 状态循环调用).
*/
ota_result_t doRelayPackets(TickType_t deadline_tick);
/**
* @brief 发送结束指令并验证 APP 存活.
*/
ota_result_t doFinishAndVerify();
// ── 传输进度跟踪(状态机内部)──
uint32_t m_transferTotalPackets = 0;
uint32_t m_transferCurrentSeq = 0;
uint32_t m_transferFlashOffset = 0;
uint32_t m_transferRetryCount = 0;
TickType_t m_transferDeadline = 0;
// ── 加密/工具方法(不变)──
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
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);
static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq);
static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out);
static bool isHandshakeEchoAck(const uint8_t *payload, size_t len);
static bool isChallengePayloadAck(const uint8_t *payload, size_t len);
static bool isConfirmPlainAck(const uint8_t *payload, size_t len);
static bool isSizeSeq0Ack(const uint8_t *payload, size_t len);
static bool isFinishPlainAck(const uint8_t *payload, size_t len);
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));
static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
uint8_t *out_k_session, uint8_t *out_uid);
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
};
#endif // OTA_RELAY_H
#endif // OTA_RELAY_H