feat(ota): 会话 epoch/通道标识 + BLE 中止命令 + NVS 线程安全
- 新增 ota_channel_t(WiFi/BLE) + m_otaEpoch 会话代次, abortCurrentIfEpoch 防止同类型重启后旧 handler 误清新会话 - 同通道再次发起 OTA 时自动中止旧会话并重开 - 统一 prepareGd32Storage(validate+begin+erase+meta), 消除 BLE/WiFi 两路径隐式差异 - OtaRelay 拆分子步骤(doRelayPackets/doFinishAndVerify), 状态成员私有化消除数据竞争 - Gd32FlashStore NVS 读写加 mutex,metaUpdate 原子化 - BLE OTA 新增 0xFB 中止命令(保持连接)+ 0x20 ABORTED 处理 - HTTP handler 改 per-request buffer 支持并发 - 修复 CRITICAL 状态 RSSI 恢复无法降级的缺陷 - main.cpp 关键任务失败触发看门狗重启 - style.css 增强动画 + prefers-reduced-motion 兼容
This commit is contained in:
@@ -67,7 +67,10 @@ extern "C" void app_main(void)
|
||||
0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:负责温度与 WiFi RSSI 上报,缺失会让运维陷入盲区,
|
||||
// 无法及时发现硬件异常,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 health_monitor_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "健康监控任务已创建");
|
||||
}
|
||||
@@ -99,7 +102,10 @@ extern "C" void app_main(void)
|
||||
0 // 核0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:OTA 中继是本设备核心职责(经 SPI 派发固件分片到目标 MCU),
|
||||
// 缺失则 OTA 刷写主流程彻底瘫痪,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 ota_relay_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "OTA 中继任务已创建");
|
||||
}
|
||||
@@ -115,7 +121,10 @@ extern "C" void app_main(void)
|
||||
0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:WiFi OTA 是 OTA 刷写主通道之一,缺失则 WiFi 侧固件升级不可用,
|
||||
// 系统失去主要刷写路径,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 wifi_ota_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "WiFi OTA 任务已创建");
|
||||
}
|
||||
@@ -131,7 +140,12 @@ extern "C" void app_main(void)
|
||||
0 // ESP32-C3 单核,固定核0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:BLE OTA 是 OTA 刷写主通道之一(与 WiFi OTA 互为冗余),
|
||||
// 缺失则 BLE 侧固件升级完全不可用,必须重启恢复
|
||||
// 注:此处为 BLE OTA 主服务任务,区别于 ble_ota.cpp 内部的
|
||||
// ble_ota_data_worker 子任务(后者可降级,不在本文件创建)
|
||||
DEBUG_ERR("MAIN", "创建 ble_ota_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* app_config.h - 统一配置文件(C++ 版本)
|
||||
* ESP32-C3 OTA 中继项目
|
||||
*
|
||||
@@ -59,7 +59,7 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< HTTP 服
|
||||
// OTA 配置
|
||||
// ============================================================
|
||||
inline constexpr size_t OTA_BLOCK_SIZE = 8192;
|
||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB,对齐 GD32 APP_MAX_SIZE (0x36000)
|
||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB = 221184B = 0x35F90,固件大小上限
|
||||
inline constexpr size_t GD32_OTA_PACKET_SIZE = 48; // GD32 OTA 实际转发包载荷
|
||||
inline constexpr uint32_t OTA_POLL_RETRIES = 40; // 每包 ACK 轮询次数
|
||||
inline constexpr uint32_t OTA_RESEND_RETRIES = 10; // 每包重发次数上限
|
||||
@@ -98,6 +98,7 @@ inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7; ///< BLE 控
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8; ///< BLE 控制命令:触发 ESP32 本机 OTA
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9; ///< BLE 控制命令:SHA-256 哈希验证
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_RESTART_ESP32 = 0xFA; ///< BLE 控制命令:重启 ESP32
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_ABORT_OTA = 0xFB; ///< BLE 控制命令:中止当前 OTA(不断开 BLE)
|
||||
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; ///< BLE 数据通道:GD32 固件包标识
|
||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; ///< BLE 数据通道:ESP32 固件包标识
|
||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; ///< BLE 单包数据载荷大小(字节)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 33
|
||||
#define FW_VERSION_STRING "0.3.33"
|
||||
#define FW_VERSION_PATCH 34
|
||||
#define FW_VERSION_STRING "0.3.34"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-06-25 01:39:58"
|
||||
#define BUILD_TIME "2026-06-28 04:26:17"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "7a41eac"
|
||||
#define GIT_HASH "6ef9730"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* debug_log.cpp - 调试日志输出实现
|
||||
*
|
||||
* 日志会同时写入三处:
|
||||
@@ -120,6 +120,8 @@ char* DebugLog::ringDrainJson(int *out_len)
|
||||
int count = ringCount();
|
||||
int capacity = count * (LOG_RING_SIZE * 2) + 16;
|
||||
if (capacity < 16) capacity = 16;
|
||||
// 当 count 较大(如 64)时 capacity 可能达 32KB+,单次 malloc 有风险。
|
||||
// 当前为简化实现保留单次 malloc;若后续日志量增大,可改为按 8KB 分块 realloc。
|
||||
char *buf = (char*)malloc(capacity);
|
||||
if (!buf) return NULL;
|
||||
|
||||
@@ -130,10 +132,22 @@ char* DebugLog::ringDrainJson(int *out_len)
|
||||
int entryLen = 0;
|
||||
|
||||
while (ringPopOldest(entry, sizeof(entry), &entryLen) && entryLen > 0) {
|
||||
if (!first) buf[pos++] = ',';
|
||||
// 逗号写入前做边界检查,预留 4 字节给 ']' + '\0' 及末尾余量
|
||||
if (!first) {
|
||||
if (pos + 1 >= capacity - 4) break;
|
||||
buf[pos++] = ',';
|
||||
}
|
||||
first = false;
|
||||
buf[pos++] = '"';
|
||||
|
||||
// 单条日志转义后超过 LOG_RING_SIZE * 2 时截断,标记 "..." 后缀
|
||||
int content_start = pos;
|
||||
bool truncated = false;
|
||||
for (int i = 0; i < entryLen && pos < capacity - 4; i++) {
|
||||
if (pos - content_start >= LOG_RING_SIZE * 2) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
char c = entry[i];
|
||||
if (c == '"') { buf[pos++] = '\\'; buf[pos++] = '"'; }
|
||||
else if (c == '\\') { buf[pos++] = '\\'; buf[pos++] = '\\'; }
|
||||
@@ -142,6 +156,14 @@ char* DebugLog::ringDrainJson(int *out_len)
|
||||
else if (c == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; }
|
||||
else if ((unsigned char)c >= 0x20) { buf[pos++] = c; }
|
||||
}
|
||||
if (truncated) {
|
||||
// 确保 "..." + '"' + ']' + '\0' (共 6 字节) 有空间才写入标记
|
||||
if (pos + 6 <= capacity) {
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
}
|
||||
}
|
||||
buf[pos++] = '"';
|
||||
}
|
||||
buf[pos++] = ']';
|
||||
@@ -216,6 +238,8 @@ esp_err_t DebugLog::uartInit()
|
||||
}
|
||||
|
||||
m_uartInitialized = true;
|
||||
// 标记 UART 真正可用,send() 依据此标志决定是否调用 uart_write_bytes
|
||||
m_uartAvailable = true;
|
||||
ESP_LOGD(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)",
|
||||
UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_DEBUG_BAUD_RATE);
|
||||
|
||||
@@ -255,7 +279,13 @@ esp_err_t DebugLog::init()
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uartInit();
|
||||
// 检查 uartInit 返回值;失败时仍让 ring buffer 模式工作,
|
||||
// 但记录"UART 不可用"日志,并在 send() 中跳过 uart_write_bytes 调用。
|
||||
esp_err_t uart_ret = uartInit();
|
||||
if (uart_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "UART 不可用,仅 ring buffer 模式 (err=%s)", esp_err_to_name(uart_ret));
|
||||
m_uartAvailable = false;
|
||||
}
|
||||
|
||||
m_logRingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
@@ -302,8 +332,8 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
||||
esp_err_t udp_ret = ESP_FAIL;
|
||||
esp_err_t uart_ret = ESP_FAIL;
|
||||
|
||||
// UART 通道
|
||||
if (m_uartInitialized) {
|
||||
// UART 通道(UART 不可用时跳过 uart_write_bytes 调用)
|
||||
if (m_uartAvailable) {
|
||||
uart_ret = uartSend(data, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* debug_log.h - 调试日志输出
|
||||
*
|
||||
* 当前日志会并行写入:
|
||||
@@ -95,6 +95,9 @@ private:
|
||||
struct sockaddr_in m_debugAddr;
|
||||
bool m_initialized = false;
|
||||
bool m_uartInitialized = false;
|
||||
// 记录 UART 是否真正可用(uartInit 失败时为 false,跳过 uart_write_bytes)。
|
||||
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
|
||||
bool m_uartAvailable = false;
|
||||
|
||||
static constexpr int LOG_RING_SLOTS = 64;
|
||||
static constexpr int LOG_RING_SIZE = 256;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* health_monitor.cpp - 系统健康监控
|
||||
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
|
||||
*
|
||||
@@ -46,6 +46,21 @@ void HealthMonitor::init()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 反初始化:停止并删除温度监控定时器,回收 esp_timer 资源。
|
||||
* 当前 HealthMonitor 任务为无限循环,无显式删除路径,
|
||||
* 本方法预留给未来停用场景(如安全关机后停止监控)使用。
|
||||
*/
|
||||
void HealthMonitor::deinit()
|
||||
{
|
||||
if (m_tempTimer != nullptr) {
|
||||
esp_timer_stop(m_tempTimer);
|
||||
esp_timer_delete(m_tempTimer);
|
||||
m_tempTimer = nullptr;
|
||||
DEBUG_INFO("HEALTH", "温度监控定时器已停止并删除");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 线程安全的健康状态读写
|
||||
// ============================================================
|
||||
@@ -55,10 +70,8 @@ void HealthMonitor::init()
|
||||
*/
|
||||
health_state_t HealthMonitor::getState()
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
health_state_t s = m_healthState;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
return s;
|
||||
LockGuard lock(m_mutex);
|
||||
return m_healthState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,9 +80,8 @@ health_state_t HealthMonitor::getState()
|
||||
*/
|
||||
void HealthMonitor::setState(health_state_t newState)
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
LockGuard lock(m_mutex);
|
||||
m_healthState = newState;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -90,10 +102,8 @@ health_state_t HealthMonitor::getCurrent()
|
||||
*/
|
||||
float HealthMonitor::getLastTemperature()
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
float t = m_lastTemperature;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
return t;
|
||||
LockGuard lock(m_mutex);
|
||||
return m_lastTemperature;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,10 +114,13 @@ float HealthMonitor::getLastTemperature()
|
||||
*/
|
||||
bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
int64_t lastMs = m_lastWifiRssiMs;
|
||||
int8_t rssi = m_lastWifiRssi;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
int64_t lastMs;
|
||||
int8_t rssi;
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
lastMs = m_lastWifiRssiMs;
|
||||
rssi = m_lastWifiRssi;
|
||||
}
|
||||
|
||||
if (lastMs <= 0) {
|
||||
return false;
|
||||
@@ -138,9 +151,10 @@ void HealthMonitor::tempMonitorCallback(void *arg)
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.m_mutex) xSemaphoreTake(self.m_mutex, portMAX_DELAY);
|
||||
self.m_lastTemperature = temp;
|
||||
if (self.m_mutex) xSemaphoreGive(self.m_mutex);
|
||||
{
|
||||
LockGuard lock(self.m_mutex);
|
||||
self.m_lastTemperature = temp;
|
||||
}
|
||||
|
||||
if (temp >= TEMP_CRITICAL) {
|
||||
self.setState(SYS_SAFE_SHUTDOWN);
|
||||
@@ -180,17 +194,17 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
}
|
||||
|
||||
// 定时器,每1秒读取一次温度
|
||||
esp_timer_handle_t temp_timer;
|
||||
esp_timer_create_args_t timer_args = {};
|
||||
timer_args.callback = &tempMonitorCallback;
|
||||
timer_args.arg = nullptr;
|
||||
timer_args.name = "temp_monitor";
|
||||
timer_args.dispatch_method = ESP_TIMER_TASK;
|
||||
|
||||
if (esp_timer_create(&timer_args, &temp_timer) == ESP_OK) {
|
||||
esp_timer_start_periodic(temp_timer, 1000000); // 1秒
|
||||
if (esp_timer_create(&timer_args, &self.m_tempTimer) == ESP_OK) {
|
||||
esp_timer_start_periodic(self.m_tempTimer, 1000000); // 1秒
|
||||
DEBUG_INFO("MON", "温度监控定时器已启动 (1秒间隔)");
|
||||
} else {
|
||||
self.m_tempTimer = nullptr;
|
||||
DEBUG_ERR("MON", "创建温度监控定时器失败");
|
||||
}
|
||||
|
||||
@@ -246,7 +260,12 @@ health_status_t HealthMonitor::checkBeforeOta()
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
|
||||
}
|
||||
|
||||
if (!m_wifiConnected) {
|
||||
bool wifi_connected;
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
wifi_connected = m_wifiConnected;
|
||||
}
|
||||
if (!wifi_connected) {
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接");
|
||||
return HEALTH_MON_CRITICAL;
|
||||
}
|
||||
@@ -320,6 +339,7 @@ health_status_t HealthMonitor::checkBeforeOtaRelay()
|
||||
*/
|
||||
void HealthMonitor::setWifiConnected(bool connected)
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
m_wifiConnected = connected;
|
||||
}
|
||||
|
||||
@@ -348,10 +368,13 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
|
||||
return HEALTH_MON_WARNING;
|
||||
}
|
||||
|
||||
// RSSI 已恢复正常(>= WIFI_RSSI_NORMAL):从 WARNING 或 CRITICAL 任一状态降级到 NORMAL。
|
||||
// 历史缺陷:原仅当 cur == SYS_WARNING 时降级,导致 CRITICAL 状态下 RSSI 恢复正常无法降级。
|
||||
// 注意:SAFE_SHUTDOWN 不应被 RSSI 恢复降级,仅处理 WARNING/CRITICAL。
|
||||
health_state_t cur = getState();
|
||||
if (cur == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
|
||||
if ((cur == SYS_WARNING || cur == SYS_CRITICAL) && rssi >= WIFI_RSSI_NORMAL) {
|
||||
setState(SYS_NORMAL);
|
||||
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
|
||||
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm (从 %d 降级)", rssi, WIFI_RSSI_NORMAL, (int)cur);
|
||||
logEvent("WIFI恢复", rssi);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* health_monitor.h - 系统健康监控头文件
|
||||
*
|
||||
* HealthMonitor 类封装健康状态管理、温度监控和 WiFi RSSI 监控。
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/temperature_sensor.h"
|
||||
#include "esp_timer.h"
|
||||
#include "app_config.h"
|
||||
|
||||
// T9.3: 健康状态等级
|
||||
@@ -48,6 +49,13 @@ public:
|
||||
/** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief 反初始化:停止并删除温度监控定时器,回收 esp_timer 资源。
|
||||
* 在任务删除路径中调用;当前 HealthMonitor 任务为无限循环,
|
||||
* 无显式删除路径,本方法预留给未来停用场景使用。
|
||||
*/
|
||||
void deinit();
|
||||
|
||||
/** @brief 健康监控 FreeRTOS 任务入口。 */
|
||||
static void taskEntry(void *pvParameters);
|
||||
|
||||
@@ -97,12 +105,32 @@ private:
|
||||
|
||||
SemaphoreHandle_t m_mutex = NULL;
|
||||
temperature_sensor_handle_t m_tempHandle = NULL;
|
||||
// 温度监控 esp_timer 句柄存为成员,便于 deinit() 时停止并删除定时器,
|
||||
// 避免任务被删除后定时器仍在运行、回调继续访问 instance() 成员。
|
||||
esp_timer_handle_t m_tempTimer = nullptr;
|
||||
health_state_t m_healthState = SYS_NORMAL;
|
||||
float m_lastTemperature = 0.0f;
|
||||
int8_t m_lastWifiRssi = -127;
|
||||
int64_t m_lastWifiRssiMs = 0;
|
||||
TaskHandle_t m_taskHandle = NULL;
|
||||
// m_wifiConnected 由 m_mutex 保护(setWifiConnected 写 / checkBeforeOta 读)。
|
||||
// volatile 保留为历史兼容,实际同步由 LockGuard 提供。
|
||||
volatile bool m_wifiConnected = false; ///< WiFi 连接状态(由 WiFi 事件推送)
|
||||
|
||||
static void tempMonitorCallback(void *arg);
|
||||
|
||||
// RAII 锁守卫(与 SpiMaster::LockGuard 风格一致)
|
||||
class LockGuard {
|
||||
public:
|
||||
explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) {
|
||||
if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); }
|
||||
}
|
||||
~LockGuard() {
|
||||
if (m_lock) { xSemaphoreGive(m_lock); }
|
||||
}
|
||||
LockGuard(const LockGuard&) = delete;
|
||||
LockGuard& operator=(const LockGuard&) = delete;
|
||||
private:
|
||||
SemaphoreHandle_t m_lock;
|
||||
};
|
||||
};
|
||||
|
||||
+40
-25
@@ -69,7 +69,7 @@ Esp32OtaCore& Esp32OtaCore::instance()
|
||||
|
||||
/**
|
||||
* @brief 复位所有状态变量。
|
||||
*/
|
||||
*/
|
||||
void Esp32OtaCore::resetState()
|
||||
{
|
||||
m_active = false;
|
||||
@@ -81,6 +81,39 @@ void Esp32OtaCore::resetState()
|
||||
m_ended = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief begin() 失败后的统一清理。
|
||||
* 仅释放 mutex + 复位状态(不调 esp_ota_abort,
|
||||
* begin 失败时 m_handle 未成功创建,无需也不能 abort)。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t Esp32OtaCore::beginCleanupAndReturn(esp_err_t code)
|
||||
{
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief finalize() 失败后的统一清理。
|
||||
* 若 esp_ota_end 未成功(m_ended=false),调 esp_ota_abort 释放 handle;
|
||||
* 然后复位 m_ended、释放 mutex、复位状态,返回 code。
|
||||
* 与现有 abort() 的 m_ended 守卫语义一致,避免 end 成功后误调 abort。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t Esp32OtaCore::finalizeCleanupAndReturn(esp_err_t code)
|
||||
{
|
||||
if (!m_ended) {
|
||||
esp_ota_abort(m_handle);
|
||||
}
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 开启 OTA 会话。
|
||||
* @param total_size 固件总大小(字节)。
|
||||
@@ -125,8 +158,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
|
||||
if (m_partition == NULL) {
|
||||
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
|
||||
releaseMutex();
|
||||
return ESP_FAIL;
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 目标分区: %s, size=%u, address=0x%lx",
|
||||
@@ -135,9 +167,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
if (total_size > m_partition->size) {
|
||||
DEBUG_ERR(TAG, "固件太大: %u > 分区大小: %u",
|
||||
(unsigned)total_size, (unsigned)m_partition->size);
|
||||
m_partition = nullptr;
|
||||
releaseMutex();
|
||||
return ESP_FAIL;
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
// BLE OTA 修复:传固件实际大小而非分区全尺寸,避免全分区擦除导致
|
||||
@@ -151,10 +181,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
esp_err_t err = esp_ota_begin(m_partition, erase_size, &m_handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
|
||||
m_partition = nullptr;
|
||||
m_handle = 0;
|
||||
releaseMutex();
|
||||
return err;
|
||||
return beginCleanupAndReturn(err);
|
||||
}
|
||||
|
||||
m_received = 0;
|
||||
@@ -280,11 +307,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
esp_err_t sha_err = computeShaFromPartition();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return sha_err;
|
||||
return finalizeCleanupAndReturn(sha_err);
|
||||
}
|
||||
|
||||
char device_hex[65] = {0};
|
||||
@@ -311,11 +334,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "SHA-256 不匹配: frontend=%s device=%s",
|
||||
frontend_sha_hex, device_hex);
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
return finalizeCleanupAndReturn(ESP_ERR_INVALID_VERSION);
|
||||
}
|
||||
} else if (out_device_sha_hex) {
|
||||
// 未提供前端 SHA,但仍计算设备端 SHA 供输出
|
||||
@@ -332,11 +351,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_end 失败: %s (0x%x)", esp_err_to_name(err), (unsigned)err);
|
||||
// end 失败时仍可 abort(未真正 end)
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return err;
|
||||
return finalizeCleanupAndReturn(err);
|
||||
}
|
||||
m_ended = true; // ★ end 成功,后续不得再 abort
|
||||
|
||||
|
||||
@@ -130,6 +130,24 @@ private:
|
||||
*/
|
||||
void resetState();
|
||||
|
||||
// begin() 失败后的统一清理:释放 mutex + resetState() + 返回 code
|
||||
// 不调 esp_ota_abort(begin 失败时 m_handle 未成功创建)
|
||||
/**
|
||||
* @brief begin() 失败后的统一清理。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t beginCleanupAndReturn(esp_err_t code);
|
||||
|
||||
// finalize() 失败后的统一清理:若 m_ended=false 调 esp_ota_abort 释放 handle;
|
||||
// 再复位 m_ended + 释放 mutex + resetState() + 返回 code
|
||||
/**
|
||||
* @brief finalize() 失败后的统一清理。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t finalizeCleanupAndReturn(esp_err_t code);
|
||||
|
||||
bool m_active = false;
|
||||
esp_ota_handle_t m_handle = 0;
|
||||
const esp_partition_t *m_partition = nullptr;
|
||||
|
||||
+216
-88
@@ -124,41 +124,110 @@ void OtaCoordinator::init()
|
||||
// ============================================================
|
||||
// GD32 存储阶段
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size)
|
||||
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
bool h = lock();
|
||||
|
||||
// ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程
|
||||
if (h) watchdogCheckLocked();
|
||||
|
||||
// ★ 同类型重启:若上一次 GD32 存储仍在进行中(STORING)且来自同一通道,
|
||||
// 自动中止旧存储并重新开始。覆盖"上传过程中刷新网页再上传"场景。
|
||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||
// RELAYING 是中继阶段,不可中断,也拒绝。
|
||||
if (h && m_sysState == OTA_SYS_GD32_STORING) {
|
||||
if (m_otaChannel != channel) {
|
||||
DEBUG_WARN(TAG, "GD32 存储中(通道=%d),拒绝跨通道请求(通道=%d)",
|
||||
(int)m_otaChannel, (int)channel);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
DEBUG_WARN(TAG, "GD32 存储仍在进行中(同通道),中止旧存储并重新开始");
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
// epoch 将在下方设置 GD32_STORING 后递增
|
||||
}
|
||||
|
||||
// 系统忙检查:仅拒绝真正"在传输"的活跃状态
|
||||
// STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖
|
||||
if (h && (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING ||
|
||||
m_sysState == OTA_SYS_GD32_STORING ||
|
||||
m_sysState == OTA_SYS_GD32_RELAYING)) {
|
||||
DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),先清理再开始新存储
|
||||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),允许新存储覆盖。
|
||||
// 注意:清理 NVS meta 不在此处做,避免与紧跟的 prepareGd32Storage 内
|
||||
// clearGd32Meta + saveGd32Meta 重复一次写入,仅做状态注释说明。
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) {
|
||||
DEBUG_INFO(TAG, "清理旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
DEBUG_INFO(TAG, "覆盖旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||||
}
|
||||
|
||||
if (h) {
|
||||
m_otaEpoch++; // ★ 新会话开始,递增 epoch
|
||||
m_otaChannel = channel; // ★ 记录发起通道
|
||||
m_sysState = OTA_SYS_GD32_STORING;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING", total_size);
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING (epoch=%u, channel=%d)",
|
||||
total_size, m_otaEpoch, (int)channel);
|
||||
}
|
||||
unlock(h);
|
||||
return h ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// GD32 Flash 扇区大小(与 http_handlers.cpp 中的常量保持一致,用于擦除时向上对齐)
|
||||
static constexpr uint32_t kGd32FlashSectorSize = 4096U;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
|
||||
err = beginGd32Storage(total_size, channel);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
|
||||
return err;
|
||||
}
|
||||
|
||||
// 3. 擦除 GD32 OTA 分区(按扇区大小向上对齐)
|
||||
// ★ 原 BLE 路径未擦除分区,此处统一补齐,确保两路径行为一致。
|
||||
const uint32_t erase_size =
|
||||
((total_size + kGd32FlashSectorSize - 1U) / kGd32FlashSectorSize) * kGd32FlashSectorSize;
|
||||
DEBUG_INFO(TAG, "prepareGd32Storage: 擦除 gd32_ota 分区 %u bytes (fw=%" PRIu32 ")",
|
||||
(unsigned)erase_size, total_size);
|
||||
err = eraseGd32Partition(erase_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "prepareGd32Storage: 擦除失败: %s", esp_err_to_name(err));
|
||||
abortCurrent();
|
||||
return err;
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -175,47 +244,9 @@ void OtaCoordinator::completeGd32Storage()
|
||||
|
||||
// ============================================================
|
||||
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
|
||||
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
|
||||
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::saveGd32Meta(const ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaSave(meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::loadGd32Meta(ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaLoad(meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::clearGd32Meta()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaClear();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32])
|
||||
{
|
||||
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::finalizeGd32Checksum()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaFinalizeChecksum();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::eraseGd32Partition(uint32_t size)
|
||||
{
|
||||
return Gd32FlashStore::instance().erasePartition(size);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GD32 中继阶段
|
||||
@@ -369,25 +400,42 @@ void OtaCoordinator::notifyBleCoexEnd()
|
||||
// ============================================================
|
||||
// ESP32 OTA 阶段
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
|
||||
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
bool h = lock();
|
||||
|
||||
// ★ 卡死自愈检查
|
||||
if (h) watchdogCheckLocked();
|
||||
|
||||
// 系统忙检查:GD32 传输/中继进行中,或另一通道的 ESP32 OTA 进行中,均拒绝。
|
||||
// ★ 必须显式拒绝 ESP32_RECEIVING/FINALIZING/REBOOTING:
|
||||
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
|
||||
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
|
||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||
// FINALIZING/REBOOTING 是不可逆阶段,也拒绝。
|
||||
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||||
if (m_otaChannel != channel) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
|
||||
(int)m_otaChannel, (int)channel);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 仍在接收中(同通道),中止旧会话并重新开始");
|
||||
Esp32OtaCore::instance().abort();
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
|
||||
}
|
||||
|
||||
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 收尾/重启中,均拒绝。
|
||||
// ★ 必须显式拒绝 ESP32_FINALIZING/REBOOTING:
|
||||
// 若放行,本调用会落到 Esp32OtaCore::begin 去抢全局互斥失败而返回 INVALID_STATE,
|
||||
// 随后 beginEsp32OtaWithRetry 的 forceCleanup 会把另一通道的活跃会话
|
||||
// (esp_ota_abort + 释放互斥)误中止 → 两通道并发写同一 OTA 分区。
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
|
||||
m_sysState == OTA_SYS_GD32_RELAYING ||
|
||||
m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
|
||||
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
|
||||
(int)m_sysState, getSystemStateString());
|
||||
(int)m_sysState, stateToString(m_sysState));
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
@@ -397,9 +445,12 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
|
||||
// 委托 Esp32OtaCore 处理分区/互斥逻辑
|
||||
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(); // ★ 记录起始时刻
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING", total_size);
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING (epoch=%u, channel=%d)",
|
||||
total_size, m_otaEpoch, (int)channel);
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
|
||||
}
|
||||
@@ -409,9 +460,9 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
|
||||
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
esp_err_t err = beginEsp32Ota(total_size);
|
||||
esp_err_t err = beginEsp32Ota(total_size, channel);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
|
||||
// 这种情况只可能是"锁残留"——前一次 OTA 异常退出没释放全局互斥,
|
||||
@@ -419,15 +470,17 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
|
||||
// 若系统正处于任何活跃 OTA(ESP32/GD32),绝不强制清理——那会
|
||||
// 把另一通道正在进行的 esp_ota_handle 中止、释放互斥,导致
|
||||
// 两通道并发写同一 OTA 分区(之前 BLE 与 WiFi 可同时 OTA 的根因)。
|
||||
if (!isIdle()) {
|
||||
// 避免连续 isIdle() + getSystemState() 各加锁一次(共 2 次锁)。
|
||||
ota_system_state_t cur = getSystemState();
|
||||
if (!isIdleState(cur)) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
|
||||
(int)getSystemState());
|
||||
(int)cur);
|
||||
return err;
|
||||
}
|
||||
// 协调器空闲但 Esp32OtaCore.begin 仍被拒 → 锁残留,强制清理后重试
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝但系统空闲,判定为锁残留,强制清理状态机后重试");
|
||||
Esp32OtaCore::instance().forceCleanup();
|
||||
err = beginEsp32Ota(total_size);
|
||||
err = beginEsp32Ota(total_size, channel);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
@@ -507,29 +560,60 @@ int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)
|
||||
// ============================================================
|
||||
// 中止
|
||||
// ============================================================
|
||||
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();
|
||||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
OtaRelay::instance().reset();
|
||||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
}
|
||||
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0;
|
||||
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
||||
}
|
||||
|
||||
void OtaCoordinator::abortCurrent()
|
||||
{
|
||||
bool h = lock();
|
||||
if (h) {
|
||||
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState);
|
||||
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
OtaRelay::instance().reset();
|
||||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
}
|
||||
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0; // ★ 离开忙状态
|
||||
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
||||
doAbortLocked();
|
||||
}
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 状态查询
|
||||
// ============================================================
|
||||
@@ -543,35 +627,61 @@ ota_system_state_t OtaCoordinator::getSystemState() const
|
||||
return s;
|
||||
}
|
||||
|
||||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||||
bool OtaCoordinator::isIdle() const
|
||||
ota_channel_t OtaCoordinator::getOtaChannel() const
|
||||
{
|
||||
return (getSystemState() == OTA_SYS_IDLE ||
|
||||
getSystemState() == OTA_SYS_GD32_STORED ||
|
||||
getSystemState() == OTA_SYS_GD32_COMPLETE);
|
||||
bool h = lock();
|
||||
ota_channel_t ch = m_otaChannel;
|
||||
unlock(h);
|
||||
return ch;
|
||||
}
|
||||
|
||||
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
|
||||
bool OtaCoordinator::isGd32Busy() const
|
||||
/** @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 判断给定状态值是否属于 GD32 忙态(不加锁,纯判断)。 */
|
||||
bool OtaCoordinator::isGd32BusyState(ota_system_state_t s) const
|
||||
{
|
||||
ota_system_state_t s = getSystemState();
|
||||
return (s == OTA_SYS_GD32_STORING ||
|
||||
s == OTA_SYS_GD32_RELAYING);
|
||||
}
|
||||
|
||||
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
|
||||
bool OtaCoordinator::isEsp32Busy() const
|
||||
/** @brief 判断给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。 */
|
||||
bool OtaCoordinator::isEsp32BusyState(ota_system_state_t s) const
|
||||
{
|
||||
ota_system_state_t s = getSystemState();
|
||||
return (s == OTA_SYS_ESP32_RECEIVING ||
|
||||
s == OTA_SYS_ESP32_FINALIZING ||
|
||||
s == OTA_SYS_ESP32_REBOOTING);
|
||||
}
|
||||
|
||||
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。 */
|
||||
const char* OtaCoordinator::getSystemStateString() const
|
||||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||||
bool OtaCoordinator::isIdle() const
|
||||
{
|
||||
switch (getSystemState()) {
|
||||
return isIdleState(getSystemState());
|
||||
}
|
||||
|
||||
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
|
||||
bool OtaCoordinator::isGd32Busy() const
|
||||
{
|
||||
return isGd32BusyState(getSystemState());
|
||||
}
|
||||
|
||||
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
|
||||
bool OtaCoordinator::isEsp32Busy() const
|
||||
{
|
||||
return isEsp32BusyState(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";
|
||||
@@ -585,6 +695,13 @@ const char* OtaCoordinator::getSystemStateString() const
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。
|
||||
* 委托 stateToString(getSystemState()),保持向后兼容。 */
|
||||
const char* OtaCoordinator::getSystemStateString() const
|
||||
{
|
||||
return stateToString(getSystemState());
|
||||
}
|
||||
|
||||
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
|
||||
bool OtaCoordinator::getGd32AppAlive() const
|
||||
{
|
||||
@@ -608,14 +725,22 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
||||
{
|
||||
OtaSnapshot snap{};
|
||||
|
||||
// ── coordinator 自身状态(一次 mutex)──
|
||||
// ── coordinator 自身状态(一次 mutex,避免二次加锁导致快照不一致)──
|
||||
// 持锁时一次性取 m_sysState,并调不加锁的 stateToString()
|
||||
// 就地转字符串。旧实现释放锁后再调 getSystemStateString() 二次加锁,
|
||||
// 两次取值之间状态可能变化,导致 snap.sys_state 与 snap.sys_state_str
|
||||
// 描述不同时刻状态。
|
||||
bool h = lock();
|
||||
if (h) {
|
||||
snap.sys_state = m_sysState;
|
||||
snap.storage_done = m_gd32StorageDone;
|
||||
// 就地把状态枚举转字符串,与 sys_state 来自同一时刻
|
||||
snap.sys_state_str = stateToString(snap.sys_state);
|
||||
unlock(h);
|
||||
} else {
|
||||
// 加锁失败时也填一个与默认 sys_state(=IDLE) 一致的字符串
|
||||
snap.sys_state_str = stateToString(snap.sys_state);
|
||||
}
|
||||
snap.sys_state_str = getSystemStateString();
|
||||
|
||||
// ── GD32 NVS 元数据 ──
|
||||
Gd32FlashStore::instance().metaLoad(&snap.meta);
|
||||
@@ -629,6 +754,9 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
||||
// ============================================================
|
||||
// GD32 APP 探测(PR2: 从 /system/status 移出)
|
||||
// ============================================================
|
||||
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
|
||||
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
|
||||
// 故此处保留独立实现。
|
||||
esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime)
|
||||
{
|
||||
uint32_t uptime = 0;
|
||||
|
||||
+155
-12
@@ -19,6 +19,7 @@
|
||||
#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(H4 解耦)
|
||||
|
||||
@@ -37,6 +38,14 @@ typedef enum {
|
||||
OTA_SYS_ERROR // 错误状态
|
||||
} ota_system_state_t;
|
||||
|
||||
// ============================================================
|
||||
// OTA 传输通道标识(用于同类型重启时区分同通道/跨通道)
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
OTA_CHANNEL_WIFI = 0,
|
||||
OTA_CHANNEL_BLE = 1,
|
||||
} ota_channel_t;
|
||||
|
||||
// ============================================================
|
||||
// OTA 系统全量只读快照(PR1: 收口状态读取)
|
||||
// ============================================================
|
||||
@@ -108,9 +117,21 @@ public:
|
||||
* @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);
|
||||
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief 统一 GD32 OTA 写入前的准备工作:校验大小 + begin + 擦除分区 + 清空 meta + 写入初始 meta。
|
||||
* 供 BLE seq=0 首包 / HTTP GD32 upload(非 prepared 复用路径)调用,
|
||||
* 消除两路径对"擦除/meta 初始化"序列的隐式差异(原 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 固件接收完毕时调用。
|
||||
@@ -127,21 +148,30 @@ public:
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
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 成功。
|
||||
*/
|
||||
esp_err_t saveGd32Meta(const ota_meta_t *meta);
|
||||
inline esp_err_t saveGd32Meta(const ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaSave(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载 GD32 OTA 元数据。
|
||||
* @param meta 输出元数据结构体。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t loadGd32Meta(ota_meta_t *meta);
|
||||
inline esp_err_t loadGd32Meta(ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaLoad(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新 GD32 OTA 元数据的进度。
|
||||
@@ -149,13 +179,19 @@ public:
|
||||
* @param last_seq 最后包序号。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq);
|
||||
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 成功。
|
||||
*/
|
||||
esp_err_t clearGd32Meta();
|
||||
inline esp_err_t clearGd32Meta()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaClear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 校验前端 SHA-256 与设备端固件是否一致。
|
||||
@@ -163,21 +199,52 @@ public:
|
||||
* @param out_device_sha 输出设备端 32 字节 SHA(可为 nullptr)。
|
||||
* @return ESP_OK 匹配,ESP_ERR_INVALID_CRC 不匹配。
|
||||
*/
|
||||
esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32]);
|
||||
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 成功。
|
||||
*/
|
||||
esp_err_t finalizeGd32Checksum();
|
||||
inline esp_err_t finalizeGd32Checksum()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaFinalizeChecksum();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 擦除 GD32 OTA 分区。
|
||||
* @param size 擦除大小。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t eraseGd32Partition(uint32_t size);
|
||||
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 中继阶段 ──
|
||||
/**
|
||||
@@ -205,17 +272,19 @@ public:
|
||||
* @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);
|
||||
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);
|
||||
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief ESP32 OTA 固件接收完毕,准备收尾。
|
||||
@@ -257,6 +326,25 @@ public:
|
||||
*/
|
||||
void abortCurrent();
|
||||
|
||||
/**
|
||||
* @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 全量状态快照(PR1: 收口状态读取)。
|
||||
@@ -282,26 +370,68 @@ public:
|
||||
*/
|
||||
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 检查给定状态值是否属于 GD32 忙态(不加锁,纯判断)。
|
||||
* @param s 状态枚举值。
|
||||
* @return true 表示 STORING 或 RELAYING。
|
||||
*/
|
||||
bool isGd32BusyState(ota_system_state_t s) const;
|
||||
|
||||
/**
|
||||
* @brief 检查给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。
|
||||
* @param s 状态枚举值。
|
||||
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
|
||||
*/
|
||||
bool isEsp32BusyState(ota_system_state_t s) const;
|
||||
|
||||
/**
|
||||
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
|
||||
* 内部仍加锁取一次状态后调 isIdleState()。
|
||||
* @return true 系统空闲(IDLE/STORED/COMPLETE)。
|
||||
*/
|
||||
bool isIdle() const;
|
||||
|
||||
/**
|
||||
* @brief 检查是否有 GD32 相关操作正在进行。
|
||||
* 内部仍加锁取一次状态后调 isGd32BusyState()。
|
||||
* @return true 表示 STORING 或 RELAYING。
|
||||
*/
|
||||
bool isGd32Busy() const;
|
||||
|
||||
/**
|
||||
* @brief 检查是否有 ESP32 OTA 正在进行。
|
||||
* 内部仍加锁取一次状态后调 isEsp32BusyState()。
|
||||
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
|
||||
*/
|
||||
bool isEsp32Busy() const;
|
||||
|
||||
/**
|
||||
* @brief 获取系统状态的可读字符串描述。
|
||||
* 内部委托 stateToString(getSystemState()),保持向后兼容。
|
||||
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
|
||||
*/
|
||||
const char* getSystemStateString() const;
|
||||
@@ -385,6 +515,10 @@ private:
|
||||
// 必须在已持锁状态下调用。
|
||||
void watchdogCheckLocked();
|
||||
|
||||
// ★ 已持锁版本的中止序列:根据当前 m_sysState 调用对应子模块的清理,
|
||||
// 并把状态机复位为 IDLE。abortCurrent / abortCurrentIfEpoch 共享此实现。
|
||||
void doAbortLocked();
|
||||
|
||||
// 系统状态
|
||||
ota_system_state_t m_sysState = OTA_SYS_IDLE;
|
||||
|
||||
@@ -394,6 +528,15 @@ private:
|
||||
// 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;
|
||||
|
||||
// 互斥锁(M1/L3: mutable 允许在 const 方法中加锁,消除 const_cast)
|
||||
mutable SemaphoreHandle_t m_mutex = nullptr;
|
||||
|
||||
|
||||
+301
-252
@@ -27,6 +27,30 @@
|
||||
// ★ H4 解耦:OtaRelay 不再直接 #include "ota_coordinator.h",
|
||||
// 通过 IRelayListener 接口回调通知协调器,依赖方向变为单向。
|
||||
|
||||
// ============================================================
|
||||
// 公共拒绝事件判断(文件内工具)
|
||||
// ============================================================
|
||||
// 统一各状态 handler 头部的 ABORT/TIMEOUT/HEALTH_CRIT 拒绝判断。
|
||||
// 返回 OTA_RESULT_OK 表示非拒绝事件;其他为对应错误码。
|
||||
// allowHealth: 是否将 RELAY_EVT_HEALTH_CRIT 视为拒绝(仅 TRANSFERRING 传 true)。
|
||||
static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
|
||||
{
|
||||
if (e == RELAY_EVT_ABORT) return OTA_RESULT_ERR_USER_ABORT;
|
||||
if (e == RELAY_EVT_TIMEOUT) return OTA_RESULT_ERR_TIMEOUT;
|
||||
if (allowHealth && e == RELAY_EVT_HEALTH_CRIT) return OTA_RESULT_ERR_HEALTH;
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// doRelayPackets 重试阈值(与历史代码字面值保持一致,仅命名化)
|
||||
// ============================================================
|
||||
// 原代码中 retry_count 阈值:行 1015/1075/1084 用 10U,行 1032/1050 用 3U。
|
||||
// 含义:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
|
||||
// 结构化错误属于"GD32 报告具体错误",较少重试(3次)。
|
||||
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
|
||||
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
||||
static constexpr uint32_t kLastPacketAckSeq = 0xF0F0F0F0U; // 末包 ACK 序号约定
|
||||
|
||||
// ============================================================
|
||||
// 静态成员定义
|
||||
// ============================================================
|
||||
@@ -177,7 +201,7 @@ TaskHandle_t OtaRelay::getTaskHandle()
|
||||
// ============================================================
|
||||
ota_result_t OtaRelay::processEvent(relay_event_t event, const uint8_t *payload, size_t len)
|
||||
{
|
||||
relay_state_t cur = relayState;
|
||||
relay_state_t cur = getState();
|
||||
|
||||
// ERROR 状态:仅接受 reset(通过 handleError 的 EVT_START → IDLE)
|
||||
if (cur == RELAY_ERROR) {
|
||||
@@ -237,16 +261,11 @@ ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *pay
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKING 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKING 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKING 超时");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
return rej;
|
||||
}
|
||||
|
||||
if (event != RELAY_EVT_START && event != RELAY_EVT_RESP_OK) {
|
||||
@@ -291,16 +310,11 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG 超时");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
return rej;
|
||||
}
|
||||
|
||||
// 加载元数据获取固件大小
|
||||
@@ -324,8 +338,8 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
m_transferCurrentSeq = 0;
|
||||
m_transferFlashOffset = 0;
|
||||
m_transferRetryCount = 0;
|
||||
relayTotalPackets = m_transferTotalPackets;
|
||||
relaySeq = 0;
|
||||
setRelayTotalPackets(m_transferTotalPackets);
|
||||
setRelaySeq(0);
|
||||
|
||||
setState(RELAY_TRANSFERRING);
|
||||
DEBUG_INFO("OTA", "=== 阶段2 完成: 总包数=%" PRIu32 ", → TRANSFERRING ===", m_transferTotalPackets);
|
||||
@@ -340,16 +354,11 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "TRANSFERRING 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event, /*allowHealth=*/true);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "TRANSFERRING 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_HEALTH_CRIT) {
|
||||
DEBUG_ERR("OTA", "TRANSFERRING 健康检查危险,中止");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_HEALTH;
|
||||
return rej;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets);
|
||||
@@ -373,16 +382,11 @@ ota_result_t OtaRelay::handleFinishing(relay_event_t event, const uint8_t *paylo
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "FINISHING 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "FINISHING 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "FINISHING 超时");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
return rej;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段4-5: 结束OTA + APP验证 ===");
|
||||
@@ -469,31 +473,21 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
global_timeout_ms, (uint32_t)start_tick, (uint32_t)m_transferDeadline);
|
||||
|
||||
// ── 驱动状态机 ──
|
||||
// 步骤0: IDLE → HANDSHAKING(handleIdle 内部 reset() + setState(HANDSHAKING))
|
||||
ota_result_t res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
// 5 步顺序推进:IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
|
||||
// 每步同步执行对应 handler(内部完成协议交互并推进状态)。
|
||||
// 任一步返回非 OK 即跳出并复位。
|
||||
ota_result_t res = OTA_RESULT_OK;
|
||||
while (true) {
|
||||
relay_state_t cur = getState();
|
||||
if (cur == RELAY_DONE || cur == RELAY_ERROR) break;
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤1: HANDSHAKING(内部完成握手+认证 → SIZE_NEG)
|
||||
// ★ 注意:processEvent(START) 在 HANDSHAKING handler 内同步执行 doHandshakeReq + doChallengeAndAuth
|
||||
// 期间 SPI 阻塞轮询 8~30 秒。超时检查必须放在同步 handler 执行之后。
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
// 步骤2: SIZE_NEG(内部完成大小协商 → TRANSFERRING)
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
// 步骤3: TRANSFERRING(内部完成所有数据包 → FINISHING)
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
// 步骤4: FINISHING(结束OTA + APP验证 → DONE)
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
cleanup:
|
||||
if (res != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)relayState);
|
||||
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
|
||||
reset();
|
||||
}
|
||||
return res;
|
||||
@@ -951,6 +945,158 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
||||
* 流程:从 Flash 读取 → AES 加密 → SPI 发送 → 等待 ACK → 失败则重发。
|
||||
* 每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包执行一次健康检查。
|
||||
*/
|
||||
ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, uint8_t tx_buf[64], size_t *out_chunk_size)
|
||||
{
|
||||
uint8_t plain48[GD32_OTA_PACKET_SIZE];
|
||||
memset(plain48, 0xFF, sizeof(plain48));
|
||||
size_t chunk_size = GD32_OTA_PACKET_SIZE;
|
||||
if (seq == m_transferTotalPackets - 1) {
|
||||
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
|
||||
}
|
||||
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
|
||||
tx_buf[4] = (uint8_t)(seq & 0xFFU);
|
||||
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
|
||||
tx_buf[6] = 0x00;
|
||||
tx_buf[7] = 0x00;
|
||||
|
||||
if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq);
|
||||
return OTA_RESULT_ERR_AUTH;
|
||||
}
|
||||
|
||||
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
|
||||
tx_buf[56] = (uint8_t)(if_end & 0xFFU);
|
||||
tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU);
|
||||
tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU);
|
||||
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
|
||||
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
|
||||
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
|
||||
seq, (unsigned)chunk_size, if_end,
|
||||
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
|
||||
|
||||
m_transferCurrentSeq = seq;
|
||||
if (out_chunk_size) *out_chunk_size = chunk_size;
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size, ota_result_t *out_result)
|
||||
{
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[32] = {0};
|
||||
size_t resp_len = 0;
|
||||
uint32_t retry_count = 0;
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
const uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? kLastPacketAckSeq : (seq + 1U);
|
||||
|
||||
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
|
||||
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
|
||||
resend_packet:
|
||||
while (1) {
|
||||
resp_len = 0;
|
||||
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, 64,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||
if (err == ESP_OK) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
|
||||
if (++retry_count >= kMaxResendOnNack) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
continue; // 重发本包
|
||||
}
|
||||
|
||||
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
uint32_t err_seq = 0;
|
||||
uint8_t err_code = 0;
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
|
||||
if (err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= kMaxResendOnStructError) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool got_expected = false;
|
||||
for (uint32_t retry = 0; retry < 10; ++retry) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
|
||||
if (++retry_count >= kMaxResendOnStructError) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
got_expected = true;
|
||||
break;
|
||||
}
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (got_expected) {
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= kMaxResendOnNack) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
|
||||
if (++retry_count >= kMaxResendOnNack) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (result != OTA_RESULT_OK) {
|
||||
*out_result = result;
|
||||
return true; // 中止外层循环
|
||||
}
|
||||
return false; // 继续下一包
|
||||
}
|
||||
|
||||
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
{
|
||||
DEBUG_INFO("OTA", "开始转发 %" PRIu32 " 包", m_transferTotalPackets);
|
||||
@@ -962,13 +1108,10 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
}
|
||||
|
||||
uint8_t tx_buf[64] = {0};
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[32] = {0};
|
||||
size_t resp_len = 0;
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
|
||||
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
|
||||
uint32_t retry_count = 0;
|
||||
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
|
||||
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == m_transferTotalPackets - 1) {
|
||||
health_status_t health = HealthMonitor::instance().checkBeforeOtaRelay();
|
||||
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
|
||||
@@ -977,150 +1120,32 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
}
|
||||
}
|
||||
|
||||
// 全局超时检查
|
||||
if (deadline_tick != 0 && xTaskGetTickCount() >= deadline_tick) {
|
||||
DEBUG_ERR("OTA", "全局超时: 包 %" PRIu32 "/%" PRIu32 " 已超限,中止转发", seq, m_transferTotalPackets);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
uint8_t plain48[GD32_OTA_PACKET_SIZE];
|
||||
memset(plain48, 0xFF, sizeof(plain48));
|
||||
size_t chunk_size = GD32_OTA_PACKET_SIZE;
|
||||
if (seq == m_transferTotalPackets - 1) {
|
||||
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
|
||||
}
|
||||
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
|
||||
tx_buf[4] = (uint8_t)(seq & 0xFFU);
|
||||
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
|
||||
tx_buf[6] = 0x00;
|
||||
tx_buf[7] = 0x00;
|
||||
|
||||
if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq);
|
||||
return OTA_RESULT_ERR_AUTH;
|
||||
}
|
||||
|
||||
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
|
||||
tx_buf[56] = (uint8_t)(if_end & 0xFFU);
|
||||
tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU);
|
||||
tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU);
|
||||
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
|
||||
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
|
||||
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
|
||||
seq, (unsigned)chunk_size, if_end,
|
||||
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
|
||||
|
||||
m_transferCurrentSeq = seq;
|
||||
|
||||
resend_packet:
|
||||
while (1) {
|
||||
resp_len = 0;
|
||||
err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf),
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||
if (err == ESP_OK) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
|
||||
if (++retry_count >= 10U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
continue; // 重发本包
|
||||
}
|
||||
|
||||
uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? 0xF0F0F0F0U : (seq + 1U);
|
||||
|
||||
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
uint32_t err_seq = 0;
|
||||
uint8_t err_code = 0;
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
|
||||
if (err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= 3U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool got_expected = false;
|
||||
for (uint32_t retry = 0; retry < 10; ++retry) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
|
||||
if (++retry_count >= 3U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
got_expected = true;
|
||||
break;
|
||||
}
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (got_expected) {
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= 10U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
|
||||
if (++retry_count >= 10U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
// 准备单包:读 Flash + 加密 + 组装
|
||||
size_t chunk_size = 0;
|
||||
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
|
||||
if (result != OTA_RESULT_OK) {
|
||||
break;
|
||||
return result;
|
||||
}
|
||||
|
||||
relaySeq = seq + 1;
|
||||
if (seq % (m_transferTotalPackets / 10 + 1) == 0) {
|
||||
uint32_t progress = (relaySeq * 100U) / m_transferTotalPackets;
|
||||
DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", relaySeq, m_transferTotalPackets, progress);
|
||||
// 发送单包并等待 ACK(内含重试循环)
|
||||
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
|
||||
return result; // 中止
|
||||
}
|
||||
|
||||
setRelaySeq(seq + 1);
|
||||
// 每 5% 或前 5 包/最后 5 包打印进度,避免 9 秒+静默期无法判断是否卡死
|
||||
uint32_t step = m_transferTotalPackets / 20 + 1; // ~5% 间隔
|
||||
if (seq < 5 || seq >= m_transferTotalPackets - 5 || seq % step == 0) {
|
||||
// 进度日志直接用本地 seq+1,避免再次加锁读 relaySeq
|
||||
uint32_t done = seq + 1;
|
||||
uint32_t progress = (done * 100U) / m_transferTotalPackets;
|
||||
DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", done, m_transferTotalPackets, progress);
|
||||
}
|
||||
|
||||
// ★ 包间延迟:给 GD32 Bootloader 充足时间完成 flash 写入并准备下一包 ACK。
|
||||
@@ -1132,8 +1157,36 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA 指令并验证 GD32 APP 存活。 */
|
||||
ota_result_t OtaRelay::doFinishAndVerify()
|
||||
// ============================================================
|
||||
// ============================================================
|
||||
// 统一 OtaRelay 内部两处 APP 探测逻辑(doWaitForAppAlive 轮询 + taskEntry 启动探测)。
|
||||
// 成功:setGd32AppAlive(true) + setGd32Version + 打印版本号;失败:打印 warning。
|
||||
// 注:OtaCoordinator::probeGd32AppOnce 逻辑相似,但因依赖方向不调用本函数。
|
||||
bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
|
||||
{
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ver = {};
|
||||
|
||||
esp_err_t err = SpiMaster::instance().appPing(timeout_ms, &uptime, &ver);
|
||||
if (err == ESP_OK) {
|
||||
// 持锁一次性写入两个状态字段,保证 getSnapshot() 读取一致
|
||||
StateLockGuard lock;
|
||||
gd32AppAlive = true;
|
||||
gd32Version = ver;
|
||||
if (out_uptime) *out_uptime = uptime;
|
||||
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
uptime,
|
||||
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build,
|
||||
ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build);
|
||||
return true;
|
||||
}
|
||||
if (out_uptime) *out_uptime = 0;
|
||||
DEBUG_WARN("OTA", "GD32 APP 探测失败: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试)。 */
|
||||
ota_result_t OtaRelay::doSendFinishOta()
|
||||
{
|
||||
DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP");
|
||||
|
||||
@@ -1193,6 +1246,43 @@ ota_result_t OtaRelay::doFinishAndVerify()
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:轮询等待 GD32 APP 存活(30 次重试)。 */
|
||||
ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
|
||||
{
|
||||
// ★ APP 存活验证
|
||||
setGd32AppAlive(false);
|
||||
|
||||
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
if (probeAndCacheAppAlive(timeout_ms)) {
|
||||
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
||||
break;
|
||||
}
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!getGd32AppAlive()) {
|
||||
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA + 等待 APP 存活(顺序两阶段 + 会话清理)。 */
|
||||
ota_result_t OtaRelay::doFinishAndVerify()
|
||||
{
|
||||
// 阶段1: 发送 FINISH_OTA 并等待 ACK
|
||||
ota_result_t res = doSendFinishOta();
|
||||
if (res != OTA_RESULT_OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// 会话密钥/上下文清理
|
||||
Protocol::sessionKeyClear(&m_sessionCtx);
|
||||
memset(&m_sessionCtx, 0, sizeof(session_ctx_t));
|
||||
memset(m_kSession, 0, sizeof(m_kSession));
|
||||
@@ -1203,34 +1293,8 @@ ota_result_t OtaRelay::doFinishAndVerify()
|
||||
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
|
||||
// ★ APP 存活验证
|
||||
gd32AppAlive = false;
|
||||
|
||||
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ping_ver = {};
|
||||
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime, &ping_ver);
|
||||
if (ping_err == ESP_OK) {
|
||||
gd32AppAlive = true;
|
||||
gd32Version = ping_ver;
|
||||
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
uptime,
|
||||
ping_ver.app_major, ping_ver.app_minor, ping_ver.app_patch, ping_ver.app_build,
|
||||
ping_ver.boot_major, ping_ver.boot_minor, ping_ver.boot_patch, ping_ver.boot_build);
|
||||
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
||||
break;
|
||||
}
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!gd32AppAlive) {
|
||||
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
// 阶段2: APP 存活轮询(单次 appPing 超时 1000ms,共 30 次)
|
||||
return doWaitForAppAlive(1000);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1258,24 +1322,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
{
|
||||
DEBUG_INFO("OTA", "启动初始 GD32 探测...");
|
||||
vTaskDelay(pdMS_TO_TICKS(3000)); // 等待 GD32 APP 就绪
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ping_ver = {};
|
||||
esp_err_t ping_err = SpiMaster::instance().appPing(2000, &uptime, &ping_ver);
|
||||
if (ping_err == ESP_OK) {
|
||||
StateLockGuard lock;
|
||||
gd32AppAlive = true;
|
||||
gd32Version = ping_ver;
|
||||
DEBUG_INFO("OTA", "启动 GD32 探测成功: uptime=%" PRIu32 "s, "
|
||||
"APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
uptime,
|
||||
ping_ver.app_major, ping_ver.app_minor,
|
||||
ping_ver.app_patch, ping_ver.app_build,
|
||||
ping_ver.boot_major, ping_ver.boot_minor,
|
||||
ping_ver.boot_patch, ping_ver.boot_build);
|
||||
} else {
|
||||
DEBUG_WARN("OTA", "启动 GD32 探测失败: %s (GD32 可能未就绪)",
|
||||
esp_err_to_name(ping_err));
|
||||
}
|
||||
probeAndCacheAppAlive(2000);
|
||||
}
|
||||
|
||||
while (1) {
|
||||
@@ -1287,7 +1334,8 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
|
||||
if (s_relayListener) s_relayListener->onRelayStarted();
|
||||
IRelayListener *listener = getRelayListener();
|
||||
if (listener) listener->onRelayStarted();
|
||||
ota_result_t result = instance().forwardFirmware();
|
||||
|
||||
uint32_t result_msg = (uint32_t)result;
|
||||
@@ -1303,6 +1351,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
}
|
||||
instance().reset();
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
|
||||
if (s_relayListener) s_relayListener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
listener = getRelayListener();
|
||||
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
}
|
||||
}
|
||||
|
||||
+63
-13
@@ -224,21 +224,12 @@ public:
|
||||
|
||||
// ── Flash 操作已迁移至 Gd32FlashStore(storage/gd32_flash_store.h)──
|
||||
|
||||
// ── 公开状态成员 ──
|
||||
static relay_state_t relayState; ///< 当前状态机状态
|
||||
static uint32_t relaySeq; ///< 当前中继会话序号
|
||||
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
||||
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
||||
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
||||
static TaskHandle_t taskHandle; ///< OTA 中继任务句柄
|
||||
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
|
||||
|
||||
// ── 线程安全锁 ──
|
||||
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁
|
||||
static void initMutex(); ///< 初始化状态互斥锁(app_main 中调用)
|
||||
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
||||
|
||||
/** @brief RAII 状态锁守护类,自动获取/释放 s_stateMutex。 */
|
||||
/** @brief RAII 状态锁守护类,自动获取/释放 s_stateMutex。
|
||||
* 作为 public 嵌套类保留,内部代码在持锁块内可直接访问 private 状态成员。 */
|
||||
class StateLockGuard {
|
||||
public:
|
||||
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
|
||||
@@ -268,6 +259,23 @@ private:
|
||||
static QueueHandle_t dispatchQueue; ///< 事件分发队列
|
||||
static QueueHandle_t resultQueue; ///< 结果回传队列
|
||||
|
||||
// ── 状态成员(全部改 private,统一通过 public setter/getter 访问)──
|
||||
// 外部代码使用 getState()/getRelaySeq()/... 读取,setState()/setRelaySeq()/... 写入。
|
||||
// 内部代码:若已在 StateLockGuard 块内(reset/getSnapshot/启动探测),可直接访问;
|
||||
// 否则必须使用 setter/getter,禁止裸读写(消除数据竞争)。
|
||||
static relay_state_t relayState; ///< 当前状态机状态
|
||||
static uint32_t relaySeq; ///< 当前中继会话序号
|
||||
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
||||
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
||||
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
||||
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
|
||||
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁(由 StateLockGuard 使用)
|
||||
|
||||
// taskHandle 为 write-once-at-init 字段:仅 taskEntry 启动首行写入一次,
|
||||
// 之后只读。getTaskHandle() 无锁返回,因写入发生在任务创建后、任务循环开始前,
|
||||
// 与任何外部读取者构成 happens-before(任务创建同步),无数据竞争。
|
||||
static TaskHandle_t taskHandle; ///< OTA 中继任务句柄
|
||||
|
||||
OtaRelay() = default;
|
||||
~OtaRelay() = default;
|
||||
OtaRelay(const OtaRelay&) = delete;
|
||||
@@ -328,10 +336,44 @@ private:
|
||||
*/
|
||||
ota_result_t doRelayPackets(TickType_t deadline_tick);
|
||||
/**
|
||||
* @brief 发送结束指令并验证 APP 存活.
|
||||
* @brief 准备单包:读 Flash + 组装 tx_buf + 加密 + CRC(doRelayPackets 子步骤)。
|
||||
* @param seq 当前包序号。
|
||||
* @param meta 固件元数据(用于计算末包 chunk_size)。
|
||||
* @param tx_buf 输出缓冲区(64 字节)。
|
||||
* @param out_chunk_size 输出本包有效字节数(用于 sendRelayPacketWithRetry 推进 offset)。
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_STORAGE 读 Flash 失败,OTA_RESULT_ERR_AUTH 加密失败。
|
||||
*/
|
||||
ota_result_t prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, uint8_t tx_buf[64], size_t *out_chunk_size);
|
||||
/**
|
||||
* @brief 发送单包并等待 ACK(doRelayPackets 子步骤)。
|
||||
*
|
||||
* 内含重试循环,使用以下命名常量(与历史阈值保持一致):
|
||||
* kMaxResendOnNack=10(ACK 失败/轮询耗尽重发上限)
|
||||
* kMaxResendOnStructError=3(结构化错误重发上限)
|
||||
*
|
||||
* @param seq 当前包序号。
|
||||
* @param tx_buf 已组装好的 64 字节发送缓冲区。
|
||||
* @param chunk_size 本包有效字节数(成功时用于推进 m_transferFlashOffset)。
|
||||
* @param[out] out_result 失败时写入错误码(OK 时不修改)。
|
||||
* @return true 应中止外层循环(result 已设);false 继续下一包。
|
||||
*/
|
||||
bool sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size, ota_result_t *out_result);
|
||||
/**
|
||||
* @brief 发送结束指令并验证 APP 存活(顺序调用 doSendFinishOta + doWaitForAppAlive).
|
||||
* @return OTA_RESULT_OK 成功,负值表示错误。
|
||||
*/
|
||||
ota_result_t doFinishAndVerify();
|
||||
/**
|
||||
* @brief 发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试).
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_GD32 失败。
|
||||
*/
|
||||
ota_result_t doSendFinishOta();
|
||||
/**
|
||||
* @brief 轮询等待 GD32 APP 存活(30 次重试).
|
||||
* @param timeout_ms 单次 appPing 超时(毫秒)。
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_GD32 超时失败。
|
||||
*/
|
||||
ota_result_t doWaitForAppAlive(uint32_t timeout_ms);
|
||||
|
||||
// ── 传输进度跟踪(状态机内部)──
|
||||
uint32_t m_transferTotalPackets = 0;
|
||||
@@ -343,6 +385,14 @@ private:
|
||||
// ── 加密/工具方法(不变)──
|
||||
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);
|
||||
/**
|
||||
* @brief 探测 GD32 APP 存活并缓存版本(统一 OtaRelay 内部探测逻辑).
|
||||
* 成功打印版本号,失败打印 warning。
|
||||
* @param timeout_ms appPing 超时(毫秒)。
|
||||
* @param out_uptime 可选,输出 GD32 uptime(秒)。
|
||||
* @return true 探测成功,false 失败。
|
||||
*/
|
||||
static bool probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime = nullptr);
|
||||
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,
|
||||
|
||||
+404
-217
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* ble_ota.cpp - BLE OTA GATT 服务器实现 (ESP-IDF 6.0 NimBLE)
|
||||
* 所有功能封装为 BleOta 类方法。
|
||||
* T5.1 - T5.5
|
||||
@@ -54,6 +54,7 @@
|
||||
#define BLE_OTA_FINISH_REBOOT_NOW 0x07
|
||||
#define BLE_OTA_FINISH_GD32_RELAY_START 0x10
|
||||
#define BLE_OTA_FINISH_GD32_RELAY_DONE 0x11
|
||||
#define BLE_OTA_FINISH_ABORTED 0x20 // 用户主动中止(BLE 不断开)
|
||||
#define BLE_OTA_FINISH_ERR_FINALIZE 0x81
|
||||
#define BLE_OTA_ACK_WINDOW 32
|
||||
#define BLE_ATT_ERR_OTA_BUSY 0x80 // ESP32 OTA 被其他通道占用
|
||||
@@ -81,6 +82,17 @@ struct BleEsp32FinishCtx {
|
||||
uint16_t session_id;
|
||||
};
|
||||
|
||||
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免历史多次漏 free。
|
||||
// 注意:若调用 restartEsp32()(内部 esp_restart 不返回),析构不会执行,
|
||||
// 但这是设备重启路径,资源由系统回收。
|
||||
struct BleFinishCtxDeleter {
|
||||
BleEsp32FinishCtx *p;
|
||||
explicit BleFinishCtxDeleter(BleEsp32FinishCtx *ctx) : p(ctx) {}
|
||||
~BleFinishCtxDeleter() { if (p) { free(p); p = nullptr; } }
|
||||
BleFinishCtxDeleter(const BleFinishCtxDeleter&) = delete;
|
||||
BleFinishCtxDeleter& operator=(const BleFinishCtxDeleter&) = delete;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 前向声明
|
||||
// ============================================================
|
||||
@@ -136,6 +148,8 @@ static void ble_esp32_restart_task(void *pvParameters)
|
||||
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
{
|
||||
BleEsp32FinishCtx *ctx = (BleEsp32FinishCtx *)pvParameters;
|
||||
// RAII:保证所有 return 路径自动释放 ctx(restartEsp32 不返回路径除外)
|
||||
BleFinishCtxDeleter ctx_guard(ctx);
|
||||
char *frontend_sha_hex = ctx ? ctx->sha_hex : nullptr;
|
||||
uint16_t session_id = ctx ? ctx->session_id : 0;
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动");
|
||||
@@ -155,7 +169,6 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 1));
|
||||
if (ctx) free(ctx);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
esp32_reorder_free();
|
||||
vTaskDelete(NULL);
|
||||
@@ -187,8 +200,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
|
||||
// 4) 通知全部发出后由协调器统一重启(notify + delay + esp_restart)
|
||||
DEBUG_INFO(TAG, "[FINISH] 通知已发送,重启...");
|
||||
if (ctx) free(ctx);
|
||||
OtaCoordinator::instance().restartEsp32(); // 不返回
|
||||
OtaCoordinator::instance().restartEsp32(); // 不返回,ctx 由系统回收
|
||||
} else if (fr.err == ESP_ERR_INVALID_VERSION) {
|
||||
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, packBleFinishValue(session_id, 0));
|
||||
@@ -207,7 +219,6 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
esp32_reorder_free();
|
||||
}
|
||||
|
||||
if (ctx) free(ctx);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
@@ -234,7 +245,7 @@ static void ble_ota_boost_wifi_ps()
|
||||
s_wifi_ps_boosted = true;
|
||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
||||
OtaCoordinator::instance().notifyBleCoexStart();
|
||||
DEBUG_INFO(TAG, "BLE OTA 进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
|
||||
DEBUG_INFO(TAG, "BLE OTA 数据传输进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +257,7 @@ static void ble_ota_restore_wifi_ps()
|
||||
s_wifi_ps_boosted = false;
|
||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||
DEBUG_INFO(TAG, "BLE OTA 结束:恢复 WiFi RSSI 轮询");
|
||||
DEBUG_INFO(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -312,6 +323,7 @@ struct Esp32OtaReorderBuf {
|
||||
|
||||
static Esp32OtaReorderBuf s_reorder;
|
||||
static uint32_t s_fallback_ooo_count = 0; // 回退模式下自上次 ACK 以来的乱序包计数
|
||||
static uint32_t s_gd32_ooo_count = 0; // GD32 路径自上次 ACK 以来的乱序/重复包计数
|
||||
|
||||
// 前向声明(alloc/free 互相引用)
|
||||
static void esp32_reorder_free();
|
||||
@@ -512,9 +524,14 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
// 后续包处理会覆盖它,必须拷贝一份
|
||||
uint8_t *copy = (uint8_t *)malloc(data_len);
|
||||
if (!copy) {
|
||||
DEBUG_WARN(TAG, "Reorder slot malloc 失败: seq=%u,丢弃该包(靠客户端重传)",
|
||||
seq);
|
||||
return ESP_OK; // 不算失败,靠客户端重传补
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_ERR(TAG, "Reorder slot malloc 失败: seq=%u expected=%u bytes=%u,"
|
||||
"ACK 报告缺口并 abort",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)curReceived);
|
||||
sendWindowAck(cmd,
|
||||
s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
curReceived);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
memcpy(copy, data, data_len);
|
||||
s_reorder.slots[idx] = copy;
|
||||
@@ -548,7 +565,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
{
|
||||
// ★ 通过协调器发起(内部已含 forceCleanup 重试)
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_BLE);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
@@ -564,21 +581,53 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 中止 ESP32 OTA 会话并清理(薄包装)。
|
||||
* @brief BLE OTA 会话状态统一复位(共用清理内核)。
|
||||
*
|
||||
* 集中处理三个清理动作:
|
||||
* 1. abortCurrent():通知协调器按当前状态分发清理
|
||||
* (GD32_STORING→metaClear, ESP32_RECEIVING→core.abort, IDLE→空操作),
|
||||
* 确保下次 begin() 不会被残留状态假拒绝。
|
||||
* 2. core.abort() 兜底:若 Esp32OtaCore 仍 isActive(异常路径),单独再 abort。
|
||||
* 3. BLE OTA 会话状态字段复位:m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending
|
||||
* + esp32_reorder_free() 释放 reorder buffer + s_gd32_ooo_count 归零。
|
||||
*
|
||||
* 不做:OTA 数据队列清理(调用方按需做)、WiFi PS 恢复(调用方按需做)。
|
||||
* 这两个动作在不同调用路径下时序/前置条件不同,交给调用方更安全。
|
||||
*
|
||||
* 调用方:
|
||||
* - ble_esp32_ota_abort(): ESP32 OTA 异常/中止路径
|
||||
* - 0xFB handler: 用户主动中止
|
||||
* - BLE_GAP_EVENT_DISCONNECT handler: BLE 断开(含 GD32 存储断开)
|
||||
*/
|
||||
static void ble_ota_session_reset()
|
||||
{
|
||||
// ★ 无条件通知协调器释放通道占用(m_sysState → IDLE)。
|
||||
// abortCurrent() 对 IDLE 状态是空操作,对其他状态均复位到 IDLE,
|
||||
// 确保后续 begin() 不会被残留的 ESP32_RECEIVING / ERROR 等状态假拒绝。
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
// 若 ESP32 OTA core 仍活跃(兜底),单独再 abort 一次
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
}
|
||||
// 复位 BLE OTA 会话状态,避免下次会话读到旧值
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
BleOta::instance().m_esp32LastCmd = 0;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// 释放 ESP32 reorder buffer(无 buffer 时为空操作)
|
||||
esp32_reorder_free();
|
||||
// 复位 GD32 路径乱序包计数器(节流 ACK 计数),保证下次 GD32 上传从干净状态开始
|
||||
s_gd32_ooo_count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ESP32 OTA 异常/中止时调用:会话复位 + WiFi PS 恢复。
|
||||
*
|
||||
* 关键:必须先通知协调器释放 m_sysState(避免下次 begin 被 ESP32_RECEIVING
|
||||
* 状态假拒绝),再调 core.abort() 释放 mutex。两者重复 abort 幂等安全。
|
||||
*/
|
||||
static void ble_esp32_ota_abort()
|
||||
{
|
||||
// ★ 无条件通知协调器释放通道占用(m_sysState → IDLE)。
|
||||
// abortCurrent() 对 IDLE 状态是空操作,对其他状态均复位到 IDLE,
|
||||
// 确保后续 begin() 不会被残留的 ESP32_RECEIVING / ERROR 等状态假拒绝。
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
Esp32OtaCore::instance().abort();
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// ★ 释放 reorder buffer
|
||||
esp32_reorder_free();
|
||||
ble_ota_session_reset();
|
||||
// ★ BLE OTA 中止:恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
@@ -685,7 +734,7 @@ static void ble_gd32_relay_wait_task(void *pvParameters)
|
||||
|
||||
ota_result_t result = (ota_result_t)((int32_t)result_msg);
|
||||
if (result == OTA_RESULT_OK) {
|
||||
DEBUG_INFO(TAG, "GD32 BLE OTA 中继完成,通知前端");
|
||||
DEBUG_INFO(TAG, "GD32 BLE OTA 中继完成,通知前端 (stage=0x11, session=%u)", session_id);
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_DONE,
|
||||
packBleFinishValue(session_id, 0));
|
||||
} else {
|
||||
@@ -798,6 +847,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_START,
|
||||
packBleFinishValue(session_id, 0));
|
||||
DEBUG_INFO(TAG, "已通知前端: GD32 中继已开始 (stage=0x10, session=%u)", session_id);
|
||||
|
||||
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)malloc(sizeof(BleGd32RelayWaitCtx));
|
||||
if (!ctx) {
|
||||
@@ -876,6 +926,8 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
|
||||
free(finish_ctx);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, (uint16_t)ESP_ERR_NO_MEM));
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
|
||||
}
|
||||
@@ -896,12 +948,64 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 0xFA 重启任务失败");
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, 0);
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_ABORT_OTA) {
|
||||
// 0xFB: 中止当前 OTA(保持 BLE 连接)
|
||||
// - 数据接收阶段(GD32_STORING / ESP32_RECEIVING):立即中止 + 清队列 + 回复 ABORTED
|
||||
// - 中继/收尾阶段(GD32_RELAYING / ESP32_FINALIZING):拒绝中止(SPI 转发不可中断)
|
||||
// - IDLE/已完成状态:幂等回复 ABORTED
|
||||
uint16_t session_id = 0;
|
||||
if (len >= 3) {
|
||||
session_id = (uint16_t)(buf[1] | (buf[2] << 8));
|
||||
}
|
||||
|
||||
ota_system_state_t cur_state = OtaCoordinator::instance().getSystemState();
|
||||
DEBUG_WARN(TAG, "收到 0xFB 中止请求: state=%s session=%u",
|
||||
OtaCoordinator::stateToString(cur_state), session_id);
|
||||
|
||||
if (cur_state == OTA_SYS_GD32_RELAYING) {
|
||||
DEBUG_WARN(TAG, "GD32 SPI 中继进行中,无法中止(请等待完成或断开 BLE)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
if (cur_state == OTA_SYS_ESP32_FINALIZING) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 收尾进行中,无法中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── 可中止状态:清队列 + abort core + 复位协调器 ──
|
||||
if (s_otaDataQueue) {
|
||||
BleOtaDataBatch stale;
|
||||
size_t dropped = 0;
|
||||
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
|
||||
free(stale.data);
|
||||
dropped++;
|
||||
}
|
||||
if (dropped > 0) {
|
||||
DEBUG_INFO(TAG, "中止时清理 OTA 数据队列: %zu 项", dropped);
|
||||
}
|
||||
}
|
||||
|
||||
// abortCurrent 内部按状态分发:GD32_STORING→metaClear, ESP32_RECEIVING→core.abort, IDLE→空操作
|
||||
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 + 会话状态复位
|
||||
// (m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/reorder/s_gd32_ooo_count)
|
||||
ble_ota_session_reset();
|
||||
|
||||
ble_ota_restore_wifi_ps();
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 已中止(BLE 保持连接),通知前端 (stage=0x20)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ABORTED,
|
||||
packBleFinishValue(session_id, 0));
|
||||
}
|
||||
// 0xF7 = 触发 GD32 中继
|
||||
// 0xF8 = 触发 ESP32 本机 OTA 收尾:设备端读 OTA 分区算 SHA 并与前端 32B SHA 比对,
|
||||
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH(不重启)
|
||||
// 0xFA = 重启 ESP32(配套前端 expectedReboot 自动重连)
|
||||
// 0xFB = 中止当前 OTA(不断开 BLE):仅数据接收阶段有效,中继/收尾阶段拒绝
|
||||
|
||||
return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0
|
||||
}
|
||||
@@ -966,6 +1070,248 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理单个 ESP32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
|
||||
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
|
||||
*/
|
||||
static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
const uint8_t *buf, size_t data_offset, size_t data_len,
|
||||
uint32_t expected_payload)
|
||||
{
|
||||
if (seq == 0) {
|
||||
if (total_size == 0) {
|
||||
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
|
||||
return true;
|
||||
}
|
||||
// ★ 会话判定:若已有活跃 ESP32 OTA 会话,根据已接收字节数区分两种情况:
|
||||
// ① m_received == 0:seq=0 尚未写入 flash,说明是客户端 ACK 超时后
|
||||
// 重传的 seq=0(同一会话续传),跳过 begin。
|
||||
// ② m_received > 0:seq=0 已写入且有后续数据,说明这是新的上传会话
|
||||
// (前端 abort 后 BLE 断开未完成就发起了新传输),需 abort 旧会话
|
||||
// 再 begin,否则 reorder buffer 以旧 expected 去重导致全部丢弃。
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
if (Esp32OtaCore::instance().getReceived() > 0) {
|
||||
// ★ 跨通道保护:当前活跃会话可能是 WiFi 通道发起的,
|
||||
// BLE 不应无条件 abort 别人的会话。只有当活跃会话也是 BLE
|
||||
// 通道时才允许 abort + rebegin(同会话重传/新 BLE 会话场景);
|
||||
// 否则直接互斥拒绝,让前端看到"被其他通道占用"。
|
||||
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
|
||||
if (curCh != OTA_CHANNEL_BLE) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被其他通道占用(ch=%d),BLE 拒绝抢占",
|
||||
(int)curCh);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
}
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 新会话检测: received=%u > 0, "
|
||||
"中止旧会话并重新开始",
|
||||
(unsigned)Esp32OtaCore::instance().getReceived());
|
||||
ble_esp32_ota_abort();
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin");
|
||||
}
|
||||
} else {
|
||||
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 会话有效性检查:如果 begin() 失败(如互斥拒绝),后续包也应丢弃
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// ★ 防重复写入保护:如果已接收字节 >= 声明大小,跳过(重试引起的重复包)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
if (total_size > 0 && curReceived >= total_size) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 已收满 (%u >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
(unsigned)curReceived, total_size, seq);
|
||||
sendWindowAck(cmd, seq, curReceived);
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// ★ 使用带序号顺序保护的写入:拒绝乱序包并立即 ACK 通知发送端
|
||||
esp_err_t err = esp32_write_ordered(seq, cmd, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
|
||||
seq, esp_err_to_name(err));
|
||||
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
|
||||
return true;
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
|
||||
seq, (unsigned int)Esp32OtaCore::instance().getReceived());
|
||||
|
||||
// 完成判定:以实际按序写入字节数为准。
|
||||
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
bool transferComplete = (total_size > 0 &&
|
||||
Esp32OtaCore::instance().getReceived() >= total_size);
|
||||
|
||||
// ACK 触发条件
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
transferComplete ||
|
||||
isLastPacket) {
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
seq, data_len, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size);
|
||||
}
|
||||
sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
|
||||
}
|
||||
|
||||
if (transferComplete) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
|
||||
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
BleOta::instance().m_esp32LastSeq = seq;
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理单个 GD32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
|
||||
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
|
||||
*/
|
||||
static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
const uint8_t *buf, size_t data_offset, size_t data_len,
|
||||
uint32_t expected_payload)
|
||||
{
|
||||
if (seq == 0 && total_size > 0) {
|
||||
// ★ 统一调用 prepareGd32Storage 完成:validate + begin + erase + clearMeta + saveMeta
|
||||
// 原 BLE 路径未擦除分区,现通过 prepareGd32Storage 与 HTTP 路径对齐。
|
||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(total_size, OTA_CHANNEL_BLE);
|
||||
if (prep_err == ESP_ERR_INVALID_SIZE) {
|
||||
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " max=%zu",
|
||||
total_size, OTA_MAX_FIRMWARE_SIZE);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
|
||||
return true;
|
||||
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||
DEBUG_ERR(TAG, "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
} else if (prep_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
|
||||
total_size, data_len);
|
||||
ble_ota_boost_wifi_ps();
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 顺序检查:GD32 Flash 必须按序写入,无 reorder buffer ──
|
||||
// 改进:乱序/重复包不再中止整个 batch,而是丢弃当前包 + 节流 ACK 报告缺口,
|
||||
// 让前端尽快知道设备端实际进度(last_seq)并重传缺口。
|
||||
// 策略与 ESP32 fallback 路径一致(参考 esp32_write_ordered 行 459-477)。
|
||||
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
|
||||
// 重复包:seq <= last_seq(已写入过)
|
||||
if (seq <= meta.last_seq) {
|
||||
DEBUG_DBG(TAG, "GD32 重复包: seq=%u <= last_seq=%" PRIu32 ",跳过",
|
||||
seq, meta.last_seq);
|
||||
// 重复包不触发 ACK 风暴,只在窗口边界发一次
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) {
|
||||
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
|
||||
}
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// 乱序包:seq > last_seq + 1(缺口)
|
||||
// 节流策略:每 BLE_OTA_ACK_WINDOW 个乱序包才发一次 ACK,或在窗口边界处发 ACK。
|
||||
// 避免大量乱序包涌入时触发 ACK 风暴,耗尽 NimBLE mbuf 池导致 ESP_ERR_NO_MEM。
|
||||
s_gd32_ooo_count++;
|
||||
bool shouldAck =
|
||||
(s_gd32_ooo_count >= BLE_OTA_ACK_WINDOW) ||
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
|
||||
if (shouldAck) {
|
||||
DEBUG_WARN(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),节流 ACK bytes=%" PRIu32 " (ooo=%u)",
|
||||
seq, meta.last_seq, meta.stored_size, (unsigned)s_gd32_ooo_count);
|
||||
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
|
||||
s_gd32_ooo_count = 0;
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),抑制 ACK (ooo=%u)",
|
||||
seq, meta.last_seq, (unsigned)s_gd32_ooo_count);
|
||||
}
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// 顺序包到达,复位乱序计数器
|
||||
if (s_gd32_ooo_count != 0) {
|
||||
s_gd32_ooo_count = 0;
|
||||
}
|
||||
|
||||
// --- GD32 写入 Flash ---
|
||||
uint32_t flash_offset = meta.stored_size;
|
||||
if (flash_offset >= meta.total_size) {
|
||||
DEBUG_WARN(TAG, "GD32 OTA 已收满 (%" PRIu32 " >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
flash_offset, meta.total_size, seq);
|
||||
sendWindowAck(cmd, seq, flash_offset);
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
if (flash_offset + (uint32_t)data_len > meta.total_size) {
|
||||
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
|
||||
flash_offset, data_len, meta.total_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(flash_offset, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
|
||||
seq, flash_offset, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t stored_size = flash_offset + (uint32_t)data_len;
|
||||
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
|
||||
}
|
||||
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(meta.total_size > 0 && stored_size >= meta.total_size) ||
|
||||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD)) {
|
||||
sendWindowAck(cmd, seq, stored_size);
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
|
||||
seq, flash_offset, stored_size);
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
ble_ota_restore_wifi_ps();
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理一批 OTA 数据包(在工作线程上下文中调用)。
|
||||
* 包含 ESP32 和 GD32 的数据接收逻辑。
|
||||
@@ -1012,203 +1358,22 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu offset=%zu",
|
||||
cmd, seq, total_size, data_len, offset);
|
||||
|
||||
// --- ESP32 自身 OTA:直接写 OTA 分区 ---
|
||||
// 分发到对应通道的包处理器
|
||||
bool abort_batch = false;
|
||||
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
|
||||
if (seq == 0) {
|
||||
if (total_size == 0) {
|
||||
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
|
||||
return;
|
||||
}
|
||||
// ★ 会话判定:若已有活跃 ESP32 OTA 会话,根据已接收字节数区分两种情况:
|
||||
// ① m_received == 0:seq=0 尚未写入 flash,说明是客户端 ACK 超时后
|
||||
// 重传的 seq=0(同一会话续传),跳过 begin。
|
||||
// ② m_received > 0:seq=0 已写入且有后续数据,说明这是新的上传会话
|
||||
// (前端 abort 后 BLE 断开未完成就发起了新传输),需 abort 旧会话
|
||||
// 再 begin,否则 reorder buffer 以旧 expected 去重导致全部丢弃。
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
if (Esp32OtaCore::instance().getReceived() > 0) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 新会话检测: received=%u > 0, "
|
||||
"中止旧会话并重新开始",
|
||||
(unsigned)Esp32OtaCore::instance().getReceived());
|
||||
ble_esp32_ota_abort();
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin");
|
||||
}
|
||||
} else {
|
||||
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 会话有效性检查:如果 begin() 失败(如互斥拒绝),后续包也应丢弃
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > recv_len) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 防重复写入保护:如果已接收字节 >= 声明大小,跳过(重试引起的重复包)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
if (total_size > 0 && curReceived >= total_size) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 已收满 (%u >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
(unsigned)curReceived, total_size, seq);
|
||||
sendWindowAck(cmd, seq, curReceived);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > recv_len) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 使用带序号顺序保护的写入:拒绝乱序包并立即 ACK 通知发送端
|
||||
esp_err_t err = esp32_write_ordered(seq, cmd, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
|
||||
seq, esp_err_to_name(err));
|
||||
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
|
||||
seq, (unsigned int)Esp32OtaCore::instance().getReceived());
|
||||
|
||||
// 完成判定:以实际按序写入字节数为准。
|
||||
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
bool transferComplete = (total_size > 0 &&
|
||||
Esp32OtaCore::instance().getReceived() >= total_size);
|
||||
|
||||
// ACK 触发条件
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
transferComplete ||
|
||||
isLastPacket) {
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
seq, data_len, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size);
|
||||
}
|
||||
sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
|
||||
}
|
||||
|
||||
if (transferComplete) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
|
||||
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
BleOta::instance().m_esp32LastSeq = seq;
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
}
|
||||
// --- GD32 OTA:首个分块时创建存储元数据 ---
|
||||
else if (cmd == BLE_OTA_CMD_GD32_DATA) {
|
||||
if (seq == 0 && total_size > 0) {
|
||||
uint32_t rounded_size = (uint32_t)(((total_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||
if (total_size == 0U || total_size > OTA_MAX_FIRMWARE_SIZE || rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " rounded48=%" PRIu32 " max=%zu",
|
||||
total_size, rounded_size, OTA_MAX_FIRMWARE_SIZE);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
|
||||
return;
|
||||
}
|
||||
|
||||
if (OtaCoordinator::instance().beginGd32Storage(total_size) != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
return;
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
meta.total_size = total_size;
|
||||
meta.stored_size = 0;
|
||||
meta.last_seq = 0;
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().saveGd32Meta(&meta);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "元数据保存失败");
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
|
||||
meta.total_size, data_len);
|
||||
ble_ota_boost_wifi_ps();
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
|
||||
DEBUG_ERR(TAG, "GD32 分包序号不连续: seq=%u last_seq=%" PRIu32,
|
||||
seq, meta.last_seq);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- GD32 写入 Flash ---
|
||||
uint32_t flash_offset = meta.stored_size;
|
||||
if (flash_offset >= meta.total_size) {
|
||||
DEBUG_WARN(TAG, "GD32 OTA 已收满 (%" PRIu32 " >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
flash_offset, meta.total_size, seq);
|
||||
sendWindowAck(cmd, seq, flash_offset);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > recv_len) break;
|
||||
continue;
|
||||
}
|
||||
if (flash_offset + (uint32_t)data_len > meta.total_size) {
|
||||
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
|
||||
flash_offset, data_len, meta.total_size);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(flash_offset, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
|
||||
seq, flash_offset, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t stored_size = flash_offset + (uint32_t)data_len;
|
||||
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
|
||||
}
|
||||
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(meta.total_size > 0 && stored_size >= meta.total_size) ||
|
||||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD)) {
|
||||
sendWindowAck(cmd, seq, stored_size);
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
|
||||
seq, flash_offset, stored_size);
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
ble_ota_restore_wifi_ps();
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
abort_batch = process_esp32_packet(cmd, seq, total_size,
|
||||
buf, data_offset, data_len, expected_payload);
|
||||
} else if (cmd == BLE_OTA_CMD_GD32_DATA) {
|
||||
abort_batch = process_gd32_packet(cmd, seq, total_size,
|
||||
buf, data_offset, data_len, expected_payload);
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "DATA_IN 未知 cmd: 0x%02X offset=%zu", cmd, offset);
|
||||
}
|
||||
|
||||
if (abort_batch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 推进到下一个包
|
||||
offset += packet_len;
|
||||
|
||||
@@ -1425,11 +1590,33 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
// ★ BLE 断开时恢复 WiFi 射频,并中止未完成的 ESP32 OTA
|
||||
// ★ BLE 断开时恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
|
||||
ble_esp32_ota_abort();
|
||||
|
||||
// ★ 断开时清理"BLE 数据接收阶段"残留的 OTA 会话状态,避免下次刷写被
|
||||
// 系统 busy 假拒绝。注意区分可中止 / 不可中止状态:
|
||||
// - GD32_STORING / ESP32_RECEIVING / ESP32_FINALIZING:BLE 是唯一数据源,
|
||||
// 断开即等价于中止,必须 abortCurrent() + 清理底层 core。
|
||||
// - GD32_RELAYING / ESP32_REBOOTING:SPI 中继/重启不依赖 BLE,不能打断。
|
||||
// 这些状态保留,等中继/重启自然结束。
|
||||
// - IDLE / STORED / COMPLETE / ERROR:abortCurrent 幂等安全,直接调即可。
|
||||
{
|
||||
ota_system_state_t cur = OtaCoordinator::instance().getSystemState();
|
||||
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
|
||||
OtaCoordinator::stateToString(cur));
|
||||
} else {
|
||||
if (cur == OTA_SYS_GD32_STORING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 GD32 固件存储");
|
||||
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
|
||||
cur == OTA_SYS_ESP32_FINALIZING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
|
||||
}
|
||||
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 +
|
||||
// 会话状态复位(m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/
|
||||
// reorder/s_gd32_ooo_count)
|
||||
ble_ota_session_reset();
|
||||
}
|
||||
}
|
||||
// 恢复广播以接受新连接
|
||||
ble_ota_start_advertising();
|
||||
|
||||
+253
-56
@@ -30,15 +30,14 @@
|
||||
#include "canfd_bridge.h"
|
||||
#include "ble_ota.h"
|
||||
|
||||
static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U;
|
||||
|
||||
// OTA 接收共享缓冲区
|
||||
static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];
|
||||
|
||||
static uint32_t align_up_u32(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
return ((value + alignment - 1U) / alignment) * alignment;
|
||||
}
|
||||
// OTA 准备状态:/ota/prepare 完成后设置,/ota/upload 消费后清除
|
||||
// ★ 同时记录 prepare 时的 epoch,clear 时仅在同一会话才中止,避免误清新会话
|
||||
static bool s_esp32_prepared = false;
|
||||
static uint32_t s_esp32_prepare_size = 0;
|
||||
static uint32_t s_esp32_prepare_epoch = 0;
|
||||
static bool s_gd32_prepared = false;
|
||||
static uint32_t s_gd32_prepare_size = 0;
|
||||
static uint32_t s_gd32_prepare_epoch = 0;
|
||||
|
||||
void http_set_cors_headers(httpd_req_t *req)
|
||||
{
|
||||
@@ -48,6 +47,27 @@ void http_set_cors_headers(httpd_req_t *req)
|
||||
httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
|
||||
}
|
||||
|
||||
static void clear_esp32_prepare()
|
||||
{
|
||||
if (s_esp32_prepared) {
|
||||
// ★ 仅在 prepare 仍属当前会话时中止;同类型重启已发生时跳过,避免误清新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(s_esp32_prepare_epoch);
|
||||
s_esp32_prepared = false;
|
||||
s_esp32_prepare_size = 0;
|
||||
s_esp32_prepare_epoch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_gd32_prepare()
|
||||
{
|
||||
if (s_gd32_prepared) {
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(s_gd32_prepare_epoch);
|
||||
s_gd32_prepared = false;
|
||||
s_gd32_prepare_size = 0;
|
||||
s_gd32_prepare_epoch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t http_options_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
@@ -126,7 +146,8 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req)
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
DEBUG_INFO("OTA", "响应已发送,重启...");
|
||||
DEBUG_INFO("OTA", "响应已发送,等待 TCP 刷新...");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
OtaCoordinator::instance().restartEsp32();
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -177,9 +198,122 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
||||
return total;
|
||||
}
|
||||
|
||||
esp_err_t ota_prepare_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/prepare ===");
|
||||
|
||||
char buf[256] = {0};
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "recv failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON parse failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *type_item = cJSON_GetObjectItem(root, "type");
|
||||
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
|
||||
if (!type_item || !cJSON_IsString(type_item) ||
|
||||
!size_item || !cJSON_IsNumber(size_item)) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing type or total_size");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
const char *type = cJSON_GetStringValue(type_item);
|
||||
uint32_t total_size = (uint32_t)cJSON_GetNumberValue(size_item);
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (type && strcmp(type, "esp32") == 0) {
|
||||
clear_esp32_prepare();
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
|
||||
"ESP32 OTA occupied by another channel");
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
s_esp32_prepared = true;
|
||||
s_esp32_prepare_size = total_size;
|
||||
s_esp32_prepare_epoch = OtaCoordinator::instance().getOtaEpoch();
|
||||
const char resp[] = "{\"status\": \"ok\", \"target\": \"esp32\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 准备完成: %u 字节", total_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (type && strcmp(type, "gd32") == 0) {
|
||||
clear_gd32_prepare();
|
||||
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 prepare 前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
// ★ 统一调用 prepareGd32Storage 完成:validate + begin + erase + clearMeta + saveMeta
|
||||
// 与 BLE 路径 / ota_upload_handler 共享同一序列,消除手写重复
|
||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(total_size, OTA_CHANNEL_WIFI);
|
||||
if (prep_err == ESP_ERR_INVALID_SIZE) {
|
||||
DEBUG_ERR("WIFI", "prepare: firmware size invalid: %u", total_size);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "firmware size invalid");
|
||||
return ESP_FAIL;
|
||||
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||
DEBUG_ERR("WIFI", "GD32 prepare 协调器拒绝(系统忙)");
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
|
||||
"system busy with another OTA operation");
|
||||
return ESP_FAIL;
|
||||
} else if (prep_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 prepare 失败: %s", esp_err_to_name(prep_err));
|
||||
char err_msg[96];
|
||||
snprintf(err_msg, sizeof(err_msg), "prepare gd32_ota failed: %s",
|
||||
esp_err_to_name(prep_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(false);
|
||||
WifiOta::instance().stateUnlock();
|
||||
|
||||
s_gd32_prepared = true;
|
||||
s_gd32_prepare_size = total_size;
|
||||
s_gd32_prepare_epoch = OtaCoordinator::instance().getOtaEpoch();
|
||||
const char resp[] = "{\"status\": \"ok\", \"target\": \"gd32\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
DEBUG_INFO("WIFI", "GD32 OTA 准备完成: %u 字节", total_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "unknown type");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
// 每请求分配 OTA 接收缓冲区:OTA_BLOCK_SIZE=8KB > 4KB 不适合栈分配,
|
||||
// 改为 per-request malloc 避免 ESP32/GD32 两条路径共享全局 buffer,
|
||||
// max_open_sockets=4 时多连接并发也互不干扰。
|
||||
uint8_t *recv_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
|
||||
if (!recv_buf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d", req->content_len);
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||
|
||||
@@ -191,6 +325,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
int peek_ret = httpd_req_recv(req, &peek, 1);
|
||||
if (peek_ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -231,22 +366,49 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
if (is_esp32_ota) {
|
||||
if (total_size == 0) {
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
|
||||
// ★ 若前端已通过 /ota/prepare 完成擦除,直接复用 prepared 状态,
|
||||
// 避免在大量数据传输前再次长时间阻塞 HTTP worker。
|
||||
bool used_prepare = false;
|
||||
if (s_esp32_prepared) {
|
||||
if (total_size == s_esp32_prepare_size) {
|
||||
used_prepare = true;
|
||||
s_esp32_prepared = false;
|
||||
s_esp32_prepare_size = 0;
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 使用已 prepared 状态,跳过擦除");
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||
DEBUG_WARN("WIFI", "ESP32 prepared size mismatch (prepared=%u, upload=%u),重新初始化",
|
||||
s_esp32_prepare_size, total_size);
|
||||
clear_esp32_prepare();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err;
|
||||
if (!used_prepare) {
|
||||
err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
|
||||
if (err != ESP_OK) {
|
||||
clear_esp32_prepare();
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||
}
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
uint8_t *fw_buf = s_ota_recv_buf;
|
||||
uint8_t *fw_buf = recv_buf;
|
||||
|
||||
// ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler
|
||||
// 若同类型重启已发生(新请求接管),旧 handler 的 abortCurrentIfEpoch 不会中止新会话
|
||||
uint32_t my_epoch = OtaCoordinator::instance().getOtaEpoch();
|
||||
|
||||
size_t total_firmware_written = 0;
|
||||
size_t total_received = json_header_len;
|
||||
@@ -259,16 +421,23 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret);
|
||||
core.abort();
|
||||
// ★ 客户端断开/接收失败:用 abortCurrentIfEpoch 复位协调器状态机
|
||||
// 若 epoch 不匹配(新请求已接管),跳过中止,避免误清新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
err = core.write(fw_buf, (size_t)ret);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "OTA write failed: %s", esp_err_to_name(err));
|
||||
core.abort();
|
||||
// ★ 同上:写入失败也用 abortCurrentIfEpoch,避免误中止新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -289,6 +458,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
free(recv_buf);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -299,53 +469,70 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
} else {
|
||||
gd32_valid_size = gd32_fw_size;
|
||||
}
|
||||
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_valid_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||
if (gd32_valid_size == 0U || gd32_valid_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u rounded48=%u max=%u",
|
||||
(unsigned)gd32_valid_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||
if (OtaCoordinator::instance().validateGd32Size(gd32_valid_size) != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u max=%u",
|
||||
(unsigned)gd32_valid_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||
clear_gd32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
if (OtaCoordinator::instance().beginGd32Storage(gd32_valid_size) != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(false);
|
||||
WifiOta::instance().stateUnlock();
|
||||
|
||||
const uint32_t gd32_erase_size = align_up_u32(gd32_valid_size, GD32_OTA_FLASH_SECTOR_SIZE);
|
||||
DEBUG_INFO("WIFI", "擦除 gd32_ota 分区: %u bytes (fw=%u)",
|
||||
(unsigned)gd32_erase_size, (unsigned)gd32_valid_size);
|
||||
esp_err_t erase_err = OtaCoordinator::instance().eraseGd32Partition(gd32_erase_size);
|
||||
if (erase_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
||||
char err_msg[96];
|
||||
snprintf(err_msg, sizeof(err_msg), "erase gd32_ota failed: %s", esp_err_to_name(erase_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||
return ESP_FAIL;
|
||||
// ★ 若前端已通过 /ota/prepare 完成擦除,直接复用 prepared 状态
|
||||
bool used_gd32_prepare = false;
|
||||
if (s_gd32_prepared) {
|
||||
if (gd32_valid_size == s_gd32_prepare_size) {
|
||||
used_gd32_prepare = true;
|
||||
s_gd32_prepared = false;
|
||||
s_gd32_prepare_size = 0;
|
||||
DEBUG_INFO("WIFI", "GD32 OTA 使用已 prepared 状态,跳过擦除");
|
||||
} else {
|
||||
DEBUG_WARN("WIFI", "GD32 prepared size mismatch (prepared=%u, upload=%u),重新初始化",
|
||||
s_gd32_prepare_size, gd32_valid_size);
|
||||
clear_gd32_prepare();
|
||||
}
|
||||
}
|
||||
OtaCoordinator::instance().clearGd32Meta();
|
||||
|
||||
uint8_t *buf = s_ota_recv_buf;
|
||||
if (!used_gd32_prepare) {
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
// ★ 统一调用 prepareGd32Storage 完成:begin + erase + clearMeta + saveMeta
|
||||
// 与 BLE 路径(process_gd32_packet seq=0)共用同一序列,消除隐式差异。
|
||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(gd32_valid_size, OTA_CHANNEL_WIFI);
|
||||
if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
clear_gd32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
} else if (prep_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
|
||||
clear_gd32_prepare();
|
||||
char err_msg[96];
|
||||
snprintf(err_msg, sizeof(err_msg), "prepare gd32_ota failed: %s", esp_err_to_name(prep_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(false);
|
||||
WifiOta::instance().stateUnlock();
|
||||
}
|
||||
|
||||
// ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler
|
||||
uint32_t my_epoch = OtaCoordinator::instance().getOtaEpoch();
|
||||
|
||||
uint8_t *buf = recv_buf;
|
||||
|
||||
int received = 0;
|
||||
|
||||
// expected_fw_size_raw 仅用于后续日志输出;meta 已由 prepareGd32Storage
|
||||
// (非 prepared 路径)或 /ota/prepare(prepared 路径)写入,此处不再重复保存。
|
||||
uint32_t expected_fw_size_raw = gd32_valid_size;
|
||||
ota_meta_t meta_raw = {
|
||||
.total_size = expected_fw_size_raw,
|
||||
.stored_size = 0,
|
||||
.last_seq = 0,
|
||||
.checksum = {0}
|
||||
};
|
||||
OtaCoordinator::instance().saveGd32Meta(&meta_raw);
|
||||
uint32_t gd32_offset = 0;
|
||||
|
||||
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
||||
@@ -359,7 +546,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
|
||||
first_chunk_len, esp_err_to_name(err));
|
||||
// ★ 首块写入失败:用 abortCurrentIfEpoch 避免误中止新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received = first_chunk_len;
|
||||
@@ -378,7 +568,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "GD32 upload interrupted: received=%d expected=%d",
|
||||
received, req->content_len);
|
||||
// ★ 客户端断开(刷新网页/关浏览器/网络掉):用 abortCurrentIfEpoch 避免误中止新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -386,7 +579,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s",
|
||||
(unsigned)gd32_offset, ret, esp_err_to_name(err));
|
||||
// ★ 写入失败:同上,用 abortCurrentIfEpoch
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -416,6 +612,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
free(recv_buf);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
+29
-135
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* wifi_ota.cpp - WiFi OTA 服务器实现
|
||||
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
|
||||
* T4.1 - T4.5
|
||||
@@ -442,144 +442,38 @@ esp_err_t WifiOta::httpServerStart()
|
||||
}
|
||||
}
|
||||
|
||||
httpd_uri_t upload_uri = {
|
||||
.uri = "/ota/upload",
|
||||
.method = HTTP_POST,
|
||||
.handler = ota_upload_handler,
|
||||
.user_ctx = NULL
|
||||
struct UriEntry {
|
||||
const char *uri;
|
||||
httpd_method_t method;
|
||||
esp_err_t (*handler)(httpd_req_t *);
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &upload_uri);
|
||||
httpd_uri_t upload_options_uri = {
|
||||
.uri = "/ota/upload",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
static const UriEntry kHttpRoutes[] = {
|
||||
{"/ota/upload", HTTP_POST, ota_upload_handler},
|
||||
{"/ota/relay", HTTP_POST, ota_relay_trigger_handler},
|
||||
{"/ota/update", HTTP_POST, esp32_ota_update_handler},
|
||||
{"/restart", HTTP_POST, restart_handler},
|
||||
{"/logs", HTTP_GET, logs_handler},
|
||||
{"/system/status", HTTP_GET, system_status_handler},
|
||||
{"/system/probe", HTTP_GET, system_probe_handler},
|
||||
{"/can/send", HTTP_POST, can_send_handler},
|
||||
{"/spi/send", HTTP_POST, spi_send_handler},
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &upload_options_uri);
|
||||
|
||||
httpd_uri_t relay_uri = {
|
||||
.uri = "/ota/relay",
|
||||
.method = HTTP_POST,
|
||||
.handler = ota_relay_trigger_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &relay_uri);
|
||||
httpd_uri_t relay_options_uri = {
|
||||
.uri = "/ota/relay",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &relay_options_uri);
|
||||
for (const auto &r : kHttpRoutes) {
|
||||
httpd_uri_t main_uri = {};
|
||||
main_uri.uri = r.uri;
|
||||
main_uri.method = r.method;
|
||||
main_uri.handler = r.handler;
|
||||
main_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &main_uri);
|
||||
|
||||
httpd_uri_t esp32_update_uri = {
|
||||
.uri = "/ota/update",
|
||||
.method = HTTP_POST,
|
||||
.handler = esp32_ota_update_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_update_uri);
|
||||
httpd_uri_t esp32_update_options_uri = {
|
||||
.uri = "/ota/update",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_update_options_uri);
|
||||
|
||||
httpd_uri_t restart_uri = {
|
||||
.uri = "/restart",
|
||||
.method = HTTP_POST,
|
||||
.handler = restart_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &restart_uri);
|
||||
httpd_uri_t restart_options_uri = {
|
||||
.uri = "/restart",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &restart_options_uri);
|
||||
|
||||
httpd_uri_t logs_uri = {
|
||||
.uri = "/logs",
|
||||
.method = HTTP_GET,
|
||||
.handler = logs_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_uri);
|
||||
httpd_uri_t logs_options_uri = {
|
||||
.uri = "/logs",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_options_uri);
|
||||
|
||||
// 系统状态查询端点
|
||||
httpd_uri_t status_uri = {
|
||||
.uri = "/system/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = system_status_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_uri);
|
||||
httpd_uri_t status_options_uri = {
|
||||
.uri = "/system/status",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_options_uri);
|
||||
|
||||
// PR2: GD32 APP 探测端点
|
||||
httpd_uri_t system_probe_uri = {
|
||||
.uri = "/system/probe",
|
||||
.method = HTTP_GET,
|
||||
.handler = system_probe_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_probe_uri);
|
||||
httpd_uri_t system_probe_options_uri = {
|
||||
.uri = "/system/probe",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_probe_options_uri);
|
||||
|
||||
// CAN 帧转发
|
||||
httpd_uri_t can_send_uri = {
|
||||
.uri = "/can/send",
|
||||
.method = HTTP_POST,
|
||||
.handler = can_send_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_send_uri);
|
||||
httpd_uri_t can_send_options_uri = {
|
||||
.uri = "/can/send",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_send_options_uri);
|
||||
|
||||
// SPI 直接发送
|
||||
httpd_uri_t spi_send_uri = {
|
||||
.uri = "/spi/send",
|
||||
.method = HTTP_POST,
|
||||
.handler = spi_send_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_uri);
|
||||
httpd_uri_t spi_send_options_uri = {
|
||||
.uri = "/spi/send",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri);
|
||||
httpd_uri_t opts_uri = {};
|
||||
opts_uri.uri = r.uri;
|
||||
opts_uri.method = HTTP_OPTIONS;
|
||||
opts_uri.handler = http_options_handler;
|
||||
opts_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /spi/send");
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
#include "gd32_flash_store.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
// ============================================================
|
||||
// 单例
|
||||
@@ -24,6 +26,34 @@ Gd32FlashStore& Gd32FlashStore::instance()
|
||||
return inst;
|
||||
}
|
||||
|
||||
// 构造时创建 NVS 互斥锁。
|
||||
// 单例静态局部变量在首次 instance() 调用时构造,此时 FreeRTOS 已就绪。
|
||||
Gd32FlashStore::Gd32FlashStore()
|
||||
{
|
||||
m_nvsMutex = xSemaphoreCreateMutex();
|
||||
if (!m_nvsMutex) {
|
||||
DEBUG_ERR("OTA", "Gd32FlashStore NVS mutex 创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NVS 互斥锁辅助
|
||||
// ============================================================
|
||||
bool Gd32FlashStore::lockNvs(uint32_t timeout_ms)
|
||||
{
|
||||
// 参考 OtaCoordinator::lock:返回 take 结果,由调用方传回 unlockNvs 决定是否 give。
|
||||
if (!m_nvsMutex) return false;
|
||||
return xSemaphoreTake(m_nvsMutex, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
|
||||
}
|
||||
|
||||
void Gd32FlashStore::unlockNvs(bool handle)
|
||||
{
|
||||
// 仅在对应 lockNvs 成功 take 时才 give,避免误释放。
|
||||
if (handle && m_nvsMutex) {
|
||||
xSemaphoreGive(m_nvsMutex);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分区懒初始化
|
||||
// ============================================================
|
||||
@@ -132,8 +162,8 @@ esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
// NVS 元数据持久化
|
||||
// ============================================================
|
||||
|
||||
/** @brief 从 NVS 加载 OTA 元数据,命名空间不存在时返回全零结构。 */
|
||||
esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
/** @brief 已持锁版本:从 NVS 加载 OTA 元数据,命名空间不存在时返回全零结构。 */
|
||||
esp_err_t Gd32FlashStore::metaLoadLocked(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
@@ -141,7 +171,15 @@ esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return ESP_OK;
|
||||
// 区分 nvs_open 失败原因,不再一律当 OK。
|
||||
// ESP_ERR_NVS_NOT_FOUND = 命名空间不存在(首次使用/被擦除),视为空元数据,保持兼容。
|
||||
// 其他错误(NVS 未初始化/分区丢失/损坏等)返回真实错误码,
|
||||
// 避免掩盖底层故障导致调用方误判为"空元数据"继续执行。
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
return ESP_OK;
|
||||
}
|
||||
DEBUG_WARN("OTA", "metaLoad nvs_open 失败(非 NOT_FOUND): %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
uint32_t val = 0;
|
||||
@@ -159,8 +197,24 @@ esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 将 OTA 元数据写入 NVS。 */
|
||||
esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
/** @brief 从 NVS 加载 OTA 元数据(加锁包装)。 */
|
||||
esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
// 加锁失败不能跳过锁直接访问 NVS,清零输出并返回超时
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
DEBUG_WARN("OTA", "metaLoad 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t err = metaLoadLocked(meta);
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 已持锁版本:将 OTA 元数据写入 NVS。 */
|
||||
esp_err_t Gd32FlashStore::metaSaveLocked(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
@@ -184,25 +238,57 @@ esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 传输过程中更新元数据的 stored_size 和 last_seq 字段。 */
|
||||
/** @brief 将 OTA 元数据写入 NVS(加锁包装)。 */
|
||||
esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaSave 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t err = metaSaveLocked(meta);
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 传输过程中更新元数据的 stored_size 和 last_seq 字段。
|
||||
* 整个读-改-写过程持锁,避免并发 metaUpdate 丢失更新。
|
||||
* 内部调用已持锁的 metaLoadLocked/metaSaveLocked,不再经公开方法重复加锁。
|
||||
*/
|
||||
esp_err_t Gd32FlashStore::metaUpdate(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaUpdate 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
|
||||
return metaSave(&meta);
|
||||
esp_err_t err = metaLoadLocked(&meta);
|
||||
if (err == ESP_OK) {
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
err = metaSaveLocked(&meta);
|
||||
}
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief OTA 完成或中止时清除元数据(全零写入)。 */
|
||||
esp_err_t Gd32FlashStore::metaClear()
|
||||
{
|
||||
DEBUG_INFO("OTA", "清除 OTA 元数据");
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaClear 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
ota_meta_t meta = {};
|
||||
return metaSave(&meta);
|
||||
esp_err_t err = metaSaveLocked(&meta);
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -296,13 +382,12 @@ esp_err_t Gd32FlashStore::metaVerifyChecksum()
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// 检查 checksum 是否全零(未计算过)
|
||||
bool all_zero = true;
|
||||
for (size_t i = 0; i < sizeof(meta.checksum); ++i) {
|
||||
if (meta.checksum[i] != 0x00) { all_zero = false; break; }
|
||||
}
|
||||
if (all_zero) {
|
||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,跳过校验(旧数据兼容)");
|
||||
return ESP_OK;
|
||||
// 注意:返回 INVALID_STATE 而非 ESP_OK,由调用方决定是否容忍。
|
||||
// 历史上这里直接返回 OK 跳过校验,但 meta 被意外清零时会掩盖固件损坏。
|
||||
static const uint8_t kZeroSha[32] = {0};
|
||||
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
|
||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,返回 INVALID_STATE(调用方决定容忍或失败)");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint8_t computed[32] = {0};
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* 不再由 OtaRelay 承担存储职责(解决 H1 职责超载)。
|
||||
*
|
||||
* 线程模型:
|
||||
* - 所有方法均为 static,由调用方(协调器/OtaRelay/传输层)保证串行化
|
||||
* - 通过 instance() 单例访问,NVS 元数据读写由内部 mutex 保护
|
||||
* - metaLoad/metaSave/metaUpdate/metaClear 串行化,读-改-写原子
|
||||
* - 分区指针懒初始化,首次使用时查找并缓存
|
||||
*/
|
||||
#ifndef GD32_FLASH_STORE_H
|
||||
@@ -15,6 +16,8 @@
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_partition.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#define OTA_META_NVS_NAMESPACE "ota_meta" ///< OTA 元数据 NVS 命名空间
|
||||
|
||||
@@ -71,9 +74,11 @@ public:
|
||||
// ── NVS 元数据持久化 ──
|
||||
|
||||
/**
|
||||
* @brief 从 NVS 加载 OTA 元数据(命名空间不存在时返回全零结构)。
|
||||
* @param meta 输出元数据结构体指针。
|
||||
* @return ESP_OK 成功。
|
||||
* @brief 从 NVS 加载 OTA 元数据。
|
||||
* @param meta 输出元数据结构体指针(任何错误路径下都会被清零)。
|
||||
* @return ESP_OK 成功(含命名空间不存在的兼容路径,返回全零结构);
|
||||
* ESP_ERR_TIMEOUT 加锁超时;
|
||||
* 其他错误码 = nvs_open 真实失败(如 NVS 未初始化/损坏),不再一律当 OK。
|
||||
*/
|
||||
esp_err_t metaLoad(ota_meta_t *meta);
|
||||
|
||||
@@ -135,12 +140,24 @@ public:
|
||||
const esp_partition_t* getPartition();
|
||||
|
||||
private:
|
||||
Gd32FlashStore() = default;
|
||||
Gd32FlashStore();
|
||||
~Gd32FlashStore() = default;
|
||||
Gd32FlashStore(const Gd32FlashStore&) = delete;
|
||||
Gd32FlashStore& operator=(const Gd32FlashStore&) = delete;
|
||||
|
||||
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
||||
|
||||
// ── NVS 互斥锁(保护元数据读-改-写原子性)──
|
||||
// lockNvs 返回是否成功 take;unlockNvs(handle) 据此决定是否 give,
|
||||
// 参考 OtaCoordinator::lock/unlock 模式(避免共享 bool 被并发覆盖导致死锁)。
|
||||
SemaphoreHandle_t m_nvsMutex = nullptr;
|
||||
bool lockNvs(uint32_t timeout_ms);
|
||||
void unlockNvs(bool handle);
|
||||
static constexpr uint32_t kNvsLockTimeoutMs = 100; ///< NVS 加锁超时(ms)
|
||||
|
||||
// 已持锁版本:由持锁的公开方法或 metaUpdate 调用,不再自行加锁,避免递归 take 死锁
|
||||
esp_err_t metaLoadLocked(ota_meta_t *meta);
|
||||
esp_err_t metaSaveLocked(const ota_meta_t *meta);
|
||||
};
|
||||
|
||||
#endif // GD32_FLASH_STORE_H
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* protocol.cpp - 协议层实现
|
||||
* T2.1, T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
@@ -99,21 +99,17 @@ int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
|
||||
|
||||
memset(sess, 0, sizeof(session_ctx_t));
|
||||
memcpy(sess->key, k_session, 16);
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥和状态。
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
*/
|
||||
void Protocol::sessionKeyClear(session_ctx_t *sess)
|
||||
{
|
||||
if (sess) {
|
||||
memset(sess->key, 0, 16);
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* protocol.h - 帧格式、CRC16、AES 加密解密
|
||||
* T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
@@ -12,11 +12,6 @@
|
||||
#include <stdbool.h>
|
||||
#include "app_config.h"
|
||||
|
||||
// 帧格式: SOF(1B) + CMD(1B) + LEN(2B LE) + PAYLOAD(LEN) + CRC16(2B LE)
|
||||
#define FRAME_SOF (0xEB) // 帧起始标记
|
||||
#define FRAME_MAX_PAYLOAD (64) // 安全最大 payload
|
||||
#define FRAME_OVERHEAD (6) // SOF + CMD + LEN(2) + CRC16(2)
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16; ///< AES-128 主密钥长度(字节)
|
||||
inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(字节)
|
||||
@@ -24,8 +19,6 @@ inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(
|
||||
// AES 会话上下文
|
||||
typedef struct {
|
||||
uint8_t key[16]; // 会话密钥 K_session
|
||||
uint8_t iv[16]; // CBC 模式 IV
|
||||
int used; // 会话是否已初始化标志
|
||||
} session_ctx_t;
|
||||
|
||||
/**
|
||||
@@ -64,7 +57,7 @@ public:
|
||||
*/
|
||||
static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session);
|
||||
/**
|
||||
* @brief 清除会话上下文的密钥和 IV 数据。
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
*/
|
||||
static void sessionKeyClear(session_ctx_t *sess);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* spi_master.cpp - SPI Master 驱动实现
|
||||
* T1.1 - T1.4
|
||||
* ESP-IDF 6.0 适配版本
|
||||
@@ -707,7 +707,7 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||
DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries);
|
||||
|
||||
while (retry_count < max_retries) {
|
||||
ret = syncTransferUnlocked(cmd, payload, payload_len,
|
||||
ret = transferRequestResponse(cmd, payload, payload_len,
|
||||
recv_buf, recv_len, 3000);
|
||||
if (ret == ESP_OK) {
|
||||
if (retry_count > 0) {
|
||||
@@ -728,34 +728,3 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||
|
||||
// CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦)
|
||||
|
||||
// ============================================================
|
||||
// 无锁内部版本实现
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 无锁版本的同步 SPI 传输实现。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param payload_len 负载数据长度。
|
||||
* @param recv_buf 接收数据缓冲区。
|
||||
* @param recv_len 接收数据长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t SpiMaster::syncTransferUnlocked(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms)
|
||||
{
|
||||
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 无锁版本的 SPI 发送实现。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param len 负载数据长度。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t SpiMaster::sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len)
|
||||
{
|
||||
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* spi_master.h - SPI Master 驱动头文件
|
||||
* CANFD 3通道发送接口 + 底层 SPI 总线
|
||||
*
|
||||
@@ -182,27 +182,6 @@ private:
|
||||
|
||||
|
||||
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
|
||||
/**
|
||||
* @brief 无锁版本的同步 SPI 传输(调用者须已持有锁)。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param payload_len 负载数据长度。
|
||||
* @param recv_buf 接收数据缓冲区。
|
||||
* @param recv_len 接收数据长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t syncTransferUnlocked(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms);
|
||||
/**
|
||||
* @brief 无锁版本的 SPI 发送(调用者须已持有锁)。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param len 负载数据长度。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
/**
|
||||
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
|
||||
* @param resp_type 输出响应类型。
|
||||
|
||||
Reference in New Issue
Block a user