新增UART0 RX泵任务功能,将GPIO20接收到的串口数据按切行、剥离后以tag=UART推入ring buffer,通过HTTP /logs接口显示在网页日志面板。 接线方式:任意串口源TX连接至H1/H2的ESP_RX引脚,波特率3M 8N1。 feat(motor_ota): GD32 OTA增加包级防呆机制防止错误固件刷入 中继触发前校验gd32_ota镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity), 对Bootloader包或非APP包在WiFi /ota/relay与BLE 0xF7均拒绝中继, 避免GD32 APP区被错误包擦除。新增0x87完成码用于固件身份验证失败场景。 feat(motor_state): 扩展CAN状态数据结构支持CONFIG负载 扩展motor_slot_t结构体,新增config_rx_count、config_seq和config_data字段 用于存储CONFIG帧的相关信息。CAN状态数据包大小从44B扩展到76B, 页面总数从6调整为9,支持目标值回报的配置数据传输。 fix(motor_ota): 修复OTA取消流程中的信用度管理和任务清理问题 ★ 先复位credit再发送CANCEL命令,防止传输卡住时背压导致CANCEL被拒绝。 延长取消等待时间至10s覆盖最坏情况,确保feedTask正常退出而非强制删除。 取消完成后复位credit防止锁存死锁,确保后续通信正常进行。 fix(http_server): 修复HTTP请求处理中的边界检查和安全验证 拒绝chunked请求(content_len<=0)防止无限循环,添加通道参数范围验证(ch 0~2), 在OTA中继触发前验证固件身份防止错误包刷入,使用http_json_append避免缓冲区越界。 refactor(gd32_flash): 实现GD32固件身份验证算法确保刷写安全 基于Cortex-M向量表内容实现包级身份校验,通过MSP和复位向量位置 区分APP包、Bootloader包和无效固件,为OTA安全提供基础保障。 BREAKING CHANGE: 电机状态API数据格式变更,CAN通道参数验证更严格
840 lines
30 KiB
C++
840 lines
30 KiB
C++
/*
|
||
* ota_coordinator.cpp - 全局 OTA 协调器实现
|
||
*
|
||
* OtaCoordinator 是系统级 OTA 状态的唯一权威来源。
|
||
* 传输层(WiFi/BLE)通过本协调器发起所有 OTA 操作。
|
||
*/
|
||
#include "ota_coordinator.h"
|
||
#include "monitor/can_feedback_task.h"
|
||
#include "monitor/motor_ota_relay.h"
|
||
#include "esp32_ota_core.h"
|
||
#include "relay/ota_relay.h"
|
||
#include "storage/gd32_flash_store.h"
|
||
#include "server/ble_ota.h"
|
||
#include "monitor/debug_log.h"
|
||
#include "esp_system.h"
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/task.h"
|
||
#include <inttypes.h>
|
||
|
||
#define TAG "OTA_COORD"
|
||
|
||
/** WiFi OTA 时挂起 BLE;BLE 通道自身 OTA 不挂。调用勿持 m_mutex。 */
|
||
static void wifiOtaSuspendBleIfNeeded(ota_channel_t channel)
|
||
{
|
||
if (channel != OTA_CHANNEL_WIFI) {
|
||
return;
|
||
}
|
||
(void)BleOta::instance().suspendStack();
|
||
}
|
||
|
||
/** 回到可空闲态时恢复 BLE。调用勿持 m_mutex。 */
|
||
static void wifiOtaResumeBleIfNeeded(void)
|
||
{
|
||
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||
(void)BleOta::instance().resumeStack();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 单例
|
||
// ============================================================
|
||
OtaCoordinator& OtaCoordinator::instance()
|
||
{
|
||
static OtaCoordinator inst;
|
||
return inst;
|
||
}
|
||
|
||
// ============================================================
|
||
// 线程安全辅助
|
||
// ============================================================
|
||
bool OtaCoordinator::lock() const
|
||
{
|
||
/* 返回 take 是否成功;调用方须把该值传给 unlock() */
|
||
if (!m_mutex) return false;
|
||
bool ok = (xSemaphoreTake(m_mutex, kLockTimeout) == pdTRUE);
|
||
if (!ok) {
|
||
DEBUG_ERR(TAG, "lock() 超时 (%u ms),本次状态变更被跳过",
|
||
(unsigned)pdTICKS_TO_MS(kLockTimeout));
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
void OtaCoordinator::unlock(bool handle) const
|
||
{
|
||
/* 仅当对应 lock() 成功时 give */
|
||
if (handle && m_mutex) {
|
||
xSemaphoreGive(m_mutex);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 卡死自愈检查(必须在持锁状态下调用)
|
||
// ============================================================
|
||
void OtaCoordinator::watchdogCheckLocked()
|
||
{
|
||
// 仅对"忙"状态(可能卡死的)做检查。
|
||
// REBOOTING:已 set_boot,即将 esp_restart,不入狗,避免 abort 成 IDLE 与重启竞态。
|
||
switch (m_sysState) {
|
||
case OTA_SYS_GD32_STORING:
|
||
case OTA_SYS_GD32_RELAYING:
|
||
case OTA_SYS_ESP32_RECEIVING:
|
||
case OTA_SYS_ESP32_FINALIZING:
|
||
break;
|
||
default:
|
||
return; // IDLE/STORED/COMPLETE/ERROR/REBOOTING 无需检查
|
||
}
|
||
|
||
if (m_busySinceTick == 0) {
|
||
// 未记录起始时刻(理论上不应发生),保守不触发
|
||
return;
|
||
}
|
||
|
||
TickType_t elapsed = xTaskGetTickCount() - m_busySinceTick;
|
||
if (elapsed < kBusyWatchdogTicks) {
|
||
return; // 未超时
|
||
}
|
||
|
||
DEBUG_ERR(TAG, "忙状态 %d 超过 %" PRIu32 " ms,强制中止 → IDLE",
|
||
(int)m_sysState, (uint32_t)pdTICKS_TO_MS(kBusyWatchdogTicks));
|
||
doAbortLocked();
|
||
}
|
||
|
||
esp_err_t OtaCoordinator::beginLockedWithChecks(const char *tag, bool &out_lock)
|
||
{
|
||
out_lock = lock();
|
||
if (!out_lock) return ESP_ERR_INVALID_STATE;
|
||
|
||
// ★ 卡死自愈检查
|
||
watchdogCheckLocked();
|
||
|
||
// ★ 电机 OTA 互斥:电机喂数期间禁止新的 GD32/ESP32 OTA
|
||
if (motorOtaIsActive()) {
|
||
DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 %s", tag);
|
||
unlock(true);
|
||
out_lock = false;
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
// ============================================================
|
||
// 初始化
|
||
// ============================================================
|
||
void OtaCoordinator::init()
|
||
{
|
||
if (!m_mutex) {
|
||
m_mutex = xSemaphoreCreateMutex();
|
||
if (m_mutex) {
|
||
DEBUG_INFO(TAG, "OTA 全局协调器已初始化");
|
||
} else {
|
||
DEBUG_ERR(TAG, "协调器 mutex 创建失败");
|
||
}
|
||
}
|
||
m_sysState = OTA_SYS_IDLE;
|
||
m_gd32StorageDone = false;
|
||
m_busySinceTick = 0;
|
||
|
||
// 注册协调器为 OtaRelay 的中继事件监听器
|
||
OtaRelay::setRelayListener(this);
|
||
|
||
// ★ 重启恢复:如果 NVS 里有完整固件元数据,开机时立即进入 STORED 状态,
|
||
// 否则用户重启后看到的状态是 IDLE 但 NVS 里其实有未中继的固件,前后不一致。
|
||
ota_meta_t boot_meta;
|
||
if (Gd32FlashStore::instance().metaLoad(&boot_meta) == ESP_OK &&
|
||
boot_meta.total_size > 0 &&
|
||
boot_meta.stored_size >= boot_meta.total_size) {
|
||
m_sysState = OTA_SYS_GD32_STORED;
|
||
m_gd32StorageDone = true;
|
||
DEBUG_INFO(TAG, "启动恢复: NVS 检测到完整 GD32 固件 (%" PRIu32 " 字节), → GD32_STORED",
|
||
boot_meta.total_size);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// GD32 存储阶段
|
||
// ============================================================
|
||
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||
{
|
||
bool h;
|
||
esp_err_t pre = beginLockedWithChecks("GD32 存储", h);
|
||
if (pre != ESP_OK) return pre;
|
||
|
||
// ★ 同类型重启:若上一次 GD32 存储仍在进行中(STORING)且来自同一通道,
|
||
// 自动中止旧存储并重新开始。覆盖"上传过程中刷新网页再上传"场景。
|
||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||
// RELAYING 是中继阶段,不可中断,也拒绝。
|
||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||
if (m_otaChannel != channel) {
|
||
DEBUG_WARN(TAG, "GD32存中 ch=%d 拒跨通道 ch=%d",
|
||
(int)m_otaChannel, (int)channel);
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
DEBUG_WARN(TAG, "GD32同通道重传:中止旧存储");
|
||
doAbortLocked(); // 清 meta + IDLE,与 abort 路径一致
|
||
// epoch 将在下方设置 GD32_STORING 后递增
|
||
}
|
||
|
||
// 系统忙检查:仅拒绝真正"在传输"的活跃状态
|
||
// STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖
|
||
if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||
m_sysState == OTA_SYS_ESP32_REBOOTING ||
|
||
m_sysState == OTA_SYS_GD32_RELAYING) {
|
||
DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState);
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),允许新存储覆盖。
|
||
if (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE) {
|
||
DEBUG_INFO(TAG, "覆盖旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||
}
|
||
|
||
m_otaEpoch++; // ★ 新会话开始,递增 epoch
|
||
m_otaChannel = channel; // ★ 记录发起通道
|
||
m_sysState = OTA_SYS_GD32_STORING;
|
||
m_gd32StorageDone = false;
|
||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||
DEBUG_DBG(TAG, "GD32存开始 %" PRIu32 "B →STORING ep=%u ch=%d",
|
||
total_size, m_otaEpoch, (int)channel);
|
||
// ★ 预分配 flashWriteChunk 对齐缓冲(OTA 期消除逐块 malloc;非 OTA 期零占用)
|
||
Gd32FlashStore::acquireAlignBufForOta();
|
||
unlock(h);
|
||
return ESP_OK;
|
||
}
|
||
|
||
esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||
{
|
||
// 1. 校验固件大小(统一 BLE/HTTP 两路径的大小检查)
|
||
esp_err_t err = validateGd32Size(total_size);
|
||
if (err != ESP_OK) {
|
||
DEBUG_WARN(TAG, "prepareGd32Storage: 固件大小非法 raw=%" PRIu32, total_size);
|
||
return err;
|
||
}
|
||
|
||
/* WiFi GD32 上传前先挂 BLE,腾堆给 HTTP 收流 */
|
||
wifiOtaSuspendBleIfNeeded(channel);
|
||
|
||
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
|
||
err = beginGd32Storage(total_size, channel);
|
||
if (err != ESP_OK) {
|
||
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
|
||
if (channel == OTA_CHANNEL_WIFI) {
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|
||
return err;
|
||
}
|
||
|
||
// 3. 复位擦除水位;不在 prepare 整区预擦。
|
||
// 旧方案在 HTTP JSON 头后同步擦 gd32_ota,TCP 堵在开头。
|
||
// 现由 flashWriteChunk → ensureErasedForWrite 按需擦扇区,与收流交织。
|
||
Gd32FlashStore::instance().beginWriteSession();
|
||
if (Gd32FlashStore::instance().getPartition() == nullptr) {
|
||
DEBUG_ERR(TAG, "prepareGd32Storage: 找不到 gd32_ota 分区");
|
||
abortCurrent();
|
||
return ESP_ERR_NOT_FOUND;
|
||
}
|
||
DEBUG_INFO(TAG, "prepareGd32Storage: 会话就绪 fw=%" PRIu32 "(按需擦除,无整区预擦)",
|
||
total_size);
|
||
|
||
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
|
||
clearGd32Meta();
|
||
ota_meta_t meta = {};
|
||
meta.total_size = total_size;
|
||
meta.stored_size = 0;
|
||
meta.last_seq = 0;
|
||
err = saveGd32Meta(&meta);
|
||
if (err != ESP_OK) {
|
||
DEBUG_ERR(TAG, "prepareGd32Storage: 元数据保存失败: %s", esp_err_to_name(err));
|
||
abortCurrent();
|
||
return err;
|
||
}
|
||
|
||
return ESP_OK;
|
||
}
|
||
|
||
void OtaCoordinator::completeGd32Storage()
|
||
{
|
||
bool h = lock();
|
||
if (h && m_sysState == OTA_SYS_GD32_STORING) {
|
||
m_sysState = OTA_SYS_GD32_STORED;
|
||
m_gd32StorageDone = true;
|
||
m_busySinceTick = 0; // ★ STORED 是非忙状态
|
||
DEBUG_DBG(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
|
||
/* WiFi 路径不在此处恢复 BLE:
|
||
* 旧逻辑 HTTP 一收完就 resume,55KB 极快时日志紧贴 raw upload start,
|
||
* 像「一刷新就共存恢复」。挂起到中继结束/中止(notifyRelayComplete、abort)。 */
|
||
} else if (h) {
|
||
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
|
||
}
|
||
unlock(h);
|
||
}
|
||
|
||
esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
|
||
{
|
||
completeGd32Storage();
|
||
return finalizeGd32Checksum();
|
||
}
|
||
|
||
// ============================================================
|
||
// GD32 中继阶段
|
||
// ============================================================
|
||
esp_err_t OtaCoordinator::triggerGd32Relay()
|
||
{
|
||
bool h = lock();
|
||
if (!h) {
|
||
DEBUG_ERR(TAG, "triggerGd32Relay: lock 超时");
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
watchdogCheckLocked();
|
||
|
||
/* 白名单:仅 STORED / IDLE(NVS 有完整固件)/ COMPLETE(允许再中继)可触发 */
|
||
if (m_sysState != OTA_SYS_GD32_STORED &&
|
||
m_sysState != OTA_SYS_IDLE &&
|
||
m_sysState != OTA_SYS_GD32_COMPLETE) {
|
||
DEBUG_WARN(TAG, "拒中继 state=%s(%d)",
|
||
stateToString(m_sysState), (int)m_sysState);
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
ota_meta_t meta;
|
||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK ||
|
||
meta.total_size == 0 ||
|
||
meta.stored_size < meta.total_size) {
|
||
DEBUG_WARN(TAG, "GD32 固件未完整存储,拒绝触发中继");
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
/* ★ 包级防呆:SHA 匹配只证明"传对了",不证明"传的是对的包"。
|
||
* 把 BOOT 包中继进 APP 区会擦掉好固件,触发前必须确认镜像身份为 APP。
|
||
* 返回 ESP_ERR_INVALID_SIZE 供调用方(WiFi/BLE)映射成具体错误提示。 */
|
||
const Gd32ImageIdentity identity = Gd32FlashStore::instance().validateStoredImageIdentity();
|
||
if (identity != Gd32ImageIdentity::kApp) {
|
||
DEBUG_ERR(TAG, "拒绝中继:GD32 固件身份校验失败 (identity=%d)", (int)identity);
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_SIZE;
|
||
}
|
||
|
||
if (OtaRelay::getState() != RELAY_IDLE &&
|
||
OtaRelay::getState() != RELAY_DONE &&
|
||
OtaRelay::getState() != RELAY_ERROR) {
|
||
DEBUG_WARN(TAG, "OtaRelay 正忙 (state=%d),拒绝触发", (int)OtaRelay::getState());
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
QueueHandle_t dq = OtaRelay::getDispatchQueue();
|
||
if (!dq) {
|
||
DEBUG_ERR(TAG, "OtaRelay dispatchQueue 未就绪");
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
/* 先入队再标 RELAYING:失败时状态与 CAN 反馈都不变 */
|
||
pauseCanFeedbackTask(true);
|
||
uint32_t cmd = 1;
|
||
BaseType_t ret = xQueueSend(dq, &cmd, pdMS_TO_TICKS(100));
|
||
if (ret != pdTRUE) {
|
||
DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败");
|
||
pauseCanFeedbackTask(false);
|
||
unlock(h);
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||
m_busySinceTick = xTaskGetTickCount();
|
||
DEBUG_DBG(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||
unlock(h);
|
||
return ESP_OK;
|
||
}
|
||
|
||
/** @brief 中继 task 已真正开始:若 trigger 已标 RELAYING 则只补 pause;否则兜底设状态。 */
|
||
void OtaCoordinator::notifyRelayStarted()
|
||
{
|
||
bool h = lock();
|
||
if (h) {
|
||
if (m_sysState != OTA_SYS_GD32_RELAYING) {
|
||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||
m_busySinceTick = xTaskGetTickCount();
|
||
DEBUG_DBG(TAG, "中继已开始(兜底), 系统状态 → GD32_RELAYING");
|
||
}
|
||
pauseCanFeedbackTask(true);
|
||
}
|
||
unlock(h);
|
||
}
|
||
|
||
/** @brief 通知协调器:GD32 中继已完成,根据结果转为 COMPLETE 或 ERROR。 */
|
||
void OtaCoordinator::notifyRelayComplete(bool success)
|
||
{
|
||
bool h = lock();
|
||
if (h) {
|
||
if (success) {
|
||
m_sysState = OTA_SYS_GD32_COMPLETE;
|
||
m_gd32StorageDone = false;
|
||
m_busySinceTick = 0; // ★ COMPLETE 非忙
|
||
pauseCanFeedbackTask(false);
|
||
DEBUG_INFO(TAG, "GD32 中继完成, 系统状态 → GD32_COMPLETE");
|
||
} else {
|
||
m_sysState = OTA_SYS_ERROR;
|
||
m_busySinceTick = 0; // ★ ERROR 非忙(可被后续请求恢复)
|
||
pauseCanFeedbackTask(false);
|
||
DEBUG_ERR(TAG, "GD32 中继失败, 系统状态 → ERROR");
|
||
}
|
||
}
|
||
unlock(h);
|
||
/* 中继结束:若 BLE 仍因 WiFi OTA 挂起则恢复 */
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|
||
|
||
// ============================================================
|
||
// IRelayListener 接口实现
|
||
// ============================================================
|
||
/** @brief IRelayListener 回调:中继已开始,委托给 notifyRelayStarted()。 */
|
||
void OtaCoordinator::onRelayStarted()
|
||
{
|
||
notifyRelayStarted();
|
||
}
|
||
|
||
/** @brief IRelayListener 回调:中继已完成,委托给 notifyRelayComplete()。 */
|
||
void OtaCoordinator::onRelayComplete(bool success)
|
||
{
|
||
notifyRelayComplete(success);
|
||
}
|
||
|
||
// ============================================================
|
||
// BLE 射频共存管理
|
||
// ============================================================
|
||
void OtaCoordinator::setBleCoexListener(IBleCoexListener *listener)
|
||
{
|
||
m_bleCoexListener = listener;
|
||
}
|
||
|
||
void OtaCoordinator::notifyBleCoexStart()
|
||
{
|
||
if (m_bleCoexListener) {
|
||
m_bleCoexListener->onBleOtaStart();
|
||
}
|
||
}
|
||
|
||
void OtaCoordinator::notifyBleCoexEnd()
|
||
{
|
||
if (m_bleCoexListener) {
|
||
m_bleCoexListener->onBleOtaEnd();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// ESP32 OTA 阶段
|
||
// ============================================================
|
||
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t channel)
|
||
{
|
||
bool h;
|
||
esp_err_t pre = beginLockedWithChecks("ESP32 OTA", h);
|
||
if (pre != ESP_OK) return pre;
|
||
|
||
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
|
||
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
|
||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||
if (m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||
if (m_otaChannel != channel) {
|
||
DEBUG_WARN(TAG, "ESP32收中 ch=%d 拒跨通道 ch=%d",
|
||
(int)m_otaChannel, (int)channel);
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
DEBUG_WARN(TAG, "ESP32同通道重传:中止旧会话");
|
||
doAbortLocked(); // abort core + IDLE + 恢复 CAN 反馈
|
||
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
|
||
}
|
||
|
||
// ★ FINALIZING 残留自愈:SHA 不匹配时 core 已 abort,但旧固件可能未把协调器
|
||
// 复位到 IDLE。core 已空闲 ⇒ 协调器状态是假占用,允许清掉后开新会话。
|
||
// core 仍 active ⇒ 真正在收尾,拒绝(不可 forceCleanup,防并发写分区)。
|
||
if (m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||
if (!Esp32OtaCore::instance().isActive()) {
|
||
DEBUG_WARN(TAG, "FINALIZING残留(core空闲),强制复位");
|
||
doAbortLocked();
|
||
} else {
|
||
DEBUG_WARN(TAG, "系统忙 (ESP32_FINALIZING, core 活跃),拒绝 ESP32 OTA");
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
}
|
||
|
||
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 重启中,均拒绝。
|
||
if (m_sysState == OTA_SYS_GD32_STORING ||
|
||
m_sysState == OTA_SYS_GD32_RELAYING ||
|
||
m_sysState == OTA_SYS_ESP32_REBOOTING) {
|
||
DEBUG_WARN(TAG, "忙 state=%d(%s) 拒ESP32 OTA",
|
||
(int)m_sysState, stateToString(m_sysState));
|
||
unlock(h);
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
// 委托 Esp32OtaCore 处理分区/互斥逻辑
|
||
esp_err_t err = Esp32OtaCore::instance().begin(total_size);
|
||
if (err == ESP_OK) {
|
||
m_otaEpoch++; // ★ 新会话开始,递增 epoch(使旧 handler 的 abortCurrentIfEpoch 失效)
|
||
m_otaChannel = channel; // ★ 记录发起通道
|
||
m_sysState = OTA_SYS_ESP32_RECEIVING;
|
||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
|
||
pauseCanFeedbackTask(true);
|
||
} else {
|
||
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
|
||
}
|
||
|
||
unlock(h);
|
||
return err;
|
||
}
|
||
|
||
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel)
|
||
{
|
||
/* WiFi 路径:在 begin 前挂 BLE,把 BT 堆腾给 OTA 收流(BLE 通道 OTA 不挂)。 */
|
||
wifiOtaSuspendBleIfNeeded(channel);
|
||
|
||
esp_err_t err = beginEsp32Ota(total_size, channel);
|
||
if (err == ESP_ERR_INVALID_STATE) {
|
||
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
|
||
// 这种情况只可能是"锁残留"——前一次 OTA 异常退出没释放全局互斥,
|
||
// 而协调器状态已回 IDLE。
|
||
// 若系统正处于任何活跃 OTA(ESP32/GD32),绝不强制清理——那会
|
||
// 把另一通道正在进行的 esp_ota_handle 中止、释放互斥,导致
|
||
// 两通道并发写同一 OTA 分区(之前 BLE 与 WiFi 可同时 OTA 的根因)。
|
||
// 避免连续 isIdle() + getSystemState() 各加锁一次(共 2 次锁)。
|
||
ota_system_state_t cur = getSystemState();
|
||
if (!isIdleState(cur) && cur != OTA_SYS_ERROR) {
|
||
DEBUG_WARN(TAG, "ESP32拒且非空闲 state=%d,不强制清",
|
||
(int)cur);
|
||
if (channel == OTA_CHANNEL_WIFI) {
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|
||
return err;
|
||
}
|
||
|
||
if (cur == OTA_SYS_ERROR) {
|
||
DEBUG_WARN(TAG, "ESP32拒但ERROR,清残留后重试");
|
||
} else {
|
||
DEBUG_WARN(TAG, "ESP32拒但空闲,判锁残留,强制清后重试");
|
||
}
|
||
/* ★ 若 motor OTA 的 feedTask 残留活跃(s_feed_running 仍 true,
|
||
* 常见于 cancel 请求未送达或 feedTask 卡在 SPI transact),
|
||
* 必须先 motorOtaCancel() 让 feedTask 退出,否则 forceCleanup
|
||
* 只清 ESP32 锁、motorOtaIsActive() 仍 true,重试时又被拒,
|
||
* 形成日志里"强制清后重试→电机 OTA 进行中→再拒"的死循环。 */
|
||
if (motorOtaIsActive()) {
|
||
DEBUG_WARN(TAG, "强制清前先取消残留的电机 OTA feedTask");
|
||
motorOtaCancel();
|
||
}
|
||
Esp32OtaCore::instance().forceCleanup();
|
||
err = beginEsp32Ota(total_size, channel);
|
||
}
|
||
if (err != ESP_OK && channel == OTA_CHANNEL_WIFI) {
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|
||
return err;
|
||
}
|
||
|
||
// ★ 已持锁版本的 ESP32 状态切换 helper:设置 m_sysState 并打印一条 INFO 日志。
|
||
// 供 notifyEsp32*Complete / notifyEsp32Rebooting 等锁内路径复用,避免重复样板代码。
|
||
void OtaCoordinator::setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg)
|
||
{
|
||
m_sysState = s;
|
||
/* REBOOTING 不参与 busy watchdog,清零起点避免误中止 */
|
||
if (s == OTA_SYS_ESP32_REBOOTING) {
|
||
m_busySinceTick = 0;
|
||
} else if (s == OTA_SYS_ESP32_FINALIZING && m_busySinceTick == 0) {
|
||
m_busySinceTick = xTaskGetTickCount();
|
||
}
|
||
DEBUG_DBG(TAG, "%s", dbg_msg);
|
||
}
|
||
|
||
/** @brief 通知协调器:ESP32 固件接收完毕,状态转为 FINALIZING。 */
|
||
void OtaCoordinator::notifyEsp32ReceiveComplete()
|
||
{
|
||
bool h = lock();
|
||
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||
setEsp32StateLocked(OTA_SYS_ESP32_FINALIZING,
|
||
"ESP32 固件接收完毕, 系统状态 → ESP32_FINALIZING");
|
||
}
|
||
unlock(h);
|
||
}
|
||
|
||
/** @brief 通知协调器:ESP32 OTA 收尾完成(SHA验证+标记启动),状态转为 REBOOTING。 */
|
||
void OtaCoordinator::notifyEsp32FinalizeComplete()
|
||
{
|
||
bool h = lock();
|
||
if (h) {
|
||
setEsp32StateLocked(OTA_SYS_ESP32_REBOOTING,
|
||
"ESP32 OTA 收尾完成, 系统状态 → ESP32_REBOOTING");
|
||
}
|
||
unlock(h);
|
||
}
|
||
|
||
/** @brief 通知协调器:ESP32 即将重启。 */
|
||
void OtaCoordinator::notifyEsp32Rebooting()
|
||
{
|
||
bool h = lock();
|
||
if (h) {
|
||
setEsp32StateLocked(OTA_SYS_ESP32_REBOOTING,
|
||
"ESP32 即将重启, 系统状态 → ESP32_REBOOTING");
|
||
}
|
||
unlock(h);
|
||
}
|
||
|
||
// ============================================================
|
||
// ESP32 OTA 收尾+重启序列(BLE/WiFi 共用封装)
|
||
// 关键:finalize() 内部已 end+set_boot+释放锁,reboot=false 返回后
|
||
// m_active 仍为 true、m_ended 为 true,调用方可在重启前自由查询状态。
|
||
// ============================================================
|
||
Esp32FinalizeResult OtaCoordinator::finalizeForReboot(const char *frontend_sha_hex)
|
||
{
|
||
Esp32FinalizeResult r = {};
|
||
notifyEsp32ReceiveComplete();
|
||
// reboot=false:让 core.finalize 真正返回,由调用方上报 device_sha 后再调 restartEsp32
|
||
r.err = Esp32OtaCore::instance().finalize(frontend_sha_hex,
|
||
/*reboot=*/false,
|
||
r.device_sha_hex);
|
||
if (r.err == ESP_OK) {
|
||
notifyEsp32FinalizeComplete();
|
||
} else {
|
||
/* finalize 失败:复位协调器到 IDLE */
|
||
DEBUG_ERR(TAG, "ESP32 finalize 失败: %s,复位协调器 → IDLE",
|
||
esp_err_to_name(r.err));
|
||
abortCurrent();
|
||
}
|
||
return r;
|
||
}
|
||
|
||
void OtaCoordinator::restartEsp32()
|
||
{
|
||
notifyEsp32Rebooting();
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
esp_restart();
|
||
// 不会返回
|
||
}
|
||
|
||
// ============================================================
|
||
// 中止
|
||
// ============================================================
|
||
void OtaCoordinator::doAbortLocked()
|
||
{
|
||
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d, epoch=%u)", (int)m_sysState, m_otaEpoch);
|
||
|
||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||
Gd32FlashStore::instance().metaClear();
|
||
Gd32FlashStore::releaseAlignBufForOta(); // 释放 OTA 期对齐缓冲
|
||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||
/* 中继中止必须 CANCEL 远端,否则 GD32 停在 WAITING_* 半会话 */
|
||
OtaRelay::instance().abortRemoteAndReset(true);
|
||
Gd32FlashStore::releaseAlignBufForOta(); // 中继阶段也可能写过 flash
|
||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||
Esp32OtaCore::instance().abort();
|
||
}
|
||
|
||
pauseCanFeedbackTask(false);
|
||
m_sysState = OTA_SYS_IDLE;
|
||
m_gd32StorageDone = false;
|
||
m_busySinceTick = 0;
|
||
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
||
/* 释放协调器锁后由调用方 resume BLE;doAbortLocked 可能持锁,标记后外层 resume */
|
||
}
|
||
|
||
void OtaCoordinator::abortCurrent()
|
||
{
|
||
bool h = lock();
|
||
if (h) {
|
||
doAbortLocked();
|
||
}
|
||
unlock(h);
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|
||
|
||
void OtaCoordinator::healOrphanStates()
|
||
{
|
||
bool need_resume = false;
|
||
bool h = lock();
|
||
if (h) {
|
||
ota_system_state_t before = m_sysState;
|
||
watchdogCheckLocked(); // 超时则 doAbortLocked → IDLE
|
||
// finalize 失败后 core 已空、协调器仍 FINALIZING 的假占用:status 轮询即可自愈
|
||
if (m_sysState == OTA_SYS_ESP32_FINALIZING &&
|
||
!Esp32OtaCore::instance().isActive()) {
|
||
DEBUG_WARN(TAG, "status 路径发现 ESP32_FINALIZING 残留,自愈 → IDLE");
|
||
doAbortLocked();
|
||
}
|
||
/* ERROR 非忙:status 轮询软清到 IDLE,避免 UI 长期卡 ERROR(新 begin 本也可覆盖) */
|
||
if (m_sysState == OTA_SYS_ERROR) {
|
||
DEBUG_INFO(TAG, "status 路径 ERROR → IDLE(软清)");
|
||
m_sysState = OTA_SYS_IDLE;
|
||
m_busySinceTick = 0;
|
||
m_gd32StorageDone = false;
|
||
pauseCanFeedbackTask(false);
|
||
}
|
||
// 任意自愈到 IDLE:恢复 WiFi OTA 期间挂起的 BLE(watchdog 旧路径漏 resume)
|
||
if (before != OTA_SYS_IDLE && m_sysState == OTA_SYS_IDLE) {
|
||
need_resume = true;
|
||
}
|
||
unlock(h);
|
||
}
|
||
if (need_resume) {
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|
||
}
|
||
|
||
uint32_t OtaCoordinator::getOtaEpoch() const
|
||
{
|
||
bool h = lock();
|
||
uint32_t epoch = m_otaEpoch;
|
||
unlock(h);
|
||
return epoch;
|
||
}
|
||
|
||
bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
|
||
{
|
||
bool h = lock();
|
||
if (!h) return false;
|
||
|
||
if (m_otaEpoch != epoch) {
|
||
// ★ epoch 不匹配:新会话已接管,旧 handler 不应中止
|
||
unlock(h);
|
||
return false;
|
||
}
|
||
|
||
// epoch 匹配:执行中止(共享 doAbortLocked,已持锁)
|
||
doAbortLocked();
|
||
|
||
unlock(h);
|
||
wifiOtaResumeBleIfNeeded();
|
||
return true;
|
||
}
|
||
|
||
// ============================================================
|
||
// 状态查询
|
||
// ============================================================
|
||
ota_system_state_t OtaCoordinator::getSystemState() const
|
||
{
|
||
// 对于纯读取,短暂加锁保证一致性
|
||
// m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast
|
||
bool h = lock();
|
||
ota_system_state_t s = m_sysState;
|
||
unlock(h);
|
||
return s;
|
||
}
|
||
|
||
ota_channel_t OtaCoordinator::getOtaChannel() const
|
||
{
|
||
bool h = lock();
|
||
ota_channel_t ch = m_otaChannel;
|
||
unlock(h);
|
||
return ch;
|
||
}
|
||
|
||
/** @brief 判断给定状态值是否属于空闲态(不加锁,纯判断)。
|
||
* 供持锁路径或已取一次状态的调用方复用,避免连续加锁。 */
|
||
bool OtaCoordinator::isIdleState(ota_system_state_t s) const
|
||
{
|
||
return (s == OTA_SYS_IDLE ||
|
||
s == OTA_SYS_GD32_STORED ||
|
||
s == OTA_SYS_GD32_COMPLETE);
|
||
}
|
||
|
||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||
bool OtaCoordinator::isIdle() const
|
||
{
|
||
return isIdleState(getSystemState());
|
||
}
|
||
|
||
/** @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
|
||
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。 */
|
||
const char* OtaCoordinator::stateToString(ota_system_state_t s)
|
||
{
|
||
switch (s) {
|
||
case OTA_SYS_IDLE: return "IDLE";
|
||
case OTA_SYS_GD32_STORING: return "GD32_STORING";
|
||
case OTA_SYS_GD32_STORED: return "GD32_STORED";
|
||
case OTA_SYS_GD32_RELAYING: return "GD32_RELAYING";
|
||
case OTA_SYS_GD32_COMPLETE: return "GD32_COMPLETE";
|
||
case OTA_SYS_ESP32_RECEIVING: return "ESP32_RECEIVING";
|
||
case OTA_SYS_ESP32_FINALIZING: return "ESP32_FINALIZING";
|
||
case OTA_SYS_ESP32_REBOOTING: return "ESP32_REBOOTING";
|
||
case OTA_SYS_ERROR: return "ERROR";
|
||
default: return "UNKNOWN";
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// OTA 状态快照
|
||
// ============================================================
|
||
OtaSnapshot OtaCoordinator::getSnapshot() const
|
||
{
|
||
OtaSnapshot snap{};
|
||
|
||
/* 持锁一次读取状态并生成字符串,保证快照字段一致 */
|
||
bool h = lock();
|
||
if (h) {
|
||
snap.sys_state = m_sysState;
|
||
snap.channel = m_otaChannel;
|
||
snap.storage_done = m_gd32StorageDone;
|
||
snap.sys_state_str = stateToString(snap.sys_state);
|
||
unlock(h);
|
||
} else {
|
||
// 加锁失败时也填一个与默认 sys_state(=IDLE) 一致的字符串
|
||
snap.sys_state_str = stateToString(snap.sys_state);
|
||
}
|
||
|
||
// ── GD32 NVS 元数据 ──
|
||
Gd32FlashStore::instance().metaLoad(&snap.meta);
|
||
|
||
// ── relay 进度(一次 relay mutex)──
|
||
snap.relay = OtaRelay::getSnapshot();
|
||
|
||
return snap;
|
||
}
|
||
|
||
// ============================================================
|
||
// GD32 APP 探测
|
||
// ============================================================
|
||
esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime)
|
||
{
|
||
/* 统一走 OtaRelay 公开探测,避免与 probeAndCacheAppAlive 双份逻辑 */
|
||
uint32_t uptime = 0;
|
||
if (OtaRelay::probeAndCacheAppAlive(500, &uptime)) {
|
||
if (out_version) {
|
||
*out_version = OtaRelay::getSnapshot().version;
|
||
}
|
||
if (out_uptime) {
|
||
*out_uptime = uptime;
|
||
}
|
||
DEBUG_INFO(TAG, "GD32 APP 探测成功: alive, uptime=%" PRIu32 "s", uptime);
|
||
return ESP_OK;
|
||
}
|
||
if (out_version) {
|
||
*out_version = gd32_version_t{};
|
||
}
|
||
if (out_uptime) {
|
||
*out_uptime = 0;
|
||
}
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
// ============================================================
|
||
// 完全复位
|
||
// ============================================================
|
||
void OtaCoordinator::reset()
|
||
{
|
||
// ★ 与 abortCurrent 共用 doAbortLocked:清 Esp32OtaCore / relay / meta,勿只改枚举
|
||
bool h = lock();
|
||
if (h) {
|
||
doAbortLocked();
|
||
DEBUG_INFO(TAG, "协调器已完全复位(含子系统 abort)");
|
||
}
|
||
unlock(h);
|
||
wifiOtaResumeBleIfNeeded();
|
||
}
|