添加了SPI链接V2支持文件,重构OTA协调器的存储准备流程,
统一WiFi/BLE传输路径的收尾处理逻辑。 feat(ota): 优化OTA性能并增强系统监控功能 - 修改RSSI警告阈值从-75dBm调整为-80dBm,更新相关文档说明 - 增加HTTP服务器任务栈大小至10240字节,解决栈溢出问题 - 将OTA块大小从4096字节降至2048字节以降低峰值堆内存占用 - 实现按需擦除机制替代预擦除,在GD32 OTA中按扇区动态擦除 - 添加任务栈水位主动巡检功能,监控栈使用情况并告警 - 集成FreeRTOS栈溢出钩子,提供栈溢出检测和告警 - 优化ESP32 OTA实现使用顺序写入模式,消除长时间擦除阻塞 - 改进堆内存不足告警机制,添加限流避免刷屏 - 移除不必要的调试日志输出,精简健康监控模块
This commit is contained in:
+3
-1
@@ -9,9 +9,11 @@ idf_component_register(
|
||||
"server/wifi_ota.cpp" # WiFi HTTP OTA 服务器
|
||||
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
|
||||
"server/ble_ota.cpp" # BLE GATT OTA 服务器
|
||||
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存)
|
||||
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存/栈水位主动巡检)
|
||||
"monitor/can_feedback_task.cpp" # CAN 1Hz 反馈轮询任务(0xC1)
|
||||
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
|
||||
"monitor/task_stack_check.cpp" # 任务栈水位指标收集(health_mon 巡检 + /system/status 共享)
|
||||
"monitor/stack_overflow_hook.cpp" # FreeRTOS 栈溢出钩子(canary 触发时 UART 硬写告警)
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"core"
|
||||
|
||||
@@ -53,17 +53,15 @@ inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4; ///< AP 模
|
||||
// HTTP 服务器配置
|
||||
// ============================================================
|
||||
inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服务器监听端口
|
||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< HTTP 服务器任务栈大小(字节)
|
||||
// ★ 曾降至 6142 省 2KB,但 system_status_handler→sample_cpu_usage() 在栈上分配
|
||||
// TaskStatus_t tasks[24](~864B)+previous_samples[24](192B)+CpuUsageSnapshot(~608B)≈1.7KB,
|
||||
// 加上 spi_send_handler 的 buf[512]+resp[1024]+SPI 调用链 ≈3.5KB,
|
||||
// 再加 httpd 框架开销 ~2KB → 总需求逼近 6KB,6144 栈导致栈溢出 → TCB 被破坏
|
||||
// → "Stack protection fault"(栈边界被 "http" ASCII 覆盖)。恢复 8192 留足余量。
|
||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 10240; ///< HTTP 服务器任务栈大小(字节)
|
||||
// ★ 历史:6144 栈溢出;8192 在上传收尾仍紧。曾提到 12288 但 ESP32-C3 堆极紧
|
||||
// (OTA 后 free~5KB、largest~1KB)。SHA context 已改 static,httpd 栈 10KB 折中。
|
||||
// 真·栈溢出钩子会 abort;卡 100% 更多是堆碎导致回包/后续分配失败。
|
||||
|
||||
// ============================================================
|
||||
// OTA 配置
|
||||
// ============================================================
|
||||
inline constexpr size_t OTA_BLOCK_SIZE = 4096; ///< OTA 上传静态接收缓冲区,从 8192 降到 4096 省 4KB
|
||||
inline constexpr size_t OTA_BLOCK_SIZE = 2048; ///< 上传接收块:2KB 降峰值堆(原 4/8KB)
|
||||
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 轮询次数
|
||||
@@ -76,7 +74,7 @@ inline constexpr uint32_t OTA_POLL_NO_FRAME_DELAY_MS = 8; // 轮询未找
|
||||
// 健康监控阈值
|
||||
// ============================================================
|
||||
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85; ///< WiFi 信号严重警告阈值(dBm)
|
||||
inline constexpr int32_t WIFI_RSSI_WARNING = -75; ///< WiFi 信号一般警告阈值(dBm)
|
||||
inline constexpr int32_t WIFI_RSSI_WARNING = -80; ///< WiFi 信号一般警告阈值(dBm)
|
||||
inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; ///< OTA 传输期间 WiFi 信号临界阈值
|
||||
inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; ///< RSSI 采样最大有效期(毫秒)
|
||||
inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; ///< 每 N 包执行一次健康检查
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 41
|
||||
#define FW_VERSION_STRING "0.3.41"
|
||||
#define FW_VERSION_PATCH 42
|
||||
#define FW_VERSION_STRING "0.3.42"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-07-12 03:33:39"
|
||||
#define BUILD_TIME "2026-07-13 03:05:29"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "c0ca4e2"
|
||||
#define GIT_HASH "0b36349"
|
||||
|
||||
@@ -150,6 +150,7 @@ bool startCanFeedbackTask()
|
||||
|
||||
void pauseCanFeedbackTask(bool pause)
|
||||
{
|
||||
if (s_paused == pause) return; // 幂等:状态未变时不重复日志
|
||||
s_paused = pause;
|
||||
if (pause) {
|
||||
DEBUG_INFO(kTag, "CAN 监听已暂停(OTA 进行中)");
|
||||
|
||||
@@ -14,10 +14,25 @@
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_system.h"
|
||||
#include "driver/temperature_sensor.h"
|
||||
|
||||
#include "health_monitor.h"
|
||||
#include "debug_log.h"
|
||||
#include "task_stack_check.h"
|
||||
#include "stack_overflow_hook.h"
|
||||
|
||||
// ============================================================
|
||||
// 栈水位主动巡检
|
||||
// ============================================================
|
||||
// 告警阈值:剩余高水位 < 栈大小 20% 即告警(复用 /system/status 的任务表)。
|
||||
// 去重:每个任务只在"水位由安全跌入危险"时告警一次,恢复后再跌入才再次告警,
|
||||
// 避免 5 秒巡检刷屏。health_mon 任务从不被 OTA 暂停,DEBUG_ERR 经 UDP/UART
|
||||
// 直发,不受 OTA 门控抑制。
|
||||
static constexpr float STACK_WARN_RATIO = 0.20f; ///< 剩余水位告警阈值(栈大小比例)
|
||||
static constexpr size_t STACK_TASK_MAX = 8; ///< 受监控任务上限
|
||||
static bool s_stackWarned[STACK_TASK_MAX] = {}; ///< 各任务是否已告警(去重)
|
||||
|
||||
// ============================================================
|
||||
// 单例
|
||||
@@ -190,7 +205,6 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_ERROR_CHECK(temperature_sensor_enable(self.m_tempHandle));
|
||||
DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)");
|
||||
}
|
||||
|
||||
// 定时器,每1秒读取一次温度
|
||||
@@ -202,7 +216,6 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
|
||||
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", "创建温度监控定时器失败");
|
||||
@@ -210,16 +223,62 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
|
||||
// 周期性巡检
|
||||
while (1) {
|
||||
// 栈溢出 canary 触发检测:vApplicationStackOverflowHook 置位后,
|
||||
// 在 health_mon 完好栈上用 DEBUG_ERR 经 UDP/UART 发详细告警。
|
||||
// 标志只在置位后告警一次,不自动清除(canary 触发即不可逆,需重启恢复)。
|
||||
if (g_stackOverflowDetected) {
|
||||
DEBUG_ERR("MON", "!!! 栈溢出已被内核 canary 检测到,请检查任务栈大小 !!!");
|
||||
// 告警一次后清标志,避免 5 秒刷屏;后续若再次触发会重新置位
|
||||
g_stackOverflowDetected = false;
|
||||
}
|
||||
|
||||
uint32_t free_heap = esp_get_free_heap_size();
|
||||
if (free_heap < HEAP_WARNING_MIN) {
|
||||
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||
DEBUG_WARN("MON", "堆内存不足: free=%lu min=%lu largest_blk=%lu",
|
||||
free_heap, esp_get_minimum_free_heap_size(), largest_block);
|
||||
/* OTA 期间堆会短暂变紧,限流避免串口/UDP 二次吃堆、刷屏 */
|
||||
static TickType_t s_last_heap_warn = 0;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if ((now - s_last_heap_warn) >= pdMS_TO_TICKS(2000)) {
|
||||
s_last_heap_warn = now;
|
||||
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||
DEBUG_WARN("MON", "堆内存不足: free=%lu min=%lu largest_blk=%lu",
|
||||
(unsigned long)free_heap,
|
||||
(unsigned long)esp_get_minimum_free_heap_size(),
|
||||
(unsigned long)largest_block);
|
||||
}
|
||||
}
|
||||
health_state_t cur = self.getState();
|
||||
if (cur >= SYS_CRITICAL) {
|
||||
DEBUG_ERR("MON", "健康状态危险: %d", cur);
|
||||
}
|
||||
|
||||
// 栈水位主动巡检:复用 /system/status 的任务表,低水位时 DEBUG_ERR 主动告警。
|
||||
// health_mon 任务不被 OTA 暂停,告警经 UDP/UART 直发,穿透 OTA 门控。
|
||||
TaskStackMetric metrics[STACK_TASK_MAX];
|
||||
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
|
||||
for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) {
|
||||
if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) {
|
||||
continue;
|
||||
}
|
||||
UBaseType_t hw = uxTaskGetStackHighWaterMark(metrics[i].handle);
|
||||
size_t warn_bytes = (size_t)(metrics[i].stack_size_bytes * STACK_WARN_RATIO);
|
||||
if (hw < (UBaseType_t)warn_bytes) {
|
||||
if (!s_stackWarned[i]) {
|
||||
DEBUG_ERR("MON", "!!! 栈水位过低 !!! 任务=%s 剩余=%luB 阈值=%luB(%.0f%%) 栈大小=%luB",
|
||||
metrics[i].name,
|
||||
(unsigned long)hw,
|
||||
(unsigned long)warn_bytes,
|
||||
STACK_WARN_RATIO * 100.0f,
|
||||
(unsigned long)metrics[i].stack_size_bytes);
|
||||
s_stackWarned[i] = true;
|
||||
}
|
||||
} else if (s_stackWarned[i]) {
|
||||
// 水位恢复,清除告警标志,允许下次再次跌入时告警
|
||||
s_stackWarned[i] = false;
|
||||
DEBUG_INFO("MON", "栈水位恢复: 任务=%s 剩余=%luB",
|
||||
metrics[i].name, (unsigned long)hw);
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* stack_overflow_hook.cpp - FreeRTOS 栈溢出钩子
|
||||
*
|
||||
* CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY 开启后,内核在任务切换时检查
|
||||
* 栈 canary,若被踩则回调 vApplicationStackOverflowHook。
|
||||
*
|
||||
* 钩子在被踩栈的任务上下文中执行,此时该任务栈已损坏 —— 禁止任何依赖栈 /
|
||||
* mutex / 网络栈的调用(printf 格式化、UDP sendto、xSemaphoreTake 等都可能
|
||||
* 二次崩溃)。唯一安全做法:用 uart_write_bytes 往 UART0 硬写预写好的常量
|
||||
* 字符串(物理外设直写,不需要栈空间),并置一个 volatile 标志,由
|
||||
* HealthMonitor 巡检循环(运行在完好的 health_mon 栈上)后续用 DEBUG_ERR
|
||||
* 经 UDP/UART 发详细告警。
|
||||
*
|
||||
* UART0 直写不受 OTA 门控抑制,是 OTA 末尾断网阶段唯一可靠的物理通道。
|
||||
*/
|
||||
#include <cstring>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
// 供 HealthMonitor 巡检检测:canary 触发后置位,由巡检发详细告警。
|
||||
volatile bool g_stackOverflowDetected = false;
|
||||
|
||||
extern "C" void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
|
||||
{
|
||||
(void)xTask;
|
||||
g_stackOverflowDetected = true;
|
||||
|
||||
// 栈已损坏,只能做不依赖栈的最小操作。UART0 硬写常量字符串。
|
||||
static const char prefix[] = "\r\n!!! STACK OVERFLOW in task: ";
|
||||
uart_write_bytes(UART_NUM_0, prefix, sizeof(prefix) - 1);
|
||||
// 任务名最多 configMAX_TASK_NAME_LEN,strlen 对小已知 buffer 安全
|
||||
uart_write_bytes(UART_NUM_0, pcTaskName, strnlen(pcTaskName, configMAX_TASK_NAME_LEN));
|
||||
static const char suffix[] = " !!!\r\n";
|
||||
uart_write_bytes(UART_NUM_0, suffix, sizeof(suffix) - 1);
|
||||
|
||||
/* 立刻 abort:OTA 上传 100% 后若 httpd 栈溢出却不 abort,
|
||||
* 前端会一直停在 100% 像“卡住”,且 TCB 已损坏无法恢复。 */
|
||||
esp_system_abort("stack overflow");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* stack_overflow_hook.h - FreeRTOS 栈溢出钩子标志声明
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* canary 触发时由 vApplicationStackOverflowHook 置位;
|
||||
* HealthMonitor 巡检循环检测此标志,置位后用 DEBUG_ERR 发详细告警。 */
|
||||
extern volatile bool g_stackOverflowDetected;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* task_stack_check.cpp - 任务栈水位指标收集(共享)
|
||||
*
|
||||
* 实现 collectTaskStackMetrics():复用 http_handlers 旧版 task_metrics[] 任务表,
|
||||
* 供 health_monitor 主动巡检与 /system/status 被动报告共用。
|
||||
*/
|
||||
#include "task_stack_check.h"
|
||||
|
||||
#include "app_config.h"
|
||||
#include "health_monitor.h"
|
||||
#include "ota_relay.h"
|
||||
#include "wifi_ota.h"
|
||||
#include "ble_ota.h"
|
||||
|
||||
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
|
||||
{
|
||||
if (!out || max == 0) return 0;
|
||||
|
||||
size_t n = 0;
|
||||
|
||||
// httpd 由 esp_http_server 创建,无公开句柄;按任务名取。
|
||||
// httpd 任务在 httpd_start() 后才存在,未启动时 xTaskGetHandle 返回 NULL。
|
||||
if (n < max) {
|
||||
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), 6144 };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), 4096 };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 };
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* task_stack_check.h - 任务栈水位指标收集(共享)
|
||||
*
|
||||
* 将受监控任务的任务名/句柄/栈大小统一收集,供:
|
||||
* - health_monitor 巡检主动告警(低水位时 DEBUG_ERR,突破 OTA 门控)
|
||||
* - http_handlers /system/status 被动报告(前端每秒轮询展示)
|
||||
* 两处复用同一份任务表,避免散落维护。
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
/** @brief 单个任务的栈指标描述。 */
|
||||
struct TaskStackMetric {
|
||||
const char* name; ///< 任务名(展示用)
|
||||
TaskHandle_t handle; ///< FreeRTOS 任务句柄(可能为 NULL:任务尚未创建)
|
||||
size_t stack_size_bytes; ///< 创建该任务时分配的栈大小(字节)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 收集所有受监控任务的栈指标。
|
||||
* @param out 输出数组。
|
||||
* @param max out 数组容量。
|
||||
* @return 实际写入条数(<= max)。
|
||||
*
|
||||
* 注意:部分任务句柄在任务创建前为 NULL,调用方应跳过 handle==NULL 的条目。
|
||||
*/
|
||||
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max);
|
||||
+37
-22
@@ -12,6 +12,8 @@
|
||||
#include "esp_system.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_partition.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
|
||||
#include "mbedtls/private/sha256.h"
|
||||
@@ -170,19 +172,23 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
// BLE OTA 修复:传固件实际大小而非分区全尺寸,避免全分区擦除导致
|
||||
// 长时间阻塞触发 Task Watchdog(ESP32-C3 单核 5s 超时)。
|
||||
// esp_ota_begin 按 image_size 擦除;esp_ota_end 仅校验已写数据,
|
||||
// 未擦除残留扇区不影响校验。size 对齐到 64KB 块边界确保尾部干净。
|
||||
size_t erase_size = (total_size + 0xFFFF) & ~0xFFFFULL;
|
||||
if (erase_size > m_partition->size) {
|
||||
erase_size = m_partition->size;
|
||||
}
|
||||
esp_err_t err = esp_ota_begin(m_partition, erase_size, &m_handle);
|
||||
/* ★ 用 OTA_WITH_SEQUENTIAL_WRITES:边写边按扇区擦除,begin 几乎瞬时返回。
|
||||
* 旧逻辑按 64KB 对齐预擦 ~1MB,会在 /ota/upload 开头阻塞几十秒~数分钟:
|
||||
* 浏览器 upload 进度已到 100%,设备还在擦 Flash → 表现为“卡在 100% 很久才成功”。 */
|
||||
DEBUG_INFO(TAG, "esp_ota_begin(SEQUENTIAL) 目标=%s size=%u free=%lu...",
|
||||
m_partition->label, (unsigned)total_size,
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
const int64_t t0 = esp_timer_get_time();
|
||||
esp_err_t err = esp_ota_begin(m_partition, OTA_WITH_SEQUENTIAL_WRITES, &m_handle);
|
||||
const int64_t t1 = esp_timer_get_time();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
|
||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s (耗时 %lld ms)",
|
||||
esp_err_to_name(err), (long long)((t1 - t0) / 1000));
|
||||
return beginCleanupAndReturn(err);
|
||||
}
|
||||
DEBUG_INFO(TAG, "esp_ota_begin 完成 耗时 %lld ms free=%lu",
|
||||
(long long)((t1 - t0) / 1000),
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
|
||||
m_received = 0;
|
||||
m_active = true;
|
||||
@@ -190,7 +196,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
m_ended = false;
|
||||
memset(m_sha256, 0, sizeof(m_sha256));
|
||||
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节",
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase)",
|
||||
m_partition->label, (unsigned)total_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -207,11 +213,14 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* sequential 模式写时可能触发扇区擦除,喂狗防 WDT */
|
||||
(void)esp_task_wdt_reset();
|
||||
esp_err_t err = esp_ota_write(m_handle, data, len);
|
||||
if (err == ESP_OK) {
|
||||
m_received += len;
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u)", esp_err_to_name(err), (unsigned)len);
|
||||
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u offset=%u)",
|
||||
esp_err_to_name(err), (unsigned)len, (unsigned)m_received);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
@@ -228,30 +237,36 @@ esp_err_t Esp32OtaCore::computeShaFromPartition()
|
||||
|
||||
DEBUG_INFO(TAG, "从 OTA 分区读回计算 SHA-256 (%u 字节)...", (unsigned)m_received);
|
||||
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
mbedtls_sha256_starts(&ctx, 0);
|
||||
/* static context + 小读缓冲:勿占 httpd 栈,也勿再静态吃 4KB BSS */
|
||||
static mbedtls_sha256_context s_sha_ctx;
|
||||
static uint8_t s_sha_read_buf[1024];
|
||||
|
||||
// ★ static 缓冲区:OTA 操作由 OtaCoordinator 互斥串行化,无并发风险
|
||||
static uint8_t s_sha_read_buf[OTA_BLOCK_SIZE];
|
||||
mbedtls_sha256_init(&s_sha_ctx);
|
||||
mbedtls_sha256_starts(&s_sha_ctx, 0);
|
||||
|
||||
size_t offset = 0;
|
||||
uint32_t wdt_chunk = 0;
|
||||
while (offset < m_received) {
|
||||
size_t to_read = m_received - offset;
|
||||
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
||||
if (to_read > sizeof(s_sha_read_buf)) to_read = sizeof(s_sha_read_buf);
|
||||
esp_err_t rd_err = esp_partition_read(m_partition, offset, s_sha_read_buf, to_read);
|
||||
if (rd_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "SHA 计算: 分区读失败 @offset %u: %s",
|
||||
(unsigned)offset, esp_err_to_name(rd_err));
|
||||
mbedtls_sha256_free(&ctx);
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
return rd_err;
|
||||
}
|
||||
mbedtls_sha256_update(&ctx, s_sha_read_buf, to_read);
|
||||
mbedtls_sha256_update(&s_sha_ctx, s_sha_read_buf, to_read);
|
||||
offset += to_read;
|
||||
/* 长固件 SHA 可能 > TASK_WDT(5s);在 httpd 上跑时必须喂狗 */
|
||||
if ((++wdt_chunk & 0x0FU) == 0U) {
|
||||
(void)esp_task_wdt_reset();
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_sha256_finish(&ctx, m_sha256);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
mbedtls_sha256_finish(&s_sha_ctx, m_sha256);
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
m_shaComputed = true;
|
||||
|
||||
char hex[65] = {0};
|
||||
|
||||
@@ -181,9 +181,6 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
|
||||
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 两路径的大小检查)
|
||||
@@ -200,18 +197,12 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
|
||||
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;
|
||||
}
|
||||
// 3. 复位按需擦除水位(不在此处整段预擦)
|
||||
// 旧实现在 prepare 阶段 erase 整包固件对应分区:HTTP 主线程阻塞数秒~数十秒,
|
||||
// 期间不 httpd_req_recv → TCP 窗口塞满后浏览器进度先到 100%,再空等擦+写+SHA。
|
||||
// 现改为 flashWriteChunk 内按扇区 ensureErased,边收边擦写,进度与真实处理对齐。
|
||||
Gd32FlashStore::instance().beginWriteSession();
|
||||
DEBUG_INFO(TAG, "prepareGd32Storage: 按需擦除会话已复位 (fw=%" PRIu32 ")", total_size);
|
||||
|
||||
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
|
||||
clearGd32Meta();
|
||||
@@ -243,6 +234,12 @@ void OtaCoordinator::completeGd32Storage()
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
|
||||
{
|
||||
completeGd32Storage();
|
||||
return finalizeGd32Checksum();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
|
||||
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
|
||||
|
||||
@@ -124,10 +124,9 @@ public:
|
||||
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 清理,调用方无需再清理。
|
||||
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 复位按需擦除水位 + 清空 meta + 写入初始 meta。
|
||||
* 扇区擦除在 writeGd32Chunk/flashWriteChunk 内按写入范围推进(非整段预擦)。
|
||||
* WiFi/BLE 上传入口共用,任意步骤失败时内部 abortCurrent,调用方无需再清理。
|
||||
* @param total_size 固件总字节数。
|
||||
* @param channel 发起通道(WiFi/BLE)。
|
||||
* @return ESP_OK 成功;ESP_ERR_INVALID_SIZE 大小非法;ESP_ERR_INVALID_STATE 系统忙;其他擦除/写入错误。
|
||||
@@ -140,6 +139,13 @@ public:
|
||||
*/
|
||||
void completeGd32Storage();
|
||||
|
||||
/**
|
||||
* @brief 收流完成后的统一收尾:completeGd32Storage + finalizeGd32Checksum。
|
||||
* WiFi/BLE 传输层共用,避免一端只 complete、另一端漏算 SHA 的分叉。
|
||||
* @return finalizeGd32Checksum 的返回值(complete 本身无错误码)。
|
||||
*/
|
||||
esp_err_t finishGd32StorageAfterReceive();
|
||||
|
||||
// ── GD32 存储操作(传输层通过协调器操作 Flash,解决 H3 绕过问题)──
|
||||
/**
|
||||
* @brief 向 GD32 Flash 分区写入固件数据块。
|
||||
|
||||
+151
-76
@@ -10,7 +10,6 @@
|
||||
* 协议子步骤与加密/Flash 工具方法保持不变。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -258,12 +257,13 @@ ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *pay
|
||||
// 进入 HANDSHAKING 阶段:执行完整的握手 + 认证流程
|
||||
DEBUG_INFO("OTA", "=== 阶段1: 握手与认证开始 ===");
|
||||
|
||||
// 步骤1: 请求 GD32 进入 Bootloader
|
||||
// 0xB0 对 APP:触发写标志+复位 → BOOT 启动
|
||||
// 0xB0 对 BOOT:返回 0xB5 + resetToIdle()(IDLE 状态下无害)
|
||||
// 步骤1: 请求 GD32 进入 Bootloader(或确认已在 BOOT)
|
||||
// 必须 HELLO role=boot 成功,禁止“未确认就盲发握手”
|
||||
esp_err_t err = requestGd32AppEnterBootloader();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "GD32 进入 Bootloader 请求未确认,继续尝试握手");
|
||||
DEBUG_ERR("OTA", "Bootloader 未就绪: %s", esp_err_to_name(err));
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
// 步骤2: 发送握手请求
|
||||
@@ -311,21 +311,23 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段2: 固件大小协商 (%" PRIu32 " 字节) ===", meta.total_size);
|
||||
|
||||
m_transferTotalPackets = (meta.total_size + GD32_OTA_PACKET_SIZE - 1U) / GD32_OTA_PACKET_SIZE;
|
||||
m_transferCurrentSeq = 0;
|
||||
m_transferFlashOffset = 0;
|
||||
m_transferRetryCount = 0;
|
||||
/* total 提前写入,前端可显示「共 N 包」;seq 保持 0 直到真正开始转发 */
|
||||
setRelayTotalPackets(m_transferTotalPackets);
|
||||
setRelaySeq(0);
|
||||
|
||||
ota_result_t res = doSendFirmwareSize(meta.total_size);
|
||||
if (res != OTA_RESULT_OK) {
|
||||
setState(RELAY_ERROR);
|
||||
return res;
|
||||
}
|
||||
|
||||
// 初始化传输进度
|
||||
m_transferTotalPackets = (meta.total_size + GD32_OTA_PACKET_SIZE - 1U) / GD32_OTA_PACKET_SIZE;
|
||||
m_transferCurrentSeq = 0;
|
||||
m_transferFlashOffset = 0;
|
||||
m_transferRetryCount = 0;
|
||||
setRelayTotalPackets(m_transferTotalPackets);
|
||||
setRelaySeq(0);
|
||||
|
||||
/* 进入转发前先切状态,避免 status 长时间停在 SIZE_NEG 导致前端像卡在 0/N */
|
||||
setState(RELAY_TRANSFERRING);
|
||||
setRelaySeq(0);
|
||||
DEBUG_INFO("OTA", "=== 阶段2 完成: 总包数=%" PRIu32 ", → TRANSFERRING ===", m_transferTotalPackets);
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
@@ -373,7 +375,7 @@ ota_result_t OtaRelay::handleFinishing(relay_event_t event, const uint8_t *paylo
|
||||
return rej;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段4-5: 结束OTA + APP验证 ===");
|
||||
DEBUG_INFO("OTA", "结束中继,等待 GD32 APP 启动");
|
||||
|
||||
ota_result_t res = doFinishAndVerify();
|
||||
if (res != OTA_RESULT_OK) {
|
||||
@@ -382,7 +384,7 @@ ota_result_t OtaRelay::handleFinishing(relay_event_t event, const uint8_t *paylo
|
||||
}
|
||||
|
||||
setState(RELAY_DONE);
|
||||
DEBUG_INFO("OTA", "=== OTA 全流程完成: → DONE ===");
|
||||
// 成功总结只在 doWaitForAppAlive 打一条,避免多层「完成」刷屏
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -572,8 +574,8 @@ esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type,
|
||||
uint32_t round = 0;
|
||||
while (*resp_type == SPI_OTA_RSP_WAIT && round < max_wait_rounds) {
|
||||
++round;
|
||||
DEBUG_WARN("OTA", "%s WAIT, poll #%" PRIu32 "...", stage, round);
|
||||
esp_err_t err = SpiMaster::instance().otaPollResponse(resp_type, resp_payload, resp_max, resp_len, 100);
|
||||
/* 进度/等待文案只走前端;此处不打 INFO,避免 /logs 刷屏 */
|
||||
esp_err_t err = SpiMaster::instance().otaPollResponse(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||||
if (err == ESP_ERR_TIMEOUT) { *resp_type = SPI_OTA_RSP_WAIT; continue; }
|
||||
if (err != ESP_OK) return err;
|
||||
logRespPayload(stage, *resp_type, resp_payload, resp_len ? *resp_len : 0);
|
||||
@@ -667,11 +669,18 @@ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage,
|
||||
if (err != ESP_OK) return err;
|
||||
}
|
||||
for (uint32_t extra = 0; extra < maxExtraPolls; ++extra) {
|
||||
if (*resp_type != SPI_OTA_RSP_ACK) return ESP_OK;
|
||||
if (matcher(resp_payload, *resp_len)) return ESP_OK;
|
||||
uint32_t err_seq = 0; uint8_t err_code = 0;
|
||||
if (parseStructuredError(resp_payload, *resp_len, &err_seq, &err_code)) return ESP_OK;
|
||||
DEBUG_WARN("OTA", "%s stale ACK, poll more: len=%u", stage, (unsigned)*resp_len);
|
||||
if (*resp_type == SPI_OTA_RSP_ACK) {
|
||||
if (matcher(resp_payload, *resp_len)) return ESP_OK;
|
||||
uint32_t err_seq = 0; uint8_t err_code = 0;
|
||||
if (parseStructuredError(resp_payload, *resp_len, &err_seq, &err_code)) return ESP_OK;
|
||||
DEBUG_WARN("OTA", "%s stale ACK, poll more: len=%u", stage, (unsigned)*resp_len);
|
||||
} else if (*resp_type == SPI_OTA_RSP_ERROR || *resp_type == 0x7FU) {
|
||||
/* 明确错误帧:交给调用方判失败,不再空转 poll */
|
||||
return ESP_OK;
|
||||
} else {
|
||||
/* 非 ACK(含 0xC1 事件/空 type):继续 poll 等延迟响应,勿提前返回 */
|
||||
DEBUG_WARN("OTA", "%s unexpected type=0x%02X, poll more", stage, *resp_type);
|
||||
}
|
||||
*resp_len = 0;
|
||||
err = SpiMaster::instance().otaPollResponse(resp_type, resp_payload, resp_max, resp_len, poll_timeout_ms);
|
||||
if (err != ESP_OK) return err;
|
||||
@@ -761,17 +770,49 @@ void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48,
|
||||
esp_err_t OtaRelay::requestGd32AppEnterBootloader()
|
||||
{
|
||||
DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口");
|
||||
SpiMaster::instance().appRequestBootloader();
|
||||
|
||||
/* 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪
|
||||
* APP 模式: 复位前拉低 PB6 → BOOT 极早拉低 → init 完成释放 HIGH
|
||||
* BOOT 模式: PB6 一直 HIGH,Phase1 超时后直接握手
|
||||
* 超时退化为固定延时(容错旧固件) */
|
||||
esp_err_t wait_err = SpiMaster::instance().waitForBootloaderReady(3000);
|
||||
if (wait_err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "硬件就绪信号超时,退化为固定延时");
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
/* 1) 探测:已在 BOOT 则零开销直接成功 */
|
||||
esp_err_t hello_err = SpiMaster::instance().hello(1500U, 0U);
|
||||
if (hello_err == ESP_OK && SpiMaster::instance().gd32Role() == V2_ROLE_BOOT) {
|
||||
DEBUG_INFO("OTA", "GD32 已在 Bootloader (HELLO role=boot),跳过 ENTER_BOOT");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* 2) 发 ENTER_BOOT(ACK 可选;发送后静默,门控是 BOOT HELLO) */
|
||||
if (hello_err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "探测 HELLO 未成功: %s,仍尝试 ENTER_BOOT",
|
||||
esp_err_to_name(hello_err));
|
||||
}
|
||||
esp_err_t boot_err = SpiMaster::instance().appRequestBootloader();
|
||||
if (boot_err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "ENTER_BOOT 发送失败: %s", esp_err_to_name(boot_err));
|
||||
}
|
||||
|
||||
/* 3) 硬门控:HELLO role=boot(给 APP 擦页+复位+BOOT 起机更长时间) */
|
||||
esp_err_t wait_err = SpiMaster::instance().waitForBootloaderReady(12000);
|
||||
if (wait_err == ESP_OK) {
|
||||
DEBUG_INFO("OTA", "Bootloader 已就绪 (HELLO role=boot)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* 4) 一次重试:仍为 APP 时再 HELLO + ENTER_BOOT */
|
||||
DEBUG_WARN("OTA", "BOOT HELLO 超时,二次 HELLO+ENTER_BOOT 重试...");
|
||||
hello_err = SpiMaster::instance().hello(2000U, 0U);
|
||||
if (hello_err == ESP_OK && SpiMaster::instance().gd32Role() == V2_ROLE_BOOT) {
|
||||
DEBUG_INFO("OTA", "二次探测已在 Bootloader,继续");
|
||||
return ESP_OK;
|
||||
}
|
||||
boot_err = SpiMaster::instance().appRequestBootloader();
|
||||
if (boot_err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "二次 ENTER_BOOT 未确认 ACK: %s(仍等 BOOT HELLO)",
|
||||
esp_err_to_name(boot_err));
|
||||
}
|
||||
wait_err = SpiMaster::instance().waitForBootloaderReady(12000);
|
||||
if (wait_err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Bootloader HELLO 就绪失败(重试后): %s", esp_err_to_name(wait_err));
|
||||
return wait_err;
|
||||
}
|
||||
DEBUG_INFO("OTA", "Bootloader 已就绪 (HELLO role=boot, 二次)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -785,18 +826,27 @@ ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts)
|
||||
uint8_t last_resp_type = 0U;
|
||||
size_t last_resp_len = 0U;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
constexpr uint32_t kHandshakeReqTimeoutMs = 200U;
|
||||
constexpr uint32_t kHandshakePollTimeoutMs = 200U;
|
||||
constexpr uint32_t kHandshakeRetryGapMs = 100U;
|
||||
constexpr uint32_t kHandshakeReqTimeoutMs = 800U;
|
||||
constexpr uint32_t kHandshakePollTimeoutMs = 800U;
|
||||
constexpr uint32_t kHandshakeRetryGapMs = 50U;
|
||||
|
||||
for (uint32_t attempt = 0U; attempt < attempts; ++attempt) {
|
||||
uint8_t resp_type = 0U;
|
||||
uint8_t resp_payload[64] = {0};
|
||||
size_t resp_len = 0U;
|
||||
|
||||
/* 每若干次失败做一次 HELLO 重同步(session/从机状态) */
|
||||
if (attempt > 0U && (attempt % 5U) == 0U) {
|
||||
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_BOOT);
|
||||
DEBUG_WARN("OTA", "HANDSHAKE 前 HELLO 重同步: %s role=%u",
|
||||
esp_err_to_name(hr), SpiMaster::instance().gd32Role());
|
||||
}
|
||||
|
||||
/* otaTransfer 已含延迟一拍两步法;再经 pollUntil 处理 WAIT/陈旧 ACK。 */
|
||||
const uint32_t total_timeout_ms = kHandshakeReqTimeoutMs + kHandshakePollTimeoutMs;
|
||||
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, payload, sizeof(payload),
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
kHandshakeReqTimeoutMs);
|
||||
total_timeout_ms);
|
||||
last_err = err;
|
||||
last_resp_type = resp_type;
|
||||
last_resp_len = resp_len;
|
||||
@@ -915,16 +965,19 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
||||
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload),
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||
logRespPayload("SEND_SIZE", resp_type, resp_payload, resp_len);
|
||||
DEBUG_WARN("OTA", "SEND_SIZE poll config: poll_timeout_ms=2000 wait_rounds=300 maxExtraPolls=24");
|
||||
/* GD32 收到 SIZE 后擦 Flash,先 WAIT 再 ACK(seq=0);进度由前端读 status,不打过程 INFO */
|
||||
err = pollUntilExpectedAckShape("SEND_SIZE", &resp_type, resp_payload,
|
||||
sizeof(resp_payload), &resp_len,
|
||||
2000, 300, 24, OtaRelay::isSizeSeq0Ack);
|
||||
/*poll_timeout_ms=*/500,
|
||||
/*wait_rounds=*/120,
|
||||
/*maxExtraPolls=*/40,
|
||||
OtaRelay::isSizeSeq0Ack);
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isSizeSeq0Ack(resp_payload, resp_len)) {
|
||||
DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "大小已发送: %" PRIu32 " 字节,等待 GD32 确认...", size);
|
||||
DEBUG_INFO("OTA", "大小协商完成: %" PRIu32 " 字节,GD32 已就绪收包", size);
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -988,17 +1041,33 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
|
||||
// ★ 单包耗时统计,用于定位 OTA 速度瓶颈。
|
||||
TickType_t pkt_start = xTaskGetTickCount();
|
||||
/* 单包硬超时:避免重试环 + WAIT/CREDIT 轮询在异常时“静默卡死”十几分钟。 */
|
||||
const TickType_t pkt_deadline = pkt_start + pdMS_TO_TICKS(15000);
|
||||
TickType_t last_stall_log = pkt_start;
|
||||
|
||||
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
|
||||
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
|
||||
resend_packet:
|
||||
while (1) {
|
||||
if (xTaskGetTickCount() >= pkt_deadline) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 单包超时(15s), retries=%" PRIu32 " credit=%u",
|
||||
seq, retry_count, (unsigned)SpiMaster::instance().remoteCredit());
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
if ((xTaskGetTickCount() - last_stall_log) >= pdMS_TO_TICKS(2000)) {
|
||||
DEBUG_INFO("OTA", "包 %" PRIu32 " 仍在等待 ACK (retry=%" PRIu32 " credit=%u)",
|
||||
seq, retry_count, (unsigned)SpiMaster::instance().remoteCredit());
|
||||
last_stall_log = xTaskGetTickCount();
|
||||
}
|
||||
|
||||
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);
|
||||
/* WAIT 常见于 GD32 主循环 AES/写 Flash:多轮 poll,勿过早放弃。 */
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 200, 40);
|
||||
}
|
||||
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);
|
||||
@@ -1007,7 +1076,7 @@ resend_packet:
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
continue; // 重发本包
|
||||
}
|
||||
|
||||
@@ -1026,11 +1095,13 @@ resend_packet:
|
||||
}
|
||||
}
|
||||
|
||||
/* CREDIT 感知:若 GD32 报 CREDIT=0,说明它在写 Flash 缓冲,
|
||||
* 此时重发只会被丢弃。改为耐心 POLL 等待 CREDIT 恢复,最多等 500ms。 */
|
||||
/* CREDIT 感知:GD32 写 Flash / DATA 队列满时 CREDIT=0,耐心 POLL。 */
|
||||
if (SpiMaster::instance().remoteCredit() == 0U) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " GD32 CREDIT=0(写Flash中),POLL 等待...", seq);
|
||||
for (uint32_t cw = 0; cw < 50U; ++cw) {
|
||||
DEBUG_INFO("OTA", "包 %" PRIu32 " GD32 CREDIT=0,等待恢复...", seq);
|
||||
for (uint32_t cw = 0; cw < 100U; ++cw) {
|
||||
if (xTaskGetTickCount() >= pkt_deadline) {
|
||||
break;
|
||||
}
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload,
|
||||
sizeof(resp_payload), &resp_len, 100);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK &&
|
||||
@@ -1038,10 +1109,13 @@ resend_packet:
|
||||
got_expected_credit_ok = true;
|
||||
break;
|
||||
}
|
||||
if (err == ESP_OK && SpiMaster::instance().remoteCredit() > 0U) {
|
||||
break; /* CREDIT 恢复,可以继续轮询或重发 */
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
continue;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
if (err == ESP_OK && SpiMaster::instance().remoteCredit() > 0U) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,8 +1194,6 @@ resend_packet:
|
||||
|
||||
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
{
|
||||
DEBUG_INFO("OTA", "开始转发 %" PRIu32 " 包", m_transferTotalPackets);
|
||||
|
||||
ota_meta_t meta;
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "元数据加载失败");
|
||||
@@ -1160,19 +1232,20 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// 进度只写 relay_seq 供 /system/status;不打文字日志(前端进度条负责展示)。
|
||||
|
||||
// ★ 包间延迟:给 GD32 Bootloader 充足时间完成 flash 写入并准备下一包 ACK。
|
||||
// 无此延迟时,ESP32 立即 poll 会读到上一包的 stale ACK,导致误判为丢包。
|
||||
if (seq < m_transferTotalPackets - 1 && CONFIG_DCU_OTA_PACKET_INTERVAL_MS > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_PACKET_INTERVAL_MS));
|
||||
// ★ 包间延迟:给 GD32 写 Flash + 让低优先级 httpd 能应答 /system/status。
|
||||
// ota_relay 优先级高于 wifi_ota:若不主动让出,前端只能看到 SIZE→完成,中间全跳过。
|
||||
if (seq < m_transferTotalPackets - 1) {
|
||||
uint32_t gap_ms = CONFIG_DCU_OTA_PACKET_INTERVAL_MS;
|
||||
if (gap_ms < 1U) {
|
||||
gap_ms = 1U;
|
||||
}
|
||||
/* 每 16 包额外让出 15ms,保证 status/logs 轮询有机会跑 */
|
||||
if (((seq + 1U) % 16U) == 0U) {
|
||||
gap_ms = (gap_ms > 15U) ? gap_ms : 15U;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(gap_ms));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -1195,10 +1268,11 @@ bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
|
||||
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);
|
||||
// 启动探测等场景用 DBG;OTA 成功路径由 doWaitForAppAlive 统一 INFO 总结
|
||||
DEBUG_DBG("OTA", "GD32 APP 存活: uptime=%" PRIu32 "s 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;
|
||||
@@ -1271,27 +1345,31 @@ ota_result_t OtaRelay::doSendFinishOta()
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:轮询等待 GD32 APP 存活(30 次重试)。 */
|
||||
/** @brief 协议子步骤:轮询等待 GD32 APP 存活(10 次重试)。 */
|
||||
ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
|
||||
{
|
||||
// ★ APP 存活验证
|
||||
setGd32AppAlive(false);
|
||||
|
||||
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||
for (uint32_t attempt = 0; attempt < 10; ++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);
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/10)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!getGd32AppAlive()) {
|
||||
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
|
||||
DEBUG_ERR("OTA", "GD32 APP 启动确认超时,OTA 失败");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
// 全流程唯一成功总结(版本来自 probe 写入的快照)
|
||||
gd32_version_t ver = getSnapshot().version;
|
||||
DEBUG_INFO("OTA", "GD32 OTA 完成: APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
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 OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -1311,11 +1389,10 @@ ota_result_t OtaRelay::doFinishAndVerify()
|
||||
memset(m_nonceServer, 0, sizeof(m_nonceServer));
|
||||
memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime));
|
||||
|
||||
DEBUG_INFO("OTA", "=== OTA 完成,等待 GD32 APP 启动 ===");
|
||||
|
||||
// 元数据清理安静进行;等待/成功总结由 doWaitForAppAlive 统一输出
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
|
||||
// 阶段2: APP 存活轮询(单次 appPing 超时 1000ms,共 30 次)
|
||||
// 阶段2: APP 存活轮询
|
||||
return doWaitForAppAlive(1000);
|
||||
}
|
||||
|
||||
@@ -1357,7 +1434,6 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
|
||||
IRelayListener *listener = getRelayListener();
|
||||
pauseCanFeedbackTask(true);
|
||||
if (listener) listener->onRelayStarted();
|
||||
ota_result_t result = instance().forwardFirmware();
|
||||
|
||||
@@ -1365,7 +1441,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
xQueueSend(resultQueue, &result_msg, pdMS_TO_TICKS(100));
|
||||
|
||||
if (result == OTA_RESULT_OK) {
|
||||
DEBUG_INFO("OTA", "OTA 转发成功完成");
|
||||
// 成功日志已在 doWaitForAppAlive 输出,此处不再重复
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
} else if (result == OTA_RESULT_ERR_USER_ABORT) {
|
||||
DEBUG_ERR("OTA", "OTA 全局超时中止 (OTA_RESULT_ERR_USER_ABORT=%d)", result);
|
||||
@@ -1376,6 +1452,5 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
|
||||
listener = getRelayListener();
|
||||
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
pauseCanFeedbackTask(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ private:
|
||||
*/
|
||||
ota_result_t doSendFinishOta();
|
||||
/**
|
||||
* @brief 轮询等待 GD32 APP 存活(30 次重试).
|
||||
* @brief 轮询等待 GD32 APP 存活(10 次重试).
|
||||
* @param timeout_ms 单次 appPing 超时(毫秒)。
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_GD32 超时失败。
|
||||
*/
|
||||
|
||||
+54
-27
@@ -1196,8 +1196,6 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
uint32_t afterReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
bool progressMade = (afterReceived > beforeReceived);
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u progress=%d",
|
||||
seq, (unsigned int)afterReceived, progressMade ? 1 : 0);
|
||||
|
||||
// 完成判定:以实际按序写入字节数为准。
|
||||
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
@@ -1239,35 +1237,59 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
* @brief 处理单个 GD32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
|
||||
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
|
||||
*/
|
||||
/** GD32 路径硬失败:与 WiFi abortCurrentIfEpoch 对齐,复位协调器 + 本通道 RAM meta。 */
|
||||
static void ble_gd32_fail_abort(const char *why)
|
||||
{
|
||||
DEBUG_ERR(TAG, "GD32 OTA 中止: %s", why ? why : "?");
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
s_ble_gd32_meta = {};
|
||||
s_ble_gd32_meta_active = false;
|
||||
s_gd32_ooo_count = 0;
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// 与 ESP32 BLE 路径一致:活跃会话中的 seq=0 视为首包重传,禁止再次 prepare
|
||||
// (否则会 erase 分区、清 meta,毁掉已写入进度)。
|
||||
if (s_ble_gd32_meta_active && s_ble_gd32_meta.total_size == total_size) {
|
||||
if (s_ble_gd32_meta.stored_size > 0) {
|
||||
DEBUG_INFO(TAG, "GD32 会话已激活,seq=0 视为重传,跳过写入 stored=%" PRIu32,
|
||||
s_ble_gd32_meta.stored_size);
|
||||
sendWindowAck(cmd, 0, s_ble_gd32_meta.stored_size);
|
||||
return false;
|
||||
}
|
||||
DEBUG_INFO(TAG, "GD32 会话已激活且未落盘,seq=0 重传首块(不重新 prepare)");
|
||||
// fall through:stored_size==0 时重写 offset 0
|
||||
} else {
|
||||
// 与 WiFi 共用 prepareGd32Storage:validate + begin + erase + clearMeta + saveMeta
|
||||
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);
|
||||
s_ble_gd32_meta = {};
|
||||
s_ble_gd32_meta.total_size = total_size;
|
||||
s_ble_gd32_meta_active = true;
|
||||
ble_ota_boost_wifi_ps();
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
|
||||
total_size, data_len);
|
||||
s_ble_gd32_meta = {};
|
||||
s_ble_gd32_meta.total_size = total_size;
|
||||
s_ble_gd32_meta_active = true;
|
||||
ble_ota_boost_wifi_ps();
|
||||
}
|
||||
}
|
||||
|
||||
if (!s_ble_gd32_meta_active || s_ble_gd32_meta.total_size == 0) {
|
||||
@@ -1325,6 +1347,8 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
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);
|
||||
ble_gd32_fail_abort("write OOB");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1332,6 +1356,8 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
|
||||
seq, flash_offset, esp_err_to_name(err));
|
||||
ble_gd32_fail_abort(esp_err_to_name(err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)err);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1351,6 +1377,7 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据 checkpoint 失败 (seq=%u): %s",
|
||||
seq, esp_err_to_name(err));
|
||||
ble_gd32_fail_abort(esp_err_to_name(err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)err);
|
||||
return true;
|
||||
}
|
||||
@@ -1365,9 +1392,9 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
s_ble_gd32_meta_active = false;
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
ble_ota_restore_wifi_ps();
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
// 与 WiFi 共用 finishGd32StorageAfterReceive:complete + SHA
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
|
||||
+225
-172
@@ -14,6 +14,8 @@
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include "app_config.h"
|
||||
@@ -29,17 +31,9 @@
|
||||
#include "spi_master.h"
|
||||
#include "canfd_bridge.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include "monitor/task_stack_check.h"
|
||||
#include "ble_ota.h"
|
||||
|
||||
// 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)
|
||||
{
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
@@ -48,27 +42,6 @@ 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);
|
||||
@@ -203,16 +176,21 @@ 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 时多连接并发也互不干扰。
|
||||
// per-request 接收块:2KB,避免大块 malloc 失败 / 碎片
|
||||
uint8_t *recv_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
|
||||
if (!recv_buf) {
|
||||
DEBUG_ERR("WIFI", "/ota/upload malloc(%u) 失败 free=%lu largest=%lu",
|
||||
(unsigned)OTA_BLOCK_SIZE,
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
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_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d free=%lu largest=%lu",
|
||||
req->content_len,
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||
|
||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||
@@ -230,19 +208,35 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
bool is_esp32_ota = false;
|
||||
uint32_t total_size = 0;
|
||||
int json_header_len = 0;
|
||||
uint8_t pre_read_buf[256] = {0};
|
||||
// ★ 改为堆分配:pre_read_buf[256]+json_buf[256] 同时在栈上占 512B,
|
||||
// 上传 100% 后 GD32 路径调用 computeFlashSha256(栈上 mbedtls_sha256_context
|
||||
// ~108B)+ 日志格式化 608B,叠加逼近 8192 栈极限 → 栈溢出卡死。
|
||||
// 移到堆上释放栈压力。json_buf 在解析完成后立即释放。
|
||||
uint8_t *pre_read_buf = (uint8_t *)calloc(256, 1);
|
||||
if (!pre_read_buf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
int pre_read_len = 1;
|
||||
pre_read_buf[0] = (uint8_t)peek;
|
||||
|
||||
if (peek == '{') {
|
||||
char json_buf[256];
|
||||
char *json_buf = (char *)calloc(256, 1);
|
||||
if (!json_buf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
json_buf[0] = peek;
|
||||
json_header_len = recv_json_header(req, json_buf + 1, sizeof(json_buf) - 1);
|
||||
json_header_len = recv_json_header(req, json_buf + 1, 255);
|
||||
if (json_header_len > 0) {
|
||||
json_header_len++;
|
||||
json_buf[json_header_len] = '\0';
|
||||
pre_read_len = json_header_len;
|
||||
if (pre_read_len > (int)sizeof(pre_read_buf)) pre_read_len = (int)sizeof(pre_read_buf);
|
||||
if (pre_read_len > 256) pre_read_len = 256;
|
||||
memcpy(pre_read_buf, json_buf, pre_read_len);
|
||||
|
||||
cJSON *root = cJSON_Parse(json_buf);
|
||||
@@ -260,45 +254,27 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
}
|
||||
free(json_buf); // JSON 解析完成,立即释放
|
||||
}
|
||||
|
||||
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);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// ★ 若前端已通过 /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 状态,跳过擦除");
|
||||
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 {
|
||||
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;
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||
}
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
@@ -310,7 +286,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
size_t total_firmware_written = 0;
|
||||
size_t total_received = json_header_len;
|
||||
int last_percent = 0;
|
||||
size_t last_log_recv = 0;
|
||||
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 开始收流: content_len=%d json_hdr=%d",
|
||||
req->content_len, json_header_len);
|
||||
|
||||
while (total_received < req->content_len) {
|
||||
size_t to_read = req->content_len - total_received;
|
||||
@@ -318,46 +297,68 @@ 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);
|
||||
// ★ 客户端断开/接收失败:用 abortCurrentIfEpoch 复位协调器状态机
|
||||
// 若 epoch 不匹配(新请求已接管),跳过中止,避免误清新会话
|
||||
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d",
|
||||
ret, (unsigned)total_received, req->content_len);
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
||||
free(recv_buf);
|
||||
free(pre_read_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));
|
||||
// ★ 同上:写入失败也用 abortCurrentIfEpoch,避免误中止新会话
|
||||
DEBUG_ERR("WIFI", "OTA write failed: %s @%u",
|
||||
esp_err_to_name(err), (unsigned)total_firmware_written);
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
total_firmware_written += ret;
|
||||
total_received += ret;
|
||||
total_firmware_written += (size_t)ret;
|
||||
total_received += (size_t)ret;
|
||||
|
||||
if (total_size > 0) {
|
||||
int percent = (int)((total_firmware_written * 100) / total_size);
|
||||
if (percent / 10 > last_percent / 10) {
|
||||
last_percent = percent;
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%d%%)",
|
||||
(unsigned int)total_firmware_written, total_size, percent);
|
||||
}
|
||||
/* 长写 Flash:喂狗 + 进度日志,避免“浏览器 100% 而设备仍在写”时假死无日志 */
|
||||
(void)esp_task_wdt_reset();
|
||||
if (total_received - last_log_recv >= (128U * 1024U) ||
|
||||
total_received >= (size_t)req->content_len) {
|
||||
last_log_recv = total_received;
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 收流进度: %u / %d (written=%u)",
|
||||
(unsigned)total_received, req->content_len,
|
||||
(unsigned)total_firmware_written);
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)core.getReceived(), json_header_len);
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)",
|
||||
(unsigned int)core.getReceived(), json_header_len);
|
||||
DEBUG_INFO("WIFI", "堆(写完): free=%lu largest=%lu",
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
|
||||
/* ★ 先释放上传缓冲再回包:堆碎时 largest~1KB,先 free 4KB/2KB 再 send,
|
||||
* 避免 LWIP/httpd 发响应时 malloc pbuf 失败导致客户端卡在 100%。 */
|
||||
free(recv_buf);
|
||||
recv_buf = nullptr;
|
||||
free(pre_read_buf);
|
||||
pre_read_buf = nullptr;
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
(void)esp_task_wdt_reset();
|
||||
|
||||
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;
|
||||
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
|
||||
if (send_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "ESP32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
||||
esp_err_to_name(send_err),
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
} else {
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 上传响应已发送 (free=%lu)",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
}
|
||||
return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
const uint32_t gd32_fw_size = (uint32_t)req->content_len;
|
||||
@@ -367,58 +368,34 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
} else {
|
||||
gd32_valid_size = gd32_fw_size;
|
||||
}
|
||||
if (OtaCoordinator::instance().validateGd32Size(gd32_valid_size) != ESP_OK) {
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
// 与 BLE 共用 prepareGd32Storage(内含 validateGd32Size + begin + erase + meta)
|
||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(gd32_valid_size, OTA_CHANNEL_WIFI);
|
||||
if (prep_err == ESP_ERR_INVALID_SIZE) {
|
||||
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");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
} else if (prep_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %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);
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -428,13 +405,14 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
int received = 0;
|
||||
|
||||
// expected_fw_size_raw 仅用于后续日志输出;meta 已由 prepareGd32Storage
|
||||
// (非 prepared 路径)或 /ota/prepare(prepared 路径)写入,此处不再重复保存。
|
||||
// expected_fw_size_raw 仅用于后续日志;meta 已由 prepareGd32Storage 写入
|
||||
uint32_t expected_fw_size_raw = gd32_valid_size;
|
||||
uint32_t gd32_offset = 0;
|
||||
|
||||
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
||||
|
||||
size_t last_log_recv = (size_t)received;
|
||||
|
||||
if (json_header_len == 0 && pre_read_len > 0) {
|
||||
int first_chunk_len = pre_read_len;
|
||||
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
||||
@@ -448,6 +426,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received = first_chunk_len;
|
||||
@@ -470,6 +449,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -481,11 +461,20 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
free(recv_buf);
|
||||
free(pre_read_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
received += ret;
|
||||
gd32_offset += (uint32_t)ret;
|
||||
|
||||
(void)esp_task_wdt_reset();
|
||||
if ((size_t)received - last_log_recv >= (64U * 1024U) ||
|
||||
(size_t)received >= (size_t)req->content_len) {
|
||||
last_log_recv = (size_t)received;
|
||||
DEBUG_INFO("WIFI", "GD32 OTA 收流进度: %d / %d (written=%u)",
|
||||
received, req->content_len, (unsigned)gd32_offset);
|
||||
}
|
||||
}
|
||||
|
||||
OtaCoordinator::instance().updateGd32Meta(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
|
||||
@@ -495,23 +484,42 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
ota_meta_t final_meta;
|
||||
OtaCoordinator::instance().loadGd32Meta(&final_meta);
|
||||
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(true);
|
||||
WifiOta::instance().stateUnlock();
|
||||
DEBUG_INFO("WIFI", "GD32 firmware fully stored, pre-computing SHA-256...");
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
/* 先释放上传缓冲再算 SHA,避免与 2KB recv 并存 */
|
||||
free(recv_buf);
|
||||
recv_buf = nullptr;
|
||||
free(pre_read_buf);
|
||||
pre_read_buf = nullptr;
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
(void)esp_task_wdt_reset();
|
||||
|
||||
DEBUG_INFO("WIFI", "GD32 firmware fully stored, pre-computing SHA-256 (free=%lu)...",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
// 与 BLE 共用 finishGd32StorageAfterReceive:complete + SHA
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
} else {
|
||||
free(recv_buf);
|
||||
recv_buf = nullptr;
|
||||
free(pre_read_buf);
|
||||
pre_read_buf = nullptr;
|
||||
}
|
||||
|
||||
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\", "
|
||||
"\"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;
|
||||
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
|
||||
if (send_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
||||
esp_err_to_name(send_err),
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
} else {
|
||||
DEBUG_INFO("WIFI", "GD32 OTA 上传响应已发送 (free=%lu)",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
}
|
||||
return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
@@ -908,12 +916,6 @@ static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_
|
||||
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 8192;
|
||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 24;
|
||||
|
||||
struct TaskStackMetric {
|
||||
const char* name;
|
||||
TaskHandle_t handle;
|
||||
size_t stack_size_bytes;
|
||||
};
|
||||
|
||||
struct CpuTaskSample {
|
||||
UBaseType_t task_number;
|
||||
configRUN_TIME_COUNTER_TYPE runtime;
|
||||
@@ -968,21 +970,25 @@ static void reset_cpu_usage_baseline_locked(const TaskStatus_t* tasks, size_t ta
|
||||
s_cpu_has_last = true;
|
||||
}
|
||||
|
||||
static CpuUsageSnapshot sample_cpu_usage()
|
||||
// ★ 改为输出参数模式:CpuUsageSnapshot 608B + CpuTaskSample[24] 192B = 800B
|
||||
// 之前全在栈上,system_status_handler 又拷一份 608B → 叠加 1.4KB,
|
||||
// 加上 OtaSnapshot/TaskStackMetric/wifi_ap_record_t 约 2.3KB 栈消耗。
|
||||
// HTTP 主线程上传 100% 后调用此 handler 时极易栈溢出。改堆分配。
|
||||
static void sample_cpu_usage(CpuUsageSnapshot& result)
|
||||
{
|
||||
CpuUsageSnapshot result = {};
|
||||
result = {};
|
||||
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
|
||||
result.available = true;
|
||||
|
||||
UBaseType_t task_count = uxTaskGetNumberOfTasks();
|
||||
if (task_count == 0) {
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
task_count = std::min<UBaseType_t>(task_count + 2U, CPU_TASK_SNAPSHOT_MAX);
|
||||
|
||||
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
|
||||
if (!tasks) {
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
|
||||
@@ -990,10 +996,16 @@ static CpuUsageSnapshot sample_cpu_usage()
|
||||
int64_t now_us = esp_timer_get_time();
|
||||
if (captured_count == 0 || captured_count > task_count) {
|
||||
free(tasks);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ previous_samples[24] = 192B,改堆分配减少栈压力
|
||||
CpuTaskSample* previous_samples = (CpuTaskSample*)calloc(CPU_TASK_SNAPSHOT_MAX, sizeof(CpuTaskSample));
|
||||
if (!previous_samples) {
|
||||
free(tasks);
|
||||
return;
|
||||
}
|
||||
|
||||
CpuTaskSample previous_samples[CPU_TASK_SNAPSHOT_MAX] = {};
|
||||
size_t previous_count = 0;
|
||||
configRUN_TIME_COUNTER_TYPE previous_total = 0;
|
||||
int64_t previous_time_us = 0;
|
||||
@@ -1047,11 +1059,11 @@ static CpuUsageSnapshot sample_cpu_usage()
|
||||
result.usage_pct = std::max(0.0, 100.0 - result.idle_pct);
|
||||
}
|
||||
|
||||
free(previous_samples);
|
||||
free(tasks);
|
||||
#else
|
||||
result.available = false;
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
|
||||
@@ -1104,7 +1116,8 @@ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUs
|
||||
task.usage_pct);
|
||||
}
|
||||
|
||||
return append_jsonf(buf, cap, offset, "]},");
|
||||
// 不带尾随逗号;由后续拼接块自行添加前导逗号,避免双重逗号
|
||||
return append_jsonf(buf, cap, offset, "]}");
|
||||
}
|
||||
|
||||
esp_err_t system_status_handler(httpd_req_t *req)
|
||||
@@ -1139,20 +1152,27 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
size_t internal_largest_free_block = heap_caps_get_largest_free_block(INTERNAL_HEAP_CAPS);
|
||||
size_t internal_heap_total = heap_caps_get_total_size(INTERNAL_HEAP_CAPS);
|
||||
size_t default_heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
|
||||
TaskStackMetric task_metrics[] = {
|
||||
{ "http_server", xTaskGetCurrentTaskHandle(), HTTP_SERVER_STACK_SIZE },
|
||||
{ "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 },
|
||||
{ "ota_relay", OtaRelay::getTaskHandle(), 6144 },
|
||||
{ "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 },
|
||||
{ "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 },
|
||||
{ "ble_ota", BleOta::instance().getTaskHandle(), 4096 },
|
||||
{ "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 }
|
||||
};
|
||||
CpuUsageSnapshot cpu = sample_cpu_usage();
|
||||
// 任务栈指标复用 health_monitor 共享的任务表(collectTaskStackMetrics),
|
||||
// 避免与巡检逻辑两地维护;http_server 在本 handler 上下文中,句柄即当前任务。
|
||||
TaskStackMetric task_metrics[8];
|
||||
size_t task_metrics_count = collectTaskStackMetrics(task_metrics, 8);
|
||||
// http_server 行的句柄在 httpd 线程中取当前任务更准确
|
||||
for (size_t i = 0; i < task_metrics_count; ++i) {
|
||||
if (strcmp(task_metrics[i].name, "http_server") == 0) {
|
||||
task_metrics[i].handle = xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
}
|
||||
CpuUsageSnapshot* cpu = (CpuUsageSnapshot*)calloc(1, sizeof(CpuUsageSnapshot));
|
||||
if (!cpu) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
sample_cpu_usage(*cpu);
|
||||
|
||||
char* resp = (char*)calloc(1, SYSTEM_STATUS_JSON_CAP);
|
||||
if (!resp) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
|
||||
free(cpu);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -1197,9 +1217,41 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
snap.relay.version.boot_major, snap.relay.version.boot_minor,
|
||||
snap.relay.version.boot_patch, snap.relay.version.boot_build);
|
||||
|
||||
len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, cpu);
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, "\"task_stack\": {");
|
||||
for (size_t i = 0; i < sizeof(task_metrics) / sizeof(task_metrics[0]); ++i) {
|
||||
len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, *cpu);
|
||||
|
||||
/* SPI Link v2 统计(SPEC §12.1) */
|
||||
{
|
||||
SpiMaster::SpiLinkStats ls = SpiMaster::instance().linkStats();
|
||||
const char *role_str = "unknown";
|
||||
switch (SpiMaster::instance().gd32Role()) {
|
||||
case 1: role_str = "app"; break;
|
||||
case 2: role_str = "boot"; break;
|
||||
default: break;
|
||||
}
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
|
||||
",\"spi_link\":{\"version\":2,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\","
|
||||
"\"v2_active\":%s,\"remote_credit\":%u,"
|
||||
"\"crc_errors\":%lu,\"duplicate_requests\":%lu,\"seq_errors\":%lu,"
|
||||
"\"retries\":%lu,\"ready_timeouts\":%lu,\"rx_overruns\":%lu,"
|
||||
"\"resyncs\":%lu,\"max_resp_latency_us\":%lu}",
|
||||
role_str,
|
||||
(unsigned long)SpiMaster::instance().sessionId(),
|
||||
SpiMaster::instance().v2Active() ? "true" : "false",
|
||||
(unsigned)SpiMaster::instance().remoteCredit(),
|
||||
(unsigned long)ls.crc_errors,
|
||||
(unsigned long)ls.duplicate_requests,
|
||||
(unsigned long)ls.seq_errors,
|
||||
(unsigned long)ls.retries,
|
||||
(unsigned long)ls.ready_timeouts,
|
||||
(unsigned long)ls.rx_overruns,
|
||||
(unsigned long)ls.resyncs,
|
||||
(unsigned long)ls.max_resp_latency_us);
|
||||
}
|
||||
|
||||
// 前导逗号与前一块分隔;之前因 append_cpu_status_json 尾随逗号叠加
|
||||
// 形成 "],," 导致前端 JSON.parse 报 "Expected double-quoted property name"
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, ",\"task_stack\": {");
|
||||
for (size_t i = 0; i < task_metrics_count; ++i) {
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
|
||||
"%s\"%s\":{\"stack_size_bytes\":%lu,\"high_watermark_bytes\":%lu,\"peak_used_bytes\":%lu}",
|
||||
i == 0 ? "" : ",",
|
||||
@@ -1213,6 +1265,7 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
free(resp);
|
||||
free(cpu);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -418,9 +418,13 @@ esp_err_t WifiOta::httpServerStart()
|
||||
config.server_port = HTTP_SERVER_PORT;
|
||||
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
||||
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
||||
config.max_open_sockets = 4; // 配合 LWIP_MAX_SOCKETS=12: 4(httpd) + 1(UDP) + ~3(LWIP内部) = 8, 余量 4; 空闲时前端 status/logs/上传可能并发,3 个易被 LRU 踢掉导致连接中断
|
||||
config.lru_purge_enable = true; // socket 满时自动清理最久未用的连接
|
||||
config.recv_wait_timeout = 30; // GD32 上传前会擦除 Flash,5 秒默认值过紧
|
||||
/* LWIP_MAX_SOCKETS=12;HTTP 内部约 3 + UDP 日志 1 → 业务 socket 不宜过大。
|
||||
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。 */
|
||||
config.max_open_sockets = 4;
|
||||
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
|
||||
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
|
||||
config.lru_purge_enable = false;
|
||||
config.recv_wait_timeout = 60; // 1MB OTA 写 Flash 可能超过 30s 空闲读
|
||||
config.send_wait_timeout = 30;
|
||||
|
||||
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
||||
@@ -432,13 +436,13 @@ esp_err_t WifiOta::httpServerStart()
|
||||
return err;
|
||||
}
|
||||
|
||||
// 从 NVS 恢复固件存储状态(重启后保留)
|
||||
// GD32 固件就绪真相源为 OtaCoordinator / NVS meta(/system/status),
|
||||
// 不再在 WifiOta 上维护影子 storage_done 标志。
|
||||
{
|
||||
ota_meta_t boot_meta;
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&boot_meta) == ESP_OK &&
|
||||
boot_meta.total_size > 0 && boot_meta.stored_size >= boot_meta.total_size) {
|
||||
WifiOta::instance().setGd32StorageDone(true);
|
||||
DEBUG_INFO("WIFI", "从 NVS 恢复: 固件已就绪 (%u bytes)", boot_meta.total_size);
|
||||
DEBUG_INFO("WIFI", "从 NVS 恢复: GD32 固件已就绪 (%u bytes)", boot_meta.total_size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,8 +69,6 @@ public:
|
||||
bool getStaRetryExhausted() const { return m_staRetryExhausted.load(); }
|
||||
/** @brief 设置 STA 重试耗尽标志。 */
|
||||
void setStaRetryExhausted(bool v) { m_staRetryExhausted.store(v); }
|
||||
/** @brief 设置 GD32 固件存储完成标志。 */
|
||||
void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); }
|
||||
// ── 状态互斥锁(保护复合读-改-写操作)──
|
||||
/** @brief 获取状态互斥锁句柄。 */
|
||||
SemaphoreHandle_t getStateMutex() const { return m_stateMutex; }
|
||||
@@ -107,7 +105,6 @@ private:
|
||||
std::atomic<bool> m_staConnected{false};
|
||||
std::atomic<int> m_staRetryCount{0};
|
||||
std::atomic<bool> m_staRetryExhausted{false};
|
||||
std::atomic<bool> m_gd32StorageDone{false};
|
||||
SemaphoreHandle_t m_stateMutex = NULL;
|
||||
TaskHandle_t m_taskHandle = nullptr;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_partition.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "gd32_flash_store.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
// ============================================================
|
||||
@@ -73,6 +75,49 @@ const esp_partition_t* Gd32FlashStore::getPartition()
|
||||
// Flash 分区读写
|
||||
// ============================================================
|
||||
|
||||
void Gd32FlashStore::beginWriteSession()
|
||||
{
|
||||
m_erasedUpTo = 0;
|
||||
}
|
||||
|
||||
/** @brief 确保 [offset, offset+len) 覆盖的扇区已擦除(从 m_erasedUpTo 顺序推进)。 */
|
||||
esp_err_t Gd32FlashStore::ensureErasedForWrite(uint32_t offset, size_t len)
|
||||
{
|
||||
if (len == 0) return ESP_OK;
|
||||
const esp_partition_t *part = getPartition();
|
||||
if (!part) return ESP_ERR_NOT_FOUND;
|
||||
|
||||
const uint32_t end = offset + (uint32_t)len;
|
||||
// 目标区间已在水位内:无需再擦(顺序 OTA 不回写旧扇区)
|
||||
if (end <= m_erasedUpTo) return ESP_OK;
|
||||
|
||||
// 水位始终扇区对齐;只从 m_erasedUpTo 向高地址推进,禁止重擦已写扇区。
|
||||
// 若写入跳空,一并擦掉空隙,保持 [0, m_erasedUpTo) 连续已擦。
|
||||
uint32_t erase_to = (end + (kFlashSectorSize - 1U)) & ~(kFlashSectorSize - 1U);
|
||||
if (erase_to > part->size) {
|
||||
erase_to = part->size;
|
||||
}
|
||||
uint32_t erase_from = m_erasedUpTo;
|
||||
if (erase_from >= erase_to) return ESP_OK;
|
||||
|
||||
for (uint32_t addr = erase_from; addr < erase_to; ) {
|
||||
uint32_t chunk = kFlashSectorSize;
|
||||
if (addr + chunk > erase_to) {
|
||||
chunk = erase_to - addr;
|
||||
}
|
||||
esp_err_t err = esp_partition_erase_range(part, addr, chunk);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "ensureErased: erase @%" PRIu32 " len=%" PRIu32 " failed: %s",
|
||||
addr, chunk, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
addr += chunk;
|
||||
m_erasedUpTo = addr;
|
||||
(void)esp_task_wdt_reset();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 向 gd32_ota 分区写入数据块,自动处理非 4 字节对齐的读写。 */
|
||||
esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
@@ -91,6 +136,12 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
// 写入前按需擦除目标扇区(替代 prepare 阶段整段预擦)
|
||||
esp_err_t erase_err = ensureErasedForWrite(aligned_offset, aligned_len);
|
||||
if (erase_err != ESP_OK) {
|
||||
return erase_err;
|
||||
}
|
||||
|
||||
// 快速路径:offset 和 len 都对齐
|
||||
if (head_pad == 0 && len % 4 == 0) {
|
||||
return esp_partition_write(part, offset, data, len);
|
||||
@@ -155,7 +206,24 @@ esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
DEBUG_ERR("OTA", "erasePartition: size=%" PRIu32 " > partition=%" PRIu32, size, part->size);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return esp_partition_erase_range(part, 0, size);
|
||||
// 按扇区擦除并喂狗,避免大块 erase 长时间占住调用任务
|
||||
uint32_t done = 0;
|
||||
while (done < size) {
|
||||
uint32_t chunk = kFlashSectorSize;
|
||||
if (done + chunk > size) {
|
||||
chunk = size - done;
|
||||
}
|
||||
esp_err_t err = esp_partition_erase_range(part, done, chunk);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
done += chunk;
|
||||
if (done > m_erasedUpTo) {
|
||||
m_erasedUpTo = done;
|
||||
}
|
||||
(void)esp_task_wdt_reset();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -279,7 +347,7 @@ esp_err_t Gd32FlashStore::metaUpdate(uint32_t stored_size, uint32_t last_seq)
|
||||
/** @brief OTA 完成或中止时清除元数据(全零写入)。 */
|
||||
esp_err_t Gd32FlashStore::metaClear()
|
||||
{
|
||||
DEBUG_INFO("OTA", "清除 OTA 元数据");
|
||||
DEBUG_DBG("OTA", "清除 OTA 元数据");
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaClear 加锁超时");
|
||||
@@ -307,43 +375,49 @@ esp_err_t Gd32FlashStore::computeFlashSha256(uint8_t out_sha256[32])
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
if (mbedtls_sha256_starts(&ctx, 0) != 0) {
|
||||
mbedtls_sha256_free(&ctx);
|
||||
/* static context:OTA 串行;勿放 httpd 栈(上传 100% 后 SHA 易栈溢出) */
|
||||
static mbedtls_sha256_context s_sha_ctx;
|
||||
mbedtls_sha256_init(&s_sha_ctx);
|
||||
if (mbedtls_sha256_starts(&s_sha_ctx, 0) != 0) {
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
static constexpr size_t kChunkSize = 1024;
|
||||
uint8_t *buf = (uint8_t *)malloc(kChunkSize);
|
||||
if (!buf) {
|
||||
mbedtls_sha256_free(&ctx);
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
uint32_t remaining = meta.total_size;
|
||||
uint32_t offset = 0;
|
||||
uint32_t wdt_chunk = 0;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = (remaining > kChunkSize) ? kChunkSize : remaining;
|
||||
err = flashReadChunk(offset, buf, to_read);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "computeSha256: Flash 读取失败 offset=%" PRIu32, offset);
|
||||
free(buf);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
return err;
|
||||
}
|
||||
if (mbedtls_sha256_update(&ctx, buf, to_read) != 0) {
|
||||
if (mbedtls_sha256_update(&s_sha_ctx, buf, to_read) != 0) {
|
||||
free(buf);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
offset += to_read;
|
||||
remaining -= to_read;
|
||||
if ((++wdt_chunk & 0x0FU) == 0U) {
|
||||
(void)esp_task_wdt_reset();
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
int ret = mbedtls_sha256_finish(&ctx, out_sha256);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
int ret = mbedtls_sha256_finish(&s_sha_ctx, out_sha256);
|
||||
mbedtls_sha256_free(&s_sha_ctx);
|
||||
return (ret == 0) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,12 @@ public:
|
||||
*/
|
||||
esp_err_t erasePartition(uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief 新一轮 GD32 固件写入会话开始时调用:复位按需擦除水位。
|
||||
* prepareGd32Storage 在开始收流前调用,避免整段预擦阻塞 HTTP/BLE 收包。
|
||||
*/
|
||||
void beginWriteSession();
|
||||
|
||||
// ── NVS 元数据持久化 ──
|
||||
|
||||
/**
|
||||
@@ -147,6 +153,13 @@ private:
|
||||
|
||||
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
||||
|
||||
// ── 按需擦除水位:已保证 [0, m_erasedUpTo) 为擦除态(扇区对齐)──
|
||||
// 收流时 flashWriteChunk 按写入范围推进擦除,替代上传前整段预擦。
|
||||
// 这样 HTTP 主线程可边收边擦写,浏览器进度不会在“100% 发完”后空等整段擦除。
|
||||
static constexpr uint32_t kFlashSectorSize = 4096U;
|
||||
uint32_t m_erasedUpTo = 0;
|
||||
esp_err_t ensureErasedForWrite(uint32_t offset, size_t len);
|
||||
|
||||
// ── NVS 互斥锁(保护元数据读-改-写原子性)──
|
||||
// lockNvs 返回是否成功 take;unlockNvs(handle) 据此决定是否 give,
|
||||
// 参考 OtaCoordinator::lock/unlock 模式(避免共享 bool 被并发覆盖导致死锁)。
|
||||
|
||||
@@ -50,7 +50,6 @@ void Protocol::init()
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
DEBUG_INFO("PROT", "协议层就绪");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+727
-299
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "spi_link_v2.h"
|
||||
|
||||
// SPI 配置常量
|
||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||
@@ -76,7 +77,8 @@ public:
|
||||
// SPI OTA 协议
|
||||
// ============================================================
|
||||
|
||||
/** @brief 按 GD32 SPI OTA 最小协议执行一次请求 + 响应读取。 */
|
||||
/** @brief 按 GD32 SPI OTA 协议执行一次请求 + 响应读取(延迟一拍两步法)。
|
||||
* 内部等价于 otaSendAndPoll:首笔 CS 发请求(忽略旧响应),次笔 CS 收真实响应。 */
|
||||
esp_err_t otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
@@ -108,13 +110,49 @@ public:
|
||||
/** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
|
||||
uint8_t remoteCredit() const { return m_remoteCredit; }
|
||||
|
||||
// ============================================================
|
||||
// SPI Link v2 链路层(阶段1:帧格式 + 状态成员就位)
|
||||
// ============================================================
|
||||
|
||||
/** @brief v2 链路状态统计(SPEC/spi_link_v2.md §12)。 */
|
||||
struct SpiLinkStats {
|
||||
uint32_t crc_errors; ///< CRC 校验失败计数
|
||||
uint32_t duplicate_requests; ///< 重复请求计数(序号回退)
|
||||
uint32_t seq_errors; ///< 序号跳变计数
|
||||
uint32_t retries; ///< 重试总数
|
||||
uint32_t ready_timeouts; ///< DATA_READY 超时计数
|
||||
uint32_t rx_overruns; ///< RX overrun 计数
|
||||
uint32_t resyncs; ///< HELLO 重同步次数
|
||||
uint32_t max_resp_latency_us; ///< 最大响应延迟(微秒)
|
||||
};
|
||||
|
||||
/** @brief 当前会话 ID(0 = 未建立 session)。 */
|
||||
uint32_t sessionId() const { return m_sessionId; }
|
||||
|
||||
/** @brief v2 session 是否已激活。 */
|
||||
bool v2Active() const { return m_v2Active; }
|
||||
|
||||
/** @brief 最近一次 HELLO 得到的 GD32 角色(0=unknown, 1=app, 2=boot)。 */
|
||||
uint8_t gd32Role() const { return m_gd32Role; }
|
||||
|
||||
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
|
||||
SpiLinkStats linkStats();
|
||||
|
||||
/** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。
|
||||
* 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。
|
||||
* 成功后 m_v2Active=true,序号归零。
|
||||
* @param timeout_ms 总超时(毫秒),默认 3000。
|
||||
* @param role_hint 期望的对端角色(0=any, 1=app, 2=boot)。
|
||||
* @return ESP_OK 成功;ESP_ERR_TIMEOUT 超时;ESP_ERR_INVALID_RESPONSE 响应异常。 */
|
||||
esp_err_t hello(uint32_t timeout_ms = 3000U, uint8_t role_hint = 0U);
|
||||
|
||||
/** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */
|
||||
esp_err_t appRequestBootloader();
|
||||
|
||||
/** @brief 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪。
|
||||
* Phase1 轮询等待 LOW (APP 复位前拉低 / Bootloader 极早拉低);
|
||||
* Phase2 轮询等待 HIGH 并确认稳定 10ms (Bootloader init 完成释放)。
|
||||
* 超时返回 ESP_ERR_TIMEOUT,调用方应退化为固定延时。 */
|
||||
/** @brief 通过 HELLO 轮询等待 Bootloader 就绪(v2 方式,不再依赖 PB6 序列)。
|
||||
* App→Boot 切换后,ESP32 每 20ms 发送 HELLO(role_hint=boot),
|
||||
* 等待 HELLO_ACK 且 role=boot,最长等待 timeout_ms。
|
||||
* 超时返回 ESP_ERR_TIMEOUT。 */
|
||||
esp_err_t waitForBootloaderReady(uint32_t timeout_ms);
|
||||
|
||||
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
||||
@@ -226,6 +264,16 @@ private:
|
||||
volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口)
|
||||
volatile uint8_t m_remoteCredit = 8; ///< 对端上报的剩余信用
|
||||
|
||||
// ── SPI Link v2 会话/序号状态(阶段1 就位,阶段2 启用校验)──
|
||||
volatile uint32_t m_sessionId = 0; ///< 会话 ID(0 = 未建立,HELLO 时由 ESP32 生成)
|
||||
volatile uint8_t m_txSeq = 0; ///< 本端发送序号(每发一帧递增,重发不变)
|
||||
volatile uint8_t m_lastSentSeq = 0; ///< 上次发送的序号(用于响应归属匹配)
|
||||
volatile uint8_t m_rxAckedSeq = 0; ///< 已确认的对端最新序号
|
||||
volatile uint8_t m_expectedRxSeq = 0; ///< 期望收到的对端序号
|
||||
volatile uint8_t m_gd32Role = 0; ///< GD32 角色(0=unknown, 1=app, 2=boot)
|
||||
volatile bool m_v2Active = false; ///< v2 session 是否激活
|
||||
SpiLinkStats m_stats = {}; ///< 链路统计
|
||||
|
||||
// RAII 锁守卫
|
||||
class LockGuard {
|
||||
public:
|
||||
@@ -284,7 +332,8 @@ private:
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms,
|
||||
uint8_t expected_type = 0xFFU);
|
||||
/** @brief 无锁版本的 otaTransfer(调用者须已持有锁)。
|
||||
* @param expect_stale true 表示调用方预期读到过期/空响应(如 otaSendAndPoll 步骤1),
|
||||
* 找不到帧时降级为 DBG 日志,不打 WARN。 */
|
||||
@@ -292,6 +341,14 @@ private:
|
||||
uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms,
|
||||
bool expect_stale = false);
|
||||
|
||||
/** @brief 带重试退避的事务(SPEC §9.2)。
|
||||
* 同序号重试最多 3 次,退避 10/30/90ms;
|
||||
* 3 次失败后 HELLO 重同步,再试 1 次。
|
||||
* 调用者须已持有锁。 */
|
||||
esp_err_t transactWithRetry(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
// OTA 帧工具(静态,无状态)
|
||||
static uint32_t otaCrc32(const uint8_t *data, size_t len);
|
||||
static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
|
||||
Reference in New Issue
Block a user