Files
ESP32_COMM/main/ota/ota_coordinator.h
T
小龙虾 5db3f66b8e feat(protocol): 更新SPI Link协议至v2版本并优化CAN通信
- 将SPI OTA帧格式升级为SPI Link v2,采用新的帧格式:
  | SOF0 0xA5 | SOF1 0x5A | VER 0x02 | FLAGS | SESSION 4B LE |
  TX_SEQ | ACK_SEQ | CREDIT | TYPE | LEN 2B LE | RSV 2B | PAYLOAD |
  CRC32 4B LE |
- 新增V2_OVERHEAD=20字节开销,APP最大payload为236字节,BOOT为108字节
- 修改帧类型定义:ENTER_BOOT从0xB5合并至0xB0,新增CAN业务类型0xC0~C3
- 更新CAN协议文档,支持SPI Link v2-only模式,调整最大业务payload计算
- 扩展0xC1帧功能,支持双向通信和1Hz主动推送ID快照
- 添加0xC3完整ID快照帧类型,包含seq/rx_count等完整字段
- 更新协议文档中的错误码定义和反馈payload格式

refactor(main): 重构主程序入口并添加CAN反馈任务

- 在main.cpp中添加CAN反馈任务,更新启动流程注释
- 创建createCriticalTask辅助函数,统一关键任务创建逻辑
- 调整SPI初始化顺序,确保CAN反馈任务依赖正确
- 优化错误处理机制,关键任务失败时立即重启
- 将GD32版本探测移至ota_relay任务处理
- 更新版本号至0.3.48,调整编译时间和Git哈希

config: 更新SPI和BLE配置参数

- 添加SPI主机相关配置参数,包括CS GPIO、READY GPIO等引脚定义
- 配置SPI缓冲区大小:APP为256字节,BOOT为128字节
- 更新BLE滑动窗口参数,ACK窗口设为32,数据队列深度为72
- 调整SPI工作队列大小至5120字节,增加任务堆栈监控说明
2026-07-16 23:00:15 +08:00

524 lines
21 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_coordinator.h - 全局 OTA 协调器
*
* OtaCoordinator 是系统级 OTA 状态的唯一权威来源,负责:
* 1. 串行化 GD32 中继与 ESP32 OTA 操作(同一时刻只能进行一种)
* 2. 为传输层(WiFi/BLE)提供统一的 OTA 操作入口
* 3. 通过 FreeRTOS 队列向 OtaRelay task 投递中继命令
* 4. 向上层(web UI / status API)暴露系统级状态
*
* 线程模型:
* - 状态读写由内部 mutex 保护
* - begin/complete 接口可在任意任务中调用
* - triggerGd32Relay() 向 OtaRelay task 的 dispatchQueue 发送消息
*/
#ifndef OTA_COORDINATOR_H
#define OTA_COORDINATOR_H
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_err.h"
#include "app_config.h" // OTA_MAX_FIRMWARE_SIZE / GD32_OTA_PACKET_SIZE(validateGd32Size 用)
#include "storage/gd32_flash_store.h"
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot
// ============================================================
// 系统级 OTA 状态枚举
// ============================================================
typedef enum {
OTA_SYS_IDLE = 0,
OTA_SYS_GD32_STORING, // 正在从传输层接收 GD32 固件
OTA_SYS_GD32_STORED, // GD32 固件已完整存储,等待中继触发
OTA_SYS_GD32_RELAYING, // 正在通过 SPI 向 GD32 中继固件
OTA_SYS_GD32_COMPLETE, // GD32 OTA 完成(APP 已存活确认)
OTA_SYS_ESP32_RECEIVING, // 正在接收 ESP32 自身固件
OTA_SYS_ESP32_FINALIZING, // 正在收尾 ESP32 OTA(SHA校验 + end + set_boot)
OTA_SYS_ESP32_REBOOTING, // 即将重启 ESP32
OTA_SYS_ERROR // 错误状态
} ota_system_state_t;
// ============================================================
// OTA 传输通道标识(用于同类型重启时区分同通道/跨通道)
// ============================================================
typedef enum {
OTA_CHANNEL_WIFI = 0,
OTA_CHANNEL_BLE = 1,
} ota_channel_t;
// ============================================================
// OTA 系统全量只读快照
// ============================================================
/**
* @brief OTA 系统全量只读快照。
* 由 OtaCoordinator::getSnapshot() 一次性填充,
* 供 /system/status 等只读接口消费,避免散读各模块状态。
*/
struct OtaSnapshot {
ota_system_state_t sys_state = OTA_SYS_IDLE; ///< 系统级 OTA 状态
const char *sys_state_str = "IDLE"; ///< 状态字符串
ota_channel_t channel = OTA_CHANNEL_WIFI; ///< 当前/最近一次 OTA 通道
bool storage_done = false; ///< GD32 固件存储完成标志
ota_meta_t meta = {}; ///< GD32 NVS 元数据
RelaySnapshot relay; ///< 中继进度快照
};
// ============================================================
// IBleCoexListener - BLE 射频共存通知接口
// ============================================================
// WifiOta 实现此接口并注册到 OtaCoordinator,
// BLE OTA 开始/结束时通过协调器通知 WiFi 层,
// 避免 ble_ota → wifi_ota 的直接依赖。
/**
* @brief BLE 射频共存监听器接口。
* BLE OTA 开始/结束时,协调器通过此接口通知 WiFi 层暂停/恢复射频活动。
*/
class IBleCoexListener {
public:
virtual ~IBleCoexListener() = default;
/** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询等射频活动。 */
virtual void onBleOtaStart() = 0;
/** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */
virtual void onBleOtaEnd() = 0;
};
/**
* @brief ESP32 OTA 收尾结果(由 finalizeForReboot 返回)。
* err 为 ESP_OK 时 device_sha_hex 有效;SHA 不匹配时也填充 device_sha_hex 供前端核对。
*/
typedef struct {
esp_err_t err; ///< ESP_OK / ESP_ERR_INVALID_VERSION(SHA不匹配) / 其他
char device_sha_hex[65]; ///< 设备端 SHA hex(成功或 SHA 不匹配时填充)
} Esp32FinalizeResult;
/**
* @brief 全局 OTA 协调器(单例)
*
* 提供系统级 OTA 状态管理与操作串行化。
* 传输层(WiFi/BLE)通过本协调器发起所有 OTA 操作,
* 不再直接调用 OtaRelay / Esp32OtaCore 的底层接口。
*/
class OtaCoordinator : public IRelayListener {
public:
/**
* @brief 获取 OtaCoordinator 单例实例。
* @return OtaCoordinator 单例引用。
*/
static OtaCoordinator& instance();
// ── 初始化 ──
/**
* @brief 初始化协调器(创建 mutex 和队列)。
* 在 OtaRelay task 创建前调用。
*/
void init();
// ── GD32 存储阶段(传输层 → Flash)──
/**
* @brief 传输层开始接收 GD32 固件前调用。
* 若系统正忙(ESP32 OTA 进行中或 GD32 中继中),返回失败。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙。
*/
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
/**
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 复位擦除水位 + 清空 meta + 写入初始 meta。
* 扇区擦除由 flashWriteChunk/ensureErasedForWrite 按需进行(避免 HTTP 头卡整区预擦)。
* 收流时 write 路径只写 Flash(ensureErased 仅作水位兜底)。
* WiFi/BLE 上传入口共用,任意步骤失败时内部 abortCurrent,调用方无需再清理。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE)。
* @return ESP_OK 成功;ESP_ERR_INVALID_SIZE 大小非法;ESP_ERR_INVALID_STATE 系统忙;其他擦除/写入错误。
*/
esp_err_t prepareGd32Storage(uint32_t total_size, ota_channel_t channel);
/**
* @brief GD32 固件接收完毕时调用。
* 更新系统状态为 OTA_SYS_GD32_STORED。
*/
void completeGd32Storage();
/**
* @brief 收流完成后的统一收尾:completeGd32Storage + finalizeGd32Checksum。
* WiFi/BLE 传输层共用,避免一端只 complete、另一端漏算 SHA 的分叉。
* @return finalizeGd32Checksum 的返回值(complete 本身无错误码)。
*/
esp_err_t finishGd32StorageAfterReceive();
// ── GD32 存储操作(传输层通过协调器操作 Flash)──
/**
* @brief 向 GD32 Flash 分区写入固件数据块。
* 传输层(WiFi/BLE)通过此接口写入,不再直接调用 Gd32FlashStore。
* @param offset 写入偏移。
* @param data 数据缓冲区。
* @param len 数据长度。
* @return ESP_OK 成功。
*/
inline esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
{
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
}
/**
* @brief 保存 GD32 OTA 元数据。
* @param meta 元数据结构体。
* @return ESP_OK 成功。
*/
inline esp_err_t saveGd32Meta(const ota_meta_t *meta)
{
return Gd32FlashStore::instance().metaSave(meta);
}
/**
* @brief 加载 GD32 OTA 元数据。
* @param meta 输出元数据结构体。
* @return ESP_OK 成功。
*/
inline esp_err_t loadGd32Meta(ota_meta_t *meta)
{
return Gd32FlashStore::instance().metaLoad(meta);
}
/**
* @brief 更新 GD32 OTA 元数据的进度。
* @param stored_size 已存储大小。
* @param last_seq 最后包序号。
* @return ESP_OK 成功。
*/
inline esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
{
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
}
/**
* @brief 清除 GD32 OTA 元数据。
* @return ESP_OK 成功。
*/
inline esp_err_t clearGd32Meta()
{
return Gd32FlashStore::instance().metaClear();
}
/**
* @brief 校验前端 SHA-256 与设备端固件是否一致。
* @param frontend_sha32 前端 32 字节 SHA。
* @param out_device_sha 输出设备端 32 字节 SHA(可为 nullptr)。
* @return ESP_OK 匹配,ESP_ERR_INVALID_CRC 不匹配。
*/
inline esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32])
{
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
}
/**
* @brief 计算 GD32 固件 SHA-256 并写入元数据。
* @return ESP_OK 成功。
*/
inline esp_err_t finalizeGd32Checksum()
{
return Gd32FlashStore::instance().metaFinalizeChecksum();
}
/**
* @brief 擦除 GD32 OTA 分区。
* @param size 擦除大小。
* @return ESP_OK 成功。
*/
inline esp_err_t eraseGd32Partition(uint32_t size)
{
return Gd32FlashStore::instance().erasePartition(size);
}
/**
* @brief 校验 GD32 固件大小是否合法(BLE/HTTP 双通道共用)。
*
* 统一规则:raw==0 或 raw>MAX 拒绝;按 GD32_OTA_PACKET_SIZE 向上取整后仍>MAX 拒绝。
* 调用方各自翻译为通道特定错误码(BLE: BLE_OTA_FINISH_ERR_GD32_SIZE;HTTP: HTTPD_500)。
*
* @param raw_size 固件原始字节数。
* @return ESP_OK 合法;ESP_ERR_INVALID_SIZE 非法。
*/
inline esp_err_t validateGd32Size(uint32_t raw_size) const
{
if (raw_size == 0U || raw_size > (uint32_t)OTA_MAX_FIRMWARE_SIZE) {
return ESP_ERR_INVALID_SIZE;
}
const uint32_t packet_size = (uint32_t)GD32_OTA_PACKET_SIZE;
const uint32_t rounded = ((raw_size + packet_size - 1U) / packet_size) * packet_size;
if (rounded > (uint32_t)OTA_MAX_FIRMWARE_SIZE) {
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
// ── GD32 中继阶段 ──
/**
* @brief 触发 GD32 中继转发。
* 向 OtaRelay task 的 dispatchQueue 发送启动消息。
* @return ESP_OK 消息已投递,ESP_ERR_INVALID_STATE 不允许触发。
*/
esp_err_t triggerGd32Relay();
/**
* @brief OtaRelay task 回调:中继已开始。
* 由 OtaRelay 在开始 forwardFirmware() 时调用。
*/
void notifyRelayStarted();
/**
* @brief OtaRelay task 回调:中继已完成。
* 由 OtaRelay 在 forwardFirmware() 返回后调用。
* @param success true 成功,false 失败。
*/
void notifyRelayComplete(bool success);
// ── ESP32 OTA 阶段 ──
/**
* @brief ESP32 OTA 开始接收固件。
* 内部委托 Esp32OtaCore::begin()。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙或通道被占用。
*/
esp_err_t beginEsp32Ota(uint32_t total_size, ota_channel_t channel);
/**
* @brief 发起 ESP32 OTA,若被 ESP_ERR_INVALID_STATE 拒绝则 forceCleanup 后重试一次。
* 封装 BLE/WiFi 两端完全相同的重试模式,调用方只需调一次。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE)。
* @return ESP_OK 成功;其他 = 协调器拒绝或 Esp32OtaCore::begin 失败。
*/
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel);
/**
* @brief ESP32 OTA 固件接收完毕,准备收尾。
* 更新系统状态为 OTA_SYS_ESP32_FINALIZING。
*/
void notifyEsp32ReceiveComplete();
/**
* @brief ESP32 OTA 收尾完成(finalize 成功)。
*/
void notifyEsp32FinalizeComplete();
/**
* @brief ESP32 即将重启。
*/
void notifyEsp32Rebooting();
/**
* @brief ESP32 OTA 收尾(统一封装 BLE/WiFi 两端重复的收尾序列)。
* 步骤:notifyEsp32ReceiveComplete → Esp32OtaCore::finalize(reboot=false)
* → 成功则 notifyEsp32FinalizeComplete;
* → 失败(SHA 不匹配 / end 失败等)则 abortCurrent() 复位到 IDLE,
* 避免协调器卡在 ESP32_FINALIZING 导致后续上传被假拒绝。
* 不重启;调用方负责把 device_sha_hex 上报前端后再调 restartEsp32()。
* @param frontend_sha_hex 前端 64 字符 hex SHA-256,nullptr 表示跳过比对。
* @return Esp32FinalizeResult,err 为 ESP_OK/ESP_ERR_INVALID_VERSION/其他。
*/
Esp32FinalizeResult finalizeForReboot(const char *frontend_sha_hex);
/**
* @brief 重启 ESP32(统一封装 BLE/WiFi 两端重复的重启序列)。
* 步骤:notifyEsp32Rebooting → vTaskDelay(500ms) → esp_restart()。
* 应在 finalizeForReboot 成功并完成前端上报后调用。不会返回。
*/
void restartEsp32();
// ── 中止 ──
/**
* @brief 中止当前 OTA 操作(GD32 或 ESP32)。
* 内部调用对应子系统的 abort/reset。
*/
void abortCurrent();
/**
* @brief 自愈孤儿忙态(供 /system/status 等轮询路径调用)。
* - 忙状态 watchdog(超时完整 abort → IDLE;不含 REBOOTING)
* - ESP32_FINALIZING 但 core 已空闲:残留 → IDLE
* - ERROR → IDLE 软清,避免 UI 长期卡错误态
*/
void healOrphanStates();
/**
* @brief 获取当前 OTA 会话的 epoch(代次)。
* 每次 beginEsp32Ota/beginGd32Storage 成功时递增。
* 传输层 handler 在 begin 成功后捕获 epoch,
* 在失败清理时通过 abortCurrentIfEpoch() 检查是否仍为当前会话,
* 避免旧 handler 的清理代码误中止新会话(同类型重启场景)。
* @return 当前 epoch 值。
*/
uint32_t getOtaEpoch() const;
/**
* @brief 仅当 epoch 匹配当前会话时才中止 OTA。
* 用于旧 handler 的失败清理路径:若新请求已经接管(epoch 不匹配),
* 旧 handler 不应调 abortCurrent,否则会误中止新会话。
* @param epoch 调用方在 begin 成功时捕获的 epoch。
* @return true 已中止(epoch 匹配),false 未中止(新会话已接管)。
*/
bool abortCurrentIfEpoch(uint32_t epoch);
// ── 状态查询 ──
/**
* @brief 一次性读取系统 OTA 全量状态快照。
* coordinator mutex + relay mutex 各取一次,
* 供 /system/status 等只读接口消费。
* @return 包含 sys_state、meta、relay 进度的完整快照。
*/
OtaSnapshot getSnapshot() const;
/**
* @brief 单次 SPI 探测 GD32 APP 是否存活。
* 成功时更新 OtaRelay 缓存的 alive/version,
* 失败时清除 alive 标志。
* @param out_version 输出版本信息,可为 nullptr。
* @param out_uptime 输出 GD32 运行时间(秒),可为 nullptr。
* @return ESP_OK 存活,其他失败。
*/
esp_err_t probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime);
/**
* @brief 获取当前系统级 OTA 状态。
* @return 当前 ota_system_state_t 状态值。
*/
ota_system_state_t getSystemState() const;
/**
* @brief 获取当前 OTA 会话的发起通道(WiFi/BLE)。
* 仅在活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义;
* IDLE 等非活跃状态返回值未定义(默认 OTA_CHANNEL_WIFI)。
* @return 当前会话通道枚举值。
*/
ota_channel_t getOtaChannel() const;
/**
* @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。
* @param s 状态枚举值。
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
*/
static const char* stateToString(ota_system_state_t s);
/**
* @brief 检查给定状态值是否属于空闲态(不加锁,纯判断)。
* 调用方先 getSystemState() 取一次再传入,避免连续加锁。
* @param s 状态枚举值。
* @return true 表示 IDLE/STORED/COMPLETE。
*/
bool isIdleState(ota_system_state_t s) const;
/**
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
* 内部仍加锁取一次状态后调 isIdleState()。
* @return true 系统空闲(IDLE/STORED/COMPLETE)。
*/
bool isIdle() const;
// ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)──
/**
* @brief 获取 GD32 APP 存活状态。
* @return true 表示 GD32 APP 存活。
*/
bool getGd32AppAlive() const;
// ── 完全复位 ──
/**
* @brief 完全复位协调器和所有子系统状态。
*/
void reset();
// ── IRelayListener 接口实现 ──
/**
* @brief IRelayListener 回调:中继已开始。
* 由 OtaRelay task 在 forwardFirmware() 前通过 s_relayListener 调用。
*/
void onRelayStarted() override;
/**
* @brief IRelayListener 回调:中继已完成。
* 由 OtaRelay task 在 forwardFirmware() 返回后通过 s_relayListener 调用。
* @param success true 成功,false 失败。
*/
void onRelayComplete(bool success) override;
// ── BLE 射频共存管理 ──
/**
* @brief 注册 BLE 共存监听器(由 main 在初始化时调用)。
* @param listener WifiOta 实现的监听器指针,nullptr 取消注册。
*/
void setBleCoexListener(IBleCoexListener *listener);
/**
* @brief BLE OTA 开始时调用,通知 WiFi 层暂停 RSSI 轮询。
*/
void notifyBleCoexStart();
/**
* @brief BLE OTA 结束时调用,通知 WiFi 层恢复射频活动。
*/
void notifyBleCoexEnd();
private:
OtaCoordinator() = default;
~OtaCoordinator() = default;
OtaCoordinator(const OtaCoordinator&) = delete;
OtaCoordinator& operator=(const OtaCoordinator&) = delete;
// 线程安全辅助
// ★ lock() 返回是否成功 take;unlock(handle) 据此决定是否 give。
// 旧实现用共享 bool m_locked 记录 take 结果,多任务并发 lock() 时
// 超时方会把 m_locked 置 false,覆盖持锁方的 true,导致 unlock() 跳过 give,
// mutex 永久死锁 → 后续 take 超时触发 vTaskPriorityDisinherit assert → PANIC。
bool lock() const;
void unlock(bool handle) const;
// ★ 卡死自愈检查:忙状态超时则 doAbortLocked → IDLE(不含 REBOOTING)。
// 必须在已持锁状态下调用。
void watchdogCheckLocked();
// ★ 已持锁版本的中止序列:根据当前 m_sysState 调用对应子模块的清理,
// 并把状态机复位为 IDLE。abortCurrent / abortCurrentIfEpoch 共享此实现。
void doAbortLocked();
// ★ 已持锁版本的 ESP32 状态切换 helper:设置 m_sysState 并打印一条 INFO 日志。
// 供 notifyEsp32*Complete / notifyEsp32Rebooting 等锁内路径复用,避免重复样板代码。
void setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg);
// 系统状态
ota_system_state_t m_sysState = OTA_SYS_IDLE;
// 进入当前忙状态的时刻(tick)。IDLE/STORED/COMPLETE/ERROR 时为 0。
TickType_t m_busySinceTick = 0;
// GD32 存储跟踪
bool m_gd32StorageDone = false;
// OTA 会话代次:每次 beginEsp32Ota/beginGd32Storage 成功时递增。
// 传输层 handler 在 begin 后捕获 epoch,失败清理时通过 abortCurrentIfEpoch
// 检查 epoch 是否仍匹配,避免旧 handler 误中止同类型重启后的新会话。
uint32_t m_otaEpoch = 0;
// 当前 OTA 会话的发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
// 仅在同类型活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义。
ota_channel_t m_otaChannel = OTA_CHANNEL_WIFI;
// 互斥锁(mutable 允许在 const 方法中加锁,无需 const_cast)
mutable SemaphoreHandle_t m_mutex = nullptr;
// BLE 共存监听器(由 WifiOta 实现并注册)
IBleCoexListener *m_bleCoexListener = nullptr;
static constexpr TickType_t kLockTimeout =
pdMS_TO_TICKS(OTA_COORD_LOCK_TIMEOUT_MS);
// ★ 忙状态 watchdog:须覆盖最长中继预算;数值见 app_config.h
static constexpr TickType_t kBusyWatchdogTicks =
pdMS_TO_TICKS(OTA_COORD_BUSY_WATCHDOG_MS);
};
#endif // OTA_COORDINATOR_H