feat(protocol): 更新SPI Link协议至v2版本并优化CAN通信
- 将SPI OTA帧格式升级为SPI Link v2,采用新的帧格式: | SOF0 0xA5 | SOF1 0x5A | VER 0x02 | FLAGS | SESSION 4B LE | TX_SEQ | ACK_SEQ | CREDIT | TYPE | LEN 2B LE | RSV 2B | PAYLOAD | CRC32 4B LE | - 新增V2_OVERHEAD=20字节开销,APP最大payload为236字节,BOOT为108字节 - 修改帧类型定义:ENTER_BOOT从0xB5合并至0xB0,新增CAN业务类型0xC0~C3 - 更新CAN协议文档,支持SPI Link v2-only模式,调整最大业务payload计算 - 扩展0xC1帧功能,支持双向通信和1Hz主动推送ID快照 - 添加0xC3完整ID快照帧类型,包含seq/rx_count等完整字段 - 更新协议文档中的错误码定义和反馈payload格式 refactor(main): 重构主程序入口并添加CAN反馈任务 - 在main.cpp中添加CAN反馈任务,更新启动流程注释 - 创建createCriticalTask辅助函数,统一关键任务创建逻辑 - 调整SPI初始化顺序,确保CAN反馈任务依赖正确 - 优化错误处理机制,关键任务失败时立即重启 - 将GD32版本探测移至ota_relay任务处理 - 更新版本号至0.3.48,调整编译时间和Git哈希 config: 更新SPI和BLE配置参数 - 添加SPI主机相关配置参数,包括CS GPIO、READY GPIO等引脚定义 - 配置SPI缓冲区大小:APP为256字节,BOOT为128字节 - 更新BLE滑动窗口参数,ACK窗口设为32,数据队列深度为72 - 调整SPI工作队列大小至5120字节,增加任务堆栈监控说明
This commit is contained in:
+52
-144
@@ -1,10 +1,9 @@
|
||||
/*
|
||||
* ESP32-C3FH4 OTA 中继 - 程序入口
|
||||
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
|
||||
* FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT / CAN 反馈
|
||||
*
|
||||
* 使用各模块的单例类进行初始化和任务创建。
|
||||
* 使用各模块单例初始化并创建任务;入口只做装配,不实现协议。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_chip_info.h"
|
||||
@@ -27,194 +26,103 @@
|
||||
#include "monitor/can_feedback_task.h"
|
||||
|
||||
/**
|
||||
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
|
||||
* @brief 创建关键任务;失败则打 ERR 并 ESP_ERROR_CHECK 触发重启。
|
||||
*/
|
||||
static void createCriticalTask(TaskFunction_t entry, const char *name,
|
||||
uint32_t stack, UBaseType_t prio)
|
||||
{
|
||||
BaseType_t xRet = xTaskCreatePinnedToCore(
|
||||
entry, name, stack, NULL, prio, NULL, 0);
|
||||
if (xRet != pdPASS) {
|
||||
DEBUG_ERR("MAIN", "创建关键任务 %s 失败", name);
|
||||
ESP_ERROR_CHECK(ESP_FAIL);
|
||||
}
|
||||
DEBUG_INFO("MAIN", "任务已创建: %s (prio=%u stack=%lu)",
|
||||
name, (unsigned)prio, (unsigned long)stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 程序主入口:初始化组件并创建 FreeRTOS 任务。
|
||||
*/
|
||||
extern "C" void app_main(void)
|
||||
{
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
|
||||
// ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore
|
||||
Esp32OtaCore::initMutex();
|
||||
|
||||
// 初始化网络接口 + 事件循环(必须最早)
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
|
||||
DebugLog::instance().init();
|
||||
|
||||
// WiFi 提前初始化(调度器启动前)
|
||||
bool wifi_sta_ok = WifiOta::instance().earlyInit();
|
||||
/* earlyInit:true=已启动 STA 连接流程;false=将走 AP(或 CONFIG 直选 AP) */
|
||||
const bool wifi_early_started_sta = WifiOta::instance().earlyInit();
|
||||
|
||||
HealthMonitor::instance().init();
|
||||
DEBUG_INFO("MAIN", "健康监控状态已初始化");
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v1.0 启动");
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay %s 启动", FW_VERSION);
|
||||
DEBUG_INFO("MAIN", "Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip_info.revision);
|
||||
DEBUG_INFO("MAIN", "Free heap: %lu bytes", esp_get_free_heap_size());
|
||||
DEBUG_INFO("MAIN", "Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
|
||||
DEBUG_INFO("MAIN", "CPU Freq: %d MHz", CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ);
|
||||
DEBUG_INFO("MAIN", "WiFi: %s", wifi_sta_ok ? "STA 连接中" : "将使用 AP 模式");
|
||||
DEBUG_INFO("MAIN", "WiFi early: %s(最终 STA/AP 由 wifi_ota 任务确认)",
|
||||
wifi_early_started_sta ? "STA 连接中" : "非 STA 优先/将切 AP");
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
|
||||
// 健康监控任务(温度 + WiFi RSSI)
|
||||
BaseType_t xRet;
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
HealthMonitor::taskEntry,
|
||||
"health_mon",
|
||||
TASK_STACK_HEALTH_MON,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 4,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:负责温度与 WiFi RSSI 上报,缺失会让运维陷入盲区,
|
||||
// 无法及时发现硬件异常,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 health_monitor_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "健康监控任务已创建");
|
||||
}
|
||||
/* 健康监控:温度/RSSI;缺失则运维盲区 → 关键 */
|
||||
createCriticalTask(HealthMonitor::taskEntry, "health_mon",
|
||||
TASK_STACK_HEALTH_MON, configMAX_PRIORITIES - 4);
|
||||
|
||||
/* CPU 占用 1Hz 采样(非致命;失败时 /system/status 的 cpu.valid 保持 false) */
|
||||
/* CPU 1Hz 采样:非致命 */
|
||||
CpuStats::instance().start();
|
||||
|
||||
Protocol::instance().init();
|
||||
DEBUG_INFO("MAIN", "协议层初始化完成");
|
||||
|
||||
// OTA 全局协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
|
||||
OtaCoordinator::instance().init();
|
||||
// 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener)
|
||||
OtaCoordinator::instance().setBleCoexListener(&WifiOta::instance());
|
||||
DEBUG_INFO("MAIN", "OTA 全局协调器已初始化");
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().init();
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret);
|
||||
} else {
|
||||
/* SPI 是中继/CAN 反馈根基:失败则无法完成核心职责,按关键失败处理 */
|
||||
{
|
||||
esp_err_t ret = SpiMaster::instance().init();
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %s", esp_err_to_name(ret));
|
||||
ESP_ERROR_CHECK(ret);
|
||||
}
|
||||
DEBUG_INFO("MAIN", "SPI 总线初始化完成");
|
||||
}
|
||||
|
||||
// OTA 中继任务(中高优先级)
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
OtaRelay::taskEntry,
|
||||
"ota_relay",
|
||||
TASK_STACK_OTA_RELAY,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 2,
|
||||
NULL,
|
||||
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 中继任务已创建");
|
||||
}
|
||||
createCriticalTask(OtaRelay::taskEntry, "ota_relay",
|
||||
TASK_STACK_OTA_RELAY, configMAX_PRIORITIES - 2);
|
||||
|
||||
// WiFi OTA 服务器任务
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
WifiOta::taskEntry,
|
||||
"wifi_ota",
|
||||
TASK_STACK_WIFI_OTA,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 3,
|
||||
NULL,
|
||||
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 任务已创建");
|
||||
}
|
||||
createCriticalTask(WifiOta::taskEntry, "wifi_ota",
|
||||
TASK_STACK_WIFI_OTA, configMAX_PRIORITIES - 3);
|
||||
|
||||
// BLE OTA 任务
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
BleOta::taskEntry,
|
||||
"ble_ota",
|
||||
TASK_STACK_BLE_OTA,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 3,
|
||||
NULL,
|
||||
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 任务已创建");
|
||||
}
|
||||
createCriticalTask(BleOta::taskEntry, "ble_ota",
|
||||
TASK_STACK_BLE_OTA, configMAX_PRIORITIES - 3);
|
||||
|
||||
// CAN 1Hz 反馈轮询任务(依赖 SPI 已初始化)
|
||||
if (startCanFeedbackTask()) {
|
||||
DEBUG_INFO("MAIN", "CAN 反馈轮询任务已创建");
|
||||
} else {
|
||||
DEBUG_WARN("MAIN", "CAN 反馈轮询任务创建失败(非致命,OTA 不受影响)");
|
||||
}
|
||||
|
||||
// OTA 回滚保护:标记当前固件有效
|
||||
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
|
||||
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
|
||||
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
|
||||
if (ota_valid_err == ESP_OK) {
|
||||
DEBUG_INFO("MAIN", "OTA 回滚保护: 当前固件已标记有效");
|
||||
} else {
|
||||
DEBUG_WARN("MAIN", "OTA 回滚保护: 标记失败 (%s),可能非 OTA 启动", esp_err_to_name(ota_valid_err));
|
||||
/* 任务就绪后尽快标记 app 有效,避免后续探测拖太久导致回滚窗口拉长 */
|
||||
{
|
||||
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
|
||||
if (ota_valid_err == ESP_OK) {
|
||||
DEBUG_INFO("MAIN", "OTA 回滚保护: 当前固件已标记有效");
|
||||
} else {
|
||||
DEBUG_WARN("MAIN", "OTA 回滚保护: 标记失败 (%s),可能非 OTA 启动",
|
||||
esp_err_to_name(ota_valid_err));
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
||||
|
||||
// 启动最后步骤:读取 GD32 APP 版本号
|
||||
// 先 hello() 建立 SPI v2 会话,再 probeGd32AppOnce 读取版本
|
||||
// GD32 APP 完整初始化需 5-6 秒,使用 20 次重试 × 300ms 窗口
|
||||
{
|
||||
DEBUG_INFO("MAIN", "启动 GD32 版本探测...");
|
||||
gd32_version_t gd32_ver = {};
|
||||
bool session_established = false;
|
||||
bool gd32_ok = false;
|
||||
|
||||
for (uint32_t attempt = 0; attempt < 20; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
|
||||
if (!session_established) {
|
||||
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
|
||||
if (hr == ESP_OK) {
|
||||
session_established = true;
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t gd32_ret = OtaCoordinator::instance().probeGd32AppOnce(&gd32_ver, nullptr);
|
||||
if (gd32_ret == ESP_OK) {
|
||||
DEBUG_INFO("MAIN", "GD32 APP: v%d.%d.%d.%d (BOOT v%d.%d.%d.%d)",
|
||||
gd32_ver.app_major, gd32_ver.app_minor,
|
||||
gd32_ver.app_patch, gd32_ver.app_build,
|
||||
gd32_ver.boot_major, gd32_ver.boot_minor,
|
||||
gd32_ver.boot_patch, gd32_ver.boot_build);
|
||||
gd32_ok = true;
|
||||
break;
|
||||
}
|
||||
DEBUG_INFO("MAIN", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!gd32_ok) {
|
||||
DEBUG_WARN("MAIN", "GD32 APP 版本读取失败(OtaRelay 任务将重试)");
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("MAIN", "GD32 版本探测由 ota_relay 任务负责(hello+probe)");
|
||||
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
|
||||
DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
|
||||
@@ -53,8 +53,8 @@ inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服
|
||||
// ============================================================
|
||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< httpd:原 10KB,peak~5KB → 8KB(余量~3KB)
|
||||
inline constexpr uint32_t TASK_STACK_WIFI_OTA = 4096; ///< wifi_ota 主循环:原 8KB,peak~1.4KB
|
||||
inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(实测 peak~1KB;完整 GD32 中继后再确认水位)
|
||||
inline constexpr uint32_t TASK_STACK_SPI_WORKER = 5120; ///< spi_worker:无单独水位,6144→5120 小幅收
|
||||
inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(轻载 peak~1KB;转发时看 /system/status.task_stack)
|
||||
inline constexpr uint32_t TASK_STACK_SPI_WORKER = 5120; ///< spi_worker:5KB(帧缓冲热路径;已纳入 task_stack 巡检)
|
||||
inline constexpr uint32_t TASK_STACK_BLE_OTA = 3072; ///< ble_ota 主任务:peak~1KB
|
||||
inline constexpr uint32_t TASK_STACK_BLE_WORKER = 3072; ///< ble_ota_worker:peak~1.1KB(负载峰值曾~1.9KB)
|
||||
inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.2KB
|
||||
@@ -92,6 +92,25 @@ inline constexpr size_t HEAP_WARNING_MIN = 10240; ///< 堆内
|
||||
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; ///< UDP 日志目标 IP
|
||||
inline constexpr uint16_t DEBUG_LOG_PORT = 6666; ///< UDP 日志目标端口
|
||||
|
||||
// ============================================================
|
||||
// SPI 主机(ESP32 ↔ GD32)可调参数
|
||||
// 协议帧格式见 spi_link_v2.h;引脚与超时集中在此便于板级调整
|
||||
// ============================================================
|
||||
inline constexpr int SPI_MASTER_CS_GPIO = 4; ///< CS 片选 GPIO
|
||||
inline constexpr int SPI_READY_GPIO = 10; ///< DATA_READY 通知线 GPIO(↔ GD32 PB6)
|
||||
inline constexpr int SPI_MODE = 0; ///< CPOL=0 CPHA=0
|
||||
inline constexpr int SPI_QUEUE_SIZE = 4; ///< SPI 事务队列深度
|
||||
inline constexpr size_t SPI_OTA_FRAME_MAX = 256; ///< 单 CS 最大帧长(APP 满缓冲)
|
||||
inline constexpr size_t SPI_BOOT_TX_BUF_SIZE = 128; ///< BOOT TX DMA 满缓冲(读预算用)
|
||||
inline constexpr size_t SPI_APP_TX_BUF_SIZE = 256; ///< APP TX DMA 满缓冲(读预算用)
|
||||
|
||||
// ============================================================
|
||||
// OTA 协调器
|
||||
// ============================================================
|
||||
inline constexpr uint32_t OTA_COORD_LOCK_TIMEOUT_MS = 100; ///< 协调器 mutex 超时(ms)
|
||||
/** 忙状态自愈:须 > 中继转发预算上限(约 120s)+ 握手/SIZE 余量。 */
|
||||
inline constexpr uint32_t OTA_COORD_BUSY_WATCHDOG_MS = 150000;
|
||||
|
||||
// ============================================================
|
||||
// BLE OTA 配置
|
||||
// ============================================================
|
||||
@@ -110,8 +129,12 @@ inline constexpr uint8_t BLE_OTA_CTRL_ABORT_OTA = 0xFB; ///< 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 单包数据载荷大小(字节)
|
||||
// 滑动 ACK 窗口(须与前端 tools/js/ble/constants.js BLE_OTA_ACK_WINDOW 保持一致)
|
||||
inline constexpr size_t BLE_OTA_ACK_WINDOW = 32;
|
||||
// Reorder buffer 窗口大小(吸收 BLE 窗口内乱序到达的包,须 >= BLE_OTA_ACK_WINDOW)
|
||||
inline constexpr size_t BLE_OTA_REORDER_WINDOW = 32;
|
||||
// 数据队列深度:略大于窗口,seq=0 预擦期间多扛几批
|
||||
inline constexpr size_t BLE_OTA_DATA_QUEUE_LEN = (BLE_OTA_ACK_WINDOW * 2 + 8);
|
||||
|
||||
// ============================================================
|
||||
// 安全常量(需与 GD32 固件团队确认)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 47
|
||||
#define FW_VERSION_STRING "0.3.47"
|
||||
#define FW_VERSION_PATCH 48
|
||||
#define FW_VERSION_STRING "0.3.48"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-07-16 01:10:05"
|
||||
#define BUILD_TIME "2026-07-16 22:59:52"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "a47a027"
|
||||
#define GIT_HASH "51c89b3"
|
||||
|
||||
+67
-40
@@ -6,12 +6,17 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "esp_timer.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
/** 截断/回绕 WARN 最小间隔(30s) */
|
||||
static TickType_t cpuStatsWarnIntervalTicks()
|
||||
{
|
||||
return pdMS_TO_TICKS(30000);
|
||||
}
|
||||
|
||||
CpuStats& CpuStats::instance()
|
||||
{
|
||||
static CpuStats inst;
|
||||
@@ -78,91 +83,113 @@ static bool taskNameIsIdle(const char* name)
|
||||
|
||||
void CpuStats::sampleOnce()
|
||||
{
|
||||
UBaseType_t task_count = uxTaskGetNumberOfTasks();
|
||||
if (task_count == 0) {
|
||||
const UBaseType_t live = uxTaskGetNumberOfTasks();
|
||||
if (live == 0) {
|
||||
return;
|
||||
}
|
||||
task_count = std::min<UBaseType_t>(task_count + 2U, (UBaseType_t)CPU_TASK_SNAPSHOT_MAX);
|
||||
|
||||
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
|
||||
if (!tasks) {
|
||||
return;
|
||||
/* P1:任务数超出快照上限时节流告警(否则静默丢任务) */
|
||||
if (live > (UBaseType_t)CPU_TASK_SNAPSHOT_MAX) {
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if (m_lastTruncWarnTick == 0 ||
|
||||
(now_tick - m_lastTruncWarnTick) >= cpuStatsWarnIntervalTicks()) {
|
||||
m_lastTruncWarnTick = now_tick;
|
||||
DEBUG_WARN("CPU",
|
||||
"任务数 %lu > 快照上限 %u,本拍仅统计前 %u 个任务",
|
||||
(unsigned long)live,
|
||||
(unsigned)CPU_TASK_SNAPSHOT_MAX,
|
||||
(unsigned)CPU_TASK_SNAPSHOT_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
/* P1:固定成员缓冲,免每拍 heap */
|
||||
const UBaseType_t buf_slots =
|
||||
std::min<UBaseType_t>(live + 2U, (UBaseType_t)CPU_TASK_SNAPSHOT_MAX);
|
||||
|
||||
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
|
||||
UBaseType_t captured = uxTaskGetSystemState(tasks, task_count, &total_runtime);
|
||||
const UBaseType_t captured =
|
||||
uxTaskGetSystemState(m_taskStatusBuf, buf_slots, &total_runtime);
|
||||
const int64_t now_us = esp_timer_get_time();
|
||||
|
||||
if (captured == 0 || captured > task_count) {
|
||||
if (captured == 0 || captured > buf_slots) {
|
||||
DEBUG_WARN("CPU", "uxTaskGetSystemState 异常: captured=%lu buf=%lu",
|
||||
(unsigned long)captured, (unsigned long)task_count);
|
||||
free(tasks);
|
||||
(unsigned long)captured, (unsigned long)buf_slots);
|
||||
return;
|
||||
}
|
||||
|
||||
CpuUsageSnapshot snap = {};
|
||||
snap.available = true;
|
||||
|
||||
/*
|
||||
* P2:先用 m_lastSamples 算本窗口,再写回基线(无需栈上 previous 拷贝)。
|
||||
* total 回绕(total_runtime < m_lastTotal)时丢弃本窗,只重建基线。
|
||||
*/
|
||||
const bool had_previous = m_hasLast;
|
||||
const configRUN_TIME_COUNTER_TYPE previous_total = m_lastTotal;
|
||||
const int64_t previous_time_us = m_lastTimeUs;
|
||||
const size_t previous_count = m_lastCount;
|
||||
TaskSample previous_samples[CPU_TASK_SNAPSHOT_MAX];
|
||||
for (size_t i = 0; i < previous_count && i < CPU_TASK_SNAPSHOT_MAX; ++i) {
|
||||
previous_samples[i] = m_lastSamples[i];
|
||||
const bool wrapped = had_previous && total_runtime < m_lastTotal;
|
||||
if (wrapped) {
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if (m_lastWrapWarnTick == 0 ||
|
||||
(now_tick - m_lastWrapWarnTick) >= cpuStatsWarnIntervalTicks()) {
|
||||
m_lastWrapWarnTick = now_tick;
|
||||
DEBUG_WARN("CPU",
|
||||
"run-time 计数回绕,丢弃本采样窗 total=%" PRIu64 " prev=%" PRIu64,
|
||||
(uint64_t)total_runtime,
|
||||
(uint64_t)m_lastTotal);
|
||||
}
|
||||
}
|
||||
|
||||
/* 先更新基线,再算本窗口(与旧 HTTP 路径语义一致) */
|
||||
size_t copy_count = std::min<size_t>((size_t)captured, CPU_TASK_SNAPSHOT_MAX);
|
||||
for (size_t i = 0; i < copy_count; ++i) {
|
||||
m_lastSamples[i].task_number = tasks[i].xTaskNumber;
|
||||
m_lastSamples[i].runtime = tasks[i].ulRunTimeCounter;
|
||||
}
|
||||
m_lastCount = copy_count;
|
||||
m_lastTotal = total_runtime;
|
||||
m_lastTimeUs = now_us;
|
||||
m_hasLast = true;
|
||||
|
||||
const bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
|
||||
const uint64_t total_delta = can_compute ? runtimeDelta(total_runtime, previous_total) : 0U;
|
||||
const bool can_compute =
|
||||
had_previous && !wrapped && now_us > m_lastTimeUs;
|
||||
const uint64_t total_delta =
|
||||
can_compute ? runtimeDelta(total_runtime, m_lastTotal) : 0U;
|
||||
uint64_t idle_delta = 0U;
|
||||
|
||||
if (can_compute && total_delta > 0U) {
|
||||
for (UBaseType_t i = 0; i < captured; ++i) {
|
||||
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
|
||||
bool found = false;
|
||||
for (size_t j = 0; j < previous_count; ++j) {
|
||||
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
|
||||
last_runtime = previous_samples[j].runtime;
|
||||
for (size_t j = 0; j < m_lastCount; ++j) {
|
||||
if (m_lastSamples[j].task_number == m_taskStatusBuf[i].xTaskNumber) {
|
||||
last_runtime = m_lastSamples[j].runtime;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
continue;
|
||||
continue; /* 新任务:本窗无 delta */
|
||||
}
|
||||
uint64_t delta = runtimeDelta(tasks[i].ulRunTimeCounter, last_runtime);
|
||||
/* 单任务计数回绕时 runtimeDelta→0,本任务本窗记 0 */
|
||||
uint64_t delta = runtimeDelta(m_taskStatusBuf[i].ulRunTimeCounter, last_runtime);
|
||||
double pct = (double)delta * 100.0 / (double)total_delta;
|
||||
if (taskNameIsIdle(tasks[i].pcTaskName)) {
|
||||
if (taskNameIsIdle(m_taskStatusBuf[i].pcTaskName)) {
|
||||
idle_delta += delta;
|
||||
}
|
||||
if (snap.task_count < CPU_TASK_SNAPSHOT_MAX) {
|
||||
CpuTaskMetric& m = snap.tasks[snap.task_count++];
|
||||
const char* n = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
|
||||
const char* n = m_taskStatusBuf[i].pcTaskName ? m_taskStatusBuf[i].pcTaskName : "";
|
||||
strncpy(m.name, n, sizeof(m.name) - 1);
|
||||
m.name[sizeof(m.name) - 1] = '\0';
|
||||
m.task_number = tasks[i].xTaskNumber;
|
||||
m.task_number = m_taskStatusBuf[i].xTaskNumber;
|
||||
m.runtime_delta = delta;
|
||||
m.usage_pct = pct;
|
||||
}
|
||||
}
|
||||
snap.valid = true;
|
||||
snap.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
|
||||
snap.sample_ms = (uint32_t)((now_us - m_lastTimeUs) / 1000);
|
||||
snap.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
|
||||
snap.usage_pct = std::max(0.0, 100.0 - snap.idle_pct);
|
||||
}
|
||||
|
||||
free(tasks);
|
||||
/* 更新基线(首拍 / 正常窗 / 回绕丢窗后均重建) */
|
||||
const size_t copy_count = std::min<size_t>((size_t)captured, CPU_TASK_SNAPSHOT_MAX);
|
||||
for (size_t i = 0; i < copy_count; ++i) {
|
||||
m_lastSamples[i].task_number = m_taskStatusBuf[i].xTaskNumber;
|
||||
m_lastSamples[i].runtime = m_taskStatusBuf[i].ulRunTimeCounter;
|
||||
}
|
||||
m_lastCount = copy_count;
|
||||
m_lastTotal = total_runtime;
|
||||
m_lastTimeUs = now_us;
|
||||
m_hasLast = true;
|
||||
|
||||
portENTER_CRITICAL(&m_mux);
|
||||
m_published = snap;
|
||||
|
||||
@@ -70,4 +70,12 @@ private:
|
||||
configRUN_TIME_COUNTER_TYPE m_lastTotal = 0;
|
||||
int64_t m_lastTimeUs = 0;
|
||||
bool m_hasLast = false;
|
||||
|
||||
/** uxTaskGetSystemState 目标缓冲(固定、免每拍 calloc) */
|
||||
TaskStatus_t m_taskStatusBuf[CPU_TASK_SNAPSHOT_MAX] = {};
|
||||
|
||||
/** 任务数截断 WARN 节流(tick) */
|
||||
TickType_t m_lastTruncWarnTick = 0;
|
||||
/** 运行时计数回绕 WARN 节流(tick) */
|
||||
TickType_t m_lastWrapWarnTick = 0;
|
||||
};
|
||||
|
||||
+448
-221
@@ -12,79 +12,165 @@
|
||||
* 时序说明:
|
||||
* esp_netif_init() 创建 tcpip 线程后才可安全使用 socket
|
||||
* 本模块在首次 send() 时才创建 socket(延迟创建)
|
||||
* WiFi 连接前 sendto() 会失败(无路由),日志仍可写入 UART 与 HTTP 日志缓冲
|
||||
* WiFi 连接后 UDP 立即生效,无需额外操作
|
||||
* WiFi 未获 IP 时跳过 sendto;日志仍写 UART 与 HTTP ring
|
||||
*
|
||||
* 线程:仅任务上下文;禁止 ISR 调用 DEBUG_*(内部 portMAX_DELAY mutex)。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_netif.h"
|
||||
#include "driver/uart.h"
|
||||
|
||||
#include "debug_log.h"
|
||||
|
||||
static const char *TAG = "DEBUG_LOG";
|
||||
|
||||
static TickType_t warnIntervalTicks()
|
||||
{
|
||||
return pdMS_TO_TICKS(10000);
|
||||
}
|
||||
static constexpr int kUdpSndTimeoutMs = 200;
|
||||
static constexpr uint8_t kUdpFailRecreate = 8;
|
||||
|
||||
// ============================================================
|
||||
// 单例
|
||||
// 单例 / 工具
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 获取 DebugLog 单例实例。
|
||||
* @return DebugLog 引用。
|
||||
*/
|
||||
DebugLog& DebugLog::instance()
|
||||
{
|
||||
static DebugLog inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 日志环形缓冲区(供 HTTP /logs 拉取)
|
||||
// ============================================================
|
||||
bool DebugLog::isKeepAliveTag(const char *tag)
|
||||
{
|
||||
if (!tag) {
|
||||
return false;
|
||||
}
|
||||
/* OTA / OTA_COORD 等;上传路径 WIFI;总线 SPI */
|
||||
if (strncmp(tag, "OTA", 3) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (strcmp(tag, "SPI") == 0 || strcmp(tag, "WIFI") == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
char *DebugLog::allocEmptyJson(int *out_len)
|
||||
{
|
||||
char *empty = (char *)malloc(3);
|
||||
if (!empty) {
|
||||
return nullptr;
|
||||
}
|
||||
empty[0] = '[';
|
||||
empty[1] = ']';
|
||||
empty[2] = '\0';
|
||||
if (out_len) {
|
||||
*out_len = 2;
|
||||
}
|
||||
return empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前环形日志缓冲区中的日志条数。
|
||||
* @return 日志条数。
|
||||
* JSON 字符串内容转义写入 buf[pos..];返回新 pos。
|
||||
* 控制字符除 \n\r\t 外写成 \u00XX;空间不足时截断并尽量写 "..."。
|
||||
*/
|
||||
int DebugLog::jsonEscapeAppend(char *buf, int pos, int capacity, const char *src, int src_len)
|
||||
{
|
||||
const int content_start = pos;
|
||||
const int hard_limit = capacity - 6; /* 预留 "..." + '"' + ']' + '\0' 量级 */
|
||||
bool truncated = false;
|
||||
|
||||
for (int i = 0; i < src_len; ++i) {
|
||||
if (pos >= hard_limit) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
/* 单条转义上限,避免异常长行占满 drain */
|
||||
if (pos - content_start >= LOG_RING_SIZE * 2) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const unsigned char c = (unsigned char)src[i];
|
||||
if (c == '"') {
|
||||
if (pos + 2 > hard_limit) { truncated = true; break; }
|
||||
buf[pos++] = '\\';
|
||||
buf[pos++] = '"';
|
||||
} else if (c == '\\') {
|
||||
if (pos + 2 > hard_limit) { truncated = true; break; }
|
||||
buf[pos++] = '\\';
|
||||
buf[pos++] = '\\';
|
||||
} else if (c == '\n') {
|
||||
if (pos + 2 > hard_limit) { truncated = true; break; }
|
||||
buf[pos++] = '\\';
|
||||
buf[pos++] = 'n';
|
||||
} else if (c == '\r') {
|
||||
if (pos + 2 > hard_limit) { truncated = true; break; }
|
||||
buf[pos++] = '\\';
|
||||
buf[pos++] = 'r';
|
||||
} else if (c == '\t') {
|
||||
if (pos + 2 > hard_limit) { truncated = true; break; }
|
||||
buf[pos++] = '\\';
|
||||
buf[pos++] = 't';
|
||||
} else if (c < 0x20) {
|
||||
/* 其它控制符 → \u00XX(6 字符) */
|
||||
if (pos + 6 > hard_limit) { truncated = true; break; }
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
buf[pos++] = '\\';
|
||||
buf[pos++] = 'u';
|
||||
buf[pos++] = '0';
|
||||
buf[pos++] = '0';
|
||||
buf[pos++] = hex[(c >> 4) & 0xF];
|
||||
buf[pos++] = hex[c & 0xF];
|
||||
} else {
|
||||
buf[pos++] = (char)c;
|
||||
}
|
||||
}
|
||||
|
||||
if (truncated && pos + 3 <= hard_limit) {
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 环形缓冲(Locked 变体假定已持 m_logRingMutex)
|
||||
// ============================================================
|
||||
int DebugLog::ringCount()
|
||||
{
|
||||
if (!m_logRingMutex) return 0;
|
||||
if (!m_logRingMutex) {
|
||||
return 0;
|
||||
}
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
int count = m_logRingCount;
|
||||
const int count = m_logRingCount;
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 弹出一条最旧日志并从环形缓冲区移除。
|
||||
* @param out 输出缓冲区。
|
||||
* @param out_size 输出缓冲区大小。
|
||||
* @param out_len 输出日志长度。
|
||||
* @return true 表示成功弹出一条日志,false 表示缓冲区为空或参数无效。
|
||||
*/
|
||||
bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
|
||||
bool DebugLog::ringPopOldestLocked(char *out, size_t out_size, int *out_len)
|
||||
{
|
||||
if (out_len) {
|
||||
*out_len = 0;
|
||||
}
|
||||
if (!m_logRingMutex || !out || out_size == 0) {
|
||||
if (!out || out_size == 0 || m_logRingCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
if (m_logRingCount <= 0) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
int idx = (m_logRingHead - m_logRingCount + LOG_RING_SLOTS) % LOG_RING_SLOTS;
|
||||
const int idx = (m_logRingHead - m_logRingCount + LOG_RING_SLOTS) % LOG_RING_SLOTS;
|
||||
int len = m_logRingLens[idx];
|
||||
if (len < 0) {
|
||||
len = 0;
|
||||
@@ -105,132 +191,212 @@ bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
|
||||
|
||||
m_logRingLens[idx] = 0;
|
||||
m_logRingCount--;
|
||||
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将环形缓冲区所有日志导出为 JSON 数组字符串。
|
||||
* @param out_len 输出 JSON 字符串长度。
|
||||
* @return 动态分配的 JSON 字符串,调用者须 free()。
|
||||
*/
|
||||
char* DebugLog::ringDrainJson(int *out_len)
|
||||
bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
|
||||
{
|
||||
if (!out_len) return NULL;
|
||||
if (out_len) {
|
||||
*out_len = 0;
|
||||
}
|
||||
if (!m_logRingMutex || !out || out_size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
const bool ok = ringPopOldestLocked(out, out_size, out_len);
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** 预览即将 drain 的条数与原始字节总和(用于估 capacity)。 */
|
||||
int DebugLog::ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const
|
||||
{
|
||||
if (total_raw_len) {
|
||||
*total_raw_len = 0;
|
||||
}
|
||||
int n = m_logRingCount;
|
||||
if (n > max_n) {
|
||||
n = max_n;
|
||||
}
|
||||
if (n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const int idx = (m_logRingHead - m_logRingCount + i + LOG_RING_SLOTS) % LOG_RING_SLOTS;
|
||||
int len = m_logRingLens[idx];
|
||||
if (len < 0) {
|
||||
len = 0;
|
||||
}
|
||||
if (len >= LOG_RING_SIZE) {
|
||||
len = LOG_RING_SIZE - 1;
|
||||
}
|
||||
sum += len;
|
||||
}
|
||||
if (total_raw_len) {
|
||||
*total_raw_len = sum;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void DebugLog::ringPushLocked(const char *data, int len)
|
||||
{
|
||||
if (!data || len <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int copy_len = len;
|
||||
if (copy_len >= LOG_RING_SIZE) {
|
||||
copy_len = LOG_RING_SIZE - 1;
|
||||
}
|
||||
|
||||
memcpy(m_logRing[m_logRingHead], data, (size_t)copy_len);
|
||||
m_logRing[m_logRingHead][copy_len] = '\0';
|
||||
m_logRingLens[m_logRingHead] = copy_len;
|
||||
|
||||
m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS;
|
||||
if (m_logRingCount < LOG_RING_SLOTS) {
|
||||
m_logRingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void DebugLog::ringPush(const char *data, int len)
|
||||
{
|
||||
if (!m_logRingMutex || len <= 0) {
|
||||
return;
|
||||
}
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
ringPushLocked(data, len);
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次持锁:peek 元数据 →(可选)按堆缩小条数 → pop+转义拼 JSON。
|
||||
* capacity 按真实 raw 长度估算,避免 48×320 虚高导致 OTA 期返空。
|
||||
*/
|
||||
char *DebugLog::ringDrainJson(int *out_len)
|
||||
{
|
||||
if (!out_len) {
|
||||
return nullptr;
|
||||
}
|
||||
*out_len = 0;
|
||||
|
||||
/* 堆紧时不 malloc:返回堆上最小 "[]",调用方仍 free。 */
|
||||
const uint32_t free_now = esp_get_free_heap_size();
|
||||
if (free_now < kDrainMinFree) {
|
||||
char *empty = (char *)malloc(3);
|
||||
if (!empty) return NULL;
|
||||
empty[0] = '[';
|
||||
empty[1] = ']';
|
||||
empty[2] = '\0';
|
||||
*out_len = 2;
|
||||
return empty;
|
||||
if (free_now < kDrainMinFree || !m_logRingMutex) {
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
|
||||
int count = ringCount();
|
||||
/* OTA 沉默失败排查需要一次多拉;上限 24 条 */
|
||||
static constexpr int kMaxDrain = 24;
|
||||
if (count > kMaxDrain) count = kMaxDrain;
|
||||
int capacity = count * (LOG_RING_SIZE * 2) + 16;
|
||||
if (capacity < 16) capacity = 16;
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
int total_raw = 0;
|
||||
int drain_n = ringPeekDrainMetaLocked(LOG_DRAIN_MAX, &total_raw);
|
||||
if (drain_n <= 0) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
|
||||
/* 转义最坏 ~2x + 每条 "" 与逗号 + 数组括号 */
|
||||
auto calc_capacity = [](int n, int raw) -> int {
|
||||
int cap = raw * 2 + n * 4 + 16;
|
||||
if (cap < 16) {
|
||||
cap = 16;
|
||||
}
|
||||
return cap;
|
||||
};
|
||||
|
||||
int capacity = calc_capacity(drain_n, total_raw);
|
||||
const size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||
if (largest < (size_t)capacity) {
|
||||
char *empty = (char *)malloc(3);
|
||||
if (!empty) return NULL;
|
||||
empty[0] = '[';
|
||||
empty[1] = ']';
|
||||
empty[2] = '\0';
|
||||
*out_len = 2;
|
||||
return empty;
|
||||
|
||||
/* 堆不够则减条数重估(仍持锁,peek 只读;减 n 时从最旧开始仍一致) */
|
||||
while (drain_n > 0 && largest < (size_t)capacity) {
|
||||
drain_n = drain_n / 2;
|
||||
if (drain_n == 0) {
|
||||
break;
|
||||
}
|
||||
total_raw = 0;
|
||||
drain_n = ringPeekDrainMetaLocked(drain_n, &total_raw);
|
||||
capacity = calc_capacity(drain_n, total_raw);
|
||||
}
|
||||
|
||||
char *buf = (char*)malloc(capacity);
|
||||
if (!buf) return NULL;
|
||||
if (drain_n <= 0 || largest < (size_t)capacity) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
|
||||
/* 持锁期间尽量短:只 pop 到栈上临时区会爆栈;改为直接写入 malloc 缓冲。
|
||||
* 先释放锁再 malloc,再二次持锁 pop——中间可能插入新日志,只 drain 最旧 N 条即可。 */
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
|
||||
char *buf = (char *)malloc((size_t)capacity);
|
||||
if (!buf) {
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
/* 重新核对:ring 可能变少 */
|
||||
if (m_logRingCount <= 0) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
free(buf);
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
if (drain_n > m_logRingCount) {
|
||||
drain_n = m_logRingCount;
|
||||
}
|
||||
if (drain_n > LOG_DRAIN_MAX) {
|
||||
drain_n = LOG_DRAIN_MAX;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
buf[pos++] = '[';
|
||||
bool first = true;
|
||||
char entry[LOG_RING_SIZE];
|
||||
int entryLen = 0;
|
||||
|
||||
while (ringPopOldest(entry, sizeof(entry), &entryLen) && entryLen > 0) {
|
||||
// 逗号写入前做边界检查,预留 4 字节给 ']' + '\0' 及末尾余量
|
||||
for (int k = 0; k < drain_n; ++k) {
|
||||
/* 先估空间再 pop,避免 pop 后写不下导致丢条 */
|
||||
const int need_min = first ? 4 : 5; /* ,"x" 或 "x" 量级 + 闭合余量 */
|
||||
if (pos + need_min >= capacity - 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
int entry_len = 0;
|
||||
if (!ringPopOldestLocked(entry, sizeof(entry), &entry_len) || entry_len <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
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++] = '\\'; }
|
||||
else if (c == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; }
|
||||
else if (c == '\r') { buf[pos++] = '\\'; buf[pos++] = 'r'; }
|
||||
else if (c == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; }
|
||||
else if ((unsigned char)c >= 0x20) { buf[pos++] = c; }
|
||||
pos = jsonEscapeAppend(buf, pos, capacity, entry, entry_len);
|
||||
if (pos < capacity - 1) {
|
||||
buf[pos++] = '"';
|
||||
} else if (pos < capacity) {
|
||||
buf[pos - 1] = '"'; /* 极端:覆盖末字节闭合 */
|
||||
}
|
||||
if (truncated) {
|
||||
// 确保 "..." + '"' + ']' + '\0' (共 6 字节) 有空间才写入标记
|
||||
if (pos + 6 <= capacity) {
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
}
|
||||
}
|
||||
buf[pos++] = '"';
|
||||
}
|
||||
buf[pos++] = ']';
|
||||
buf[pos] = '\0';
|
||||
|
||||
if (pos + 1 < capacity) {
|
||||
buf[pos++] = ']';
|
||||
buf[pos] = '\0';
|
||||
} else if (capacity >= 3) {
|
||||
buf[capacity - 2] = ']';
|
||||
buf[capacity - 1] = '\0';
|
||||
pos = capacity - 1;
|
||||
}
|
||||
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
|
||||
*out_len = pos;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 向环形日志缓冲区中推送一条日志数据。
|
||||
* @param data 日志数据指针。
|
||||
* @param len 数据长度。
|
||||
*/
|
||||
void DebugLog::ringPush(const char *data, int len)
|
||||
{
|
||||
if (!m_logRingMutex || len <= 0) return;
|
||||
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
int copy_len = len;
|
||||
if (copy_len >= LOG_RING_SIZE) copy_len = LOG_RING_SIZE - 1;
|
||||
|
||||
memcpy(m_logRing[m_logRingHead], data, copy_len);
|
||||
m_logRing[m_logRingHead][copy_len] = '\0';
|
||||
m_logRingLens[m_logRingHead] = copy_len;
|
||||
|
||||
m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS;
|
||||
if (m_logRingCount < LOG_RING_SLOTS) m_logRingCount++;
|
||||
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UART 调试日志
|
||||
// UART
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 初始化 UART0 调试串口。
|
||||
* @return esp_err_t 错误码。
|
||||
*/
|
||||
esp_err_t DebugLog::uartInit()
|
||||
{
|
||||
if (m_uartInitialized) {
|
||||
@@ -248,7 +414,8 @@ esp_err_t DebugLog::uartInit()
|
||||
|
||||
esp_err_t ret = uart_driver_install(uart_port, 1024, 0, 0, NULL, 0);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "UART0 驱动安装失败: %s", esp_err_to_name(ret));
|
||||
/* 可能 console 已占用 UART0——降级为 ring-only */
|
||||
ESP_LOGW(TAG, "UART0 驱动安装失败(可能 console 占用): %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -266,49 +433,34 @@ 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);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通过 UART0 直接发送原始字节数据。
|
||||
* @param data 待发送数据指针。
|
||||
* @param len 数据长度。
|
||||
* @return esp_err_t 错误码。
|
||||
*/
|
||||
esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!m_uartInitialized) {
|
||||
if (!m_uartInitialized || !m_uartAvailable) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
int ret = uart_write_bytes(UART_NUM_0, data, len);
|
||||
const int ret = uart_write_bytes(UART_NUM_0, data, len);
|
||||
if (ret < 0) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
|
||||
// 初始化 / UDP
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。
|
||||
* @return esp_err_t 错误码。
|
||||
*/
|
||||
esp_err_t DebugLog::init()
|
||||
{
|
||||
if (m_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 检查 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));
|
||||
@@ -316,8 +468,13 @@ esp_err_t DebugLog::init()
|
||||
}
|
||||
|
||||
m_logRingMutex = xSemaphoreCreateMutex();
|
||||
if (!m_logRingMutex) {
|
||||
ESP_LOGE(TAG, "ring mutex 创建失败,HTTP /logs 将不可用");
|
||||
/* 仍标记 initialized,避免 log() 反复 init;UART 路径可工作 */
|
||||
m_initialized = true;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// 目标地址:单播到上位机
|
||||
memset(&m_debugAddr, 0, sizeof(m_debugAddr));
|
||||
m_debugAddr.sin_family = AF_INET;
|
||||
m_debugAddr.sin_port = htons(DEBUG_LOG_PORT);
|
||||
@@ -325,121 +482,173 @@ esp_err_t DebugLog::init()
|
||||
|
||||
m_initialized = true;
|
||||
ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 确保 UDP socket 已创建(延迟创建)。
|
||||
* @return esp_err_t 错误码。
|
||||
*/
|
||||
bool DebugLog::udpNetifHasIp() const
|
||||
{
|
||||
esp_netif_ip_info_t info = {};
|
||||
esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (sta && esp_netif_get_ip_info(sta, &info) == ESP_OK && info.ip.addr != 0) {
|
||||
return true;
|
||||
}
|
||||
esp_netif_t *ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||
if (ap && esp_netif_get_ip_info(ap, &info) == ESP_OK && info.ip.addr != 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DebugLog::closeUdpSocket()
|
||||
{
|
||||
int s = -1;
|
||||
portENTER_CRITICAL(&m_udpMux);
|
||||
s = m_udpSock;
|
||||
m_udpSock = -1;
|
||||
portEXIT_CRITICAL(&m_udpMux);
|
||||
if (s >= 0) {
|
||||
close(s);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t DebugLog::ensureUdpSocket()
|
||||
{
|
||||
portENTER_CRITICAL(&m_udpMux);
|
||||
if (m_udpSock >= 0) {
|
||||
portEXIT_CRITICAL(&m_udpMux);
|
||||
return ESP_OK;
|
||||
}
|
||||
portEXIT_CRITICAL(&m_udpMux);
|
||||
|
||||
m_udpSock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (m_udpSock < 0) {
|
||||
ESP_LOGE(TAG, "创建 UDP socket 失败");
|
||||
const int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s < 0) {
|
||||
ESP_LOGE(TAG, "创建 UDP socket 失败 errno=%d", errno);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "UDP socket 创建成功");
|
||||
/* 避免 sendto 在无路由/对端不可达时长时间阻塞日志路径 */
|
||||
struct timeval tv = {};
|
||||
tv.tv_sec = kUdpSndTimeoutMs / 1000;
|
||||
tv.tv_usec = (kUdpSndTimeoutMs % 1000) * 1000;
|
||||
(void)setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
portENTER_CRITICAL(&m_udpMux);
|
||||
if (m_udpSock >= 0) {
|
||||
/* 竞态:另一任务已建好 */
|
||||
portEXIT_CRITICAL(&m_udpMux);
|
||||
close(s);
|
||||
return ESP_OK;
|
||||
}
|
||||
m_udpSock = s;
|
||||
portEXIT_CRITICAL(&m_udpMux);
|
||||
|
||||
ESP_LOGI(TAG, "UDP socket 创建成功 (SNDTIMEO=%dms)", kUdpSndTimeoutMs);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送原始日志数据到底层输出通道。
|
||||
* @param data 待发送数据指针。
|
||||
* @param len 数据长度。
|
||||
* @return esp_err_t 错误码。
|
||||
*/
|
||||
esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!data || len == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t udp_ret = ESP_FAIL;
|
||||
esp_err_t uart_ret = ESP_FAIL;
|
||||
const uint32_t free_now = esp_get_free_heap_size();
|
||||
|
||||
// UART 通道(UART 不可用时跳过 uart_write_bytes 调用)
|
||||
if (m_uartAvailable) {
|
||||
uart_ret = uartSend(data, len);
|
||||
}
|
||||
|
||||
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
|
||||
* WiFi 未连接时 sendto 失败也静默跳过。
|
||||
* ★ 修复:堆不足跳过时写一条告警到 ring buffer,让前端 /logs 可见。 */
|
||||
if (m_initialized && free_now >= kSkipUdpFree) {
|
||||
/* UDP:堆紧 / 无 IP 时跳过 sendto */
|
||||
if (m_initialized && free_now >= kSkipUdpFree && udpNetifHasIp()) {
|
||||
if (m_udpSock < 0) {
|
||||
ensureUdpSocket();
|
||||
(void)ensureUdpSocket();
|
||||
}
|
||||
|
||||
if (m_udpSock >= 0) {
|
||||
size_t send_len = len > 1024 ? 1024 : len;
|
||||
int ret = ::sendto(m_udpSock, data, send_len, 0,
|
||||
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
|
||||
int sock = -1;
|
||||
portENTER_CRITICAL(&m_udpMux);
|
||||
sock = m_udpSock;
|
||||
portEXIT_CRITICAL(&m_udpMux);
|
||||
|
||||
if (sock >= 0) {
|
||||
const size_t send_len = len > 1024 ? 1024 : len;
|
||||
const int ret = ::sendto(sock, data, send_len, 0,
|
||||
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
|
||||
if (ret >= 0) {
|
||||
udp_ret = ESP_OK;
|
||||
m_udpFailStreak = 0;
|
||||
} else {
|
||||
if (m_udpFailStreak < 255) {
|
||||
m_udpFailStreak++;
|
||||
}
|
||||
if (m_udpFailStreak >= kUdpFailRecreate) {
|
||||
closeUdpSocket();
|
||||
m_udpFailStreak = 0;
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
if (m_lastUdpFailWarnTick == 0 ||
|
||||
(now - m_lastUdpFailWarnTick) >= warnIntervalTicks()) {
|
||||
m_lastUdpFailWarnTick = now;
|
||||
static const char kMsg[] = "[W][LOG] UDP 连续失败,已重建 socket\n";
|
||||
ringPush(kMsg, (int)sizeof(kMsg) - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (m_initialized) {
|
||||
/* 堆不足导致 UDP 被跳过——通过 ring buffer 记录,避免上位机无法感知 */
|
||||
static uint32_t s_lastUdpSkipWarnMs = 0;
|
||||
uint32_t now_ms = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
/* 限制告警频率:每 10 秒最多一条,避免刷屏 */
|
||||
if ((now_ms - s_lastUdpSkipWarnMs) > 10000) {
|
||||
s_lastUdpSkipWarnMs = now_ms;
|
||||
} else if (m_initialized && free_now < kSkipUdpFree) {
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
if (m_lastUdpSkipWarnTick == 0 ||
|
||||
(now - m_lastUdpSkipWarnTick) >= warnIntervalTicks()) {
|
||||
m_lastUdpSkipWarnTick = now;
|
||||
static const char kUdpSkipMsg[] = "[W][LOG] UDP 发送已暂停: 堆内存不足\n";
|
||||
ringPush(kUdpSkipMsg, sizeof(kUdpSkipMsg) - 1);
|
||||
ringPush(kUdpSkipMsg, (int)sizeof(kUdpSkipMsg) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 同时写入环形缓冲区(供 HTTP /logs 拉取)——静态,不占堆
|
||||
ringPush((const char *)data, (int)len);
|
||||
|
||||
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
/* 仅 ring 成功也算可用(OTA 堆紧时常见) */
|
||||
if (m_logRingMutex) {
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按日志级别与标签格式化输出一条日志。
|
||||
* @param level 日志级别。
|
||||
* @param tag 日志标签。
|
||||
* @param fmt 格式化字符串。
|
||||
* @param ... 可变参数。
|
||||
*/
|
||||
void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
init();
|
||||
(void)init();
|
||||
}
|
||||
|
||||
if (level > logLevel) {
|
||||
if (level > m_logLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 堆门控:OTA/SPI 的 INFO 必须进 ring,否则 WebUI 只能看到“沉默失败”。 */
|
||||
const uint32_t free_now = esp_get_free_heap_size();
|
||||
const bool keep_ota_spi = (tag != nullptr &&
|
||||
(strcmp(tag, "OTA") == 0 || strcmp(tag, "SPI") == 0));
|
||||
if (!keep_ota_spi) {
|
||||
if (free_now < kDropWarnFree && level > DEBUG_LOG_ERR) {
|
||||
return; // 无关模块:仅 ERR
|
||||
const bool keep = isKeepAliveTag(tag);
|
||||
|
||||
/*
|
||||
* 堆门控:
|
||||
* - ERR/WARN:永不因堆丢弃(排障必需)
|
||||
* - keep-alive(OTA 前缀 / SPI / WIFI):INFO 保留;极紧时仍丢 DBG
|
||||
* - 其它:free < kDropInfoFree 丢 INFO/DBG
|
||||
*/
|
||||
if (level >= DEBUG_LOG_INFO) {
|
||||
if (level >= DEBUG_LOG_DBG && free_now < kDropWarnFree) {
|
||||
return;
|
||||
}
|
||||
if (free_now < kDropInfoFree && level > DEBUG_LOG_WARN) {
|
||||
return; // 无关模块:丢 INFO/DBG
|
||||
if (!keep) {
|
||||
if (free_now < kDropInfoFree) {
|
||||
return;
|
||||
}
|
||||
} else if (level >= DEBUG_LOG_DBG && free_now < kDropInfoFree) {
|
||||
/* keep-alive DBG:中等堆紧也丢,减刷屏 */
|
||||
return;
|
||||
}
|
||||
} else if (free_now < kDropWarnFree && level >= DEBUG_LOG_DBG) {
|
||||
return; // OTA/SPI:极紧时仍丢 DBG,INFO/WARN/ERR 保留
|
||||
}
|
||||
|
||||
char prefix[24];
|
||||
const char *level_str;
|
||||
switch (level) {
|
||||
case DEBUG_LOG_ERR: level_str = "E"; break;
|
||||
@@ -448,29 +657,47 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
|
||||
case DEBUG_LOG_DBG: level_str = "D"; break;
|
||||
default: level_str = "?"; break;
|
||||
}
|
||||
snprintf(prefix, sizeof(prefix), "[%s][%s]", level_str, tag);
|
||||
|
||||
/* 栈缓冲与 LOG_RING_SIZE 对齐,减轻 httpd 等任务栈压。 */
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
char msg[160];
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
char log_buf[192];
|
||||
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
|
||||
|
||||
// ★ snprintf 截断时返回"本应写入的长度"(可能 > sizeof(log_buf)),
|
||||
// 直接传给 send() 会越界读取栈外内存 → 破坏相邻任务 TCB → "Stack protection fault"
|
||||
// (崩溃栈中出现 HTTP User-Agent / "/ota/relay" 等字符串即此症状)。
|
||||
// 截断到实际缓冲区大小,避免越界读。
|
||||
if (total < 0) {
|
||||
/* 单缓冲:与 LOG_RING_SIZE 对齐,避免 msg[]+log_buf[] 双栈 */
|
||||
char log_buf[LOG_RING_SIZE];
|
||||
const char *safe_tag = tag ? tag : "?";
|
||||
int prefix_len = snprintf(log_buf, sizeof(log_buf), "[%s][%s] ", level_str, safe_tag);
|
||||
if (prefix_len < 0) {
|
||||
return;
|
||||
}
|
||||
if ((size_t)total >= sizeof(log_buf)) {
|
||||
total = (int)sizeof(log_buf) - 1;
|
||||
log_buf[total] = '\0';
|
||||
if ((size_t)prefix_len >= sizeof(log_buf)) {
|
||||
/* 前缀已截断:保证可发送的以 \\n 结尾的串 */
|
||||
log_buf[sizeof(log_buf) - 2] = '\n';
|
||||
log_buf[sizeof(log_buf) - 1] = '\0';
|
||||
(void)send((const uint8_t *)log_buf, sizeof(log_buf) - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
send((const uint8_t *)log_buf, total);
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int body = vsnprintf(log_buf + prefix_len, sizeof(log_buf) - (size_t)prefix_len, fmt, args);
|
||||
va_end(args);
|
||||
if (body < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t used = (size_t)prefix_len + (size_t)body;
|
||||
if (used >= sizeof(log_buf)) {
|
||||
used = sizeof(log_buf) - 1;
|
||||
log_buf[used] = '\0';
|
||||
}
|
||||
|
||||
/* 保证换行结尾(截断时覆盖末字节) */
|
||||
if (used == 0 || log_buf[used - 1] != '\n') {
|
||||
if (used + 1 < sizeof(log_buf)) {
|
||||
log_buf[used++] = '\n';
|
||||
log_buf[used] = '\0';
|
||||
} else {
|
||||
log_buf[sizeof(log_buf) - 2] = '\n';
|
||||
log_buf[sizeof(log_buf) - 1] = '\0';
|
||||
used = sizeof(log_buf) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
(void)send((const uint8_t *)log_buf, used);
|
||||
}
|
||||
|
||||
+47
-29
@@ -8,8 +8,11 @@
|
||||
*
|
||||
* 注意:当前 tools/WebUI 日志面板走的是 HTTP /logs,
|
||||
* 不是直接监听 UDP 6666。
|
||||
*
|
||||
*
|
||||
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
|
||||
*
|
||||
* 线程模型:DEBUG_* / log() / send() 仅允许在任务上下文调用
|
||||
* (内部使用 portMAX_DELAY 互斥锁,禁止 ISR)。
|
||||
*/
|
||||
#ifndef DEBUG_LOG_H
|
||||
#define DEBUG_LOG_H
|
||||
@@ -23,7 +26,7 @@
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
// 调试日志配置(DEBUG_LOG_IP/PORT/ENABLED 已在 app_config.h 中定义)
|
||||
// 调试日志配置(DEBUG_LOG_IP/PORT 在 app_config.h)
|
||||
|
||||
// UART0 调试配置
|
||||
#define UART_DEBUG_TX_GPIO 21 ///< UART0 TX GPIO 引脚号
|
||||
@@ -46,13 +49,9 @@ typedef enum {
|
||||
*/
|
||||
class DebugLog {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取 DebugLog 单例实例。
|
||||
* @return DebugLog 引用。
|
||||
*/
|
||||
static DebugLog& instance();
|
||||
|
||||
/** @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 */
|
||||
/** @brief 初始化日志系统(UART + ring;UDP socket 延迟创建)。 */
|
||||
esp_err_t init();
|
||||
|
||||
/** @brief 初始化 UART0 调试串口。 */
|
||||
@@ -61,10 +60,10 @@ public:
|
||||
/** @brief 通过 UART0 直接发送一段原始字节数据。 */
|
||||
esp_err_t uartSend(const uint8_t *data, size_t len);
|
||||
|
||||
/** @brief 发送原始日志数据到底层输出通道。 */
|
||||
/** @brief 发送原始日志数据到底层输出通道(任务上下文)。 */
|
||||
esp_err_t send(const uint8_t *data, size_t len);
|
||||
|
||||
/** @brief 按日志级别与标签格式化输出一条日志。 */
|
||||
/** @brief 按日志级别与标签格式化输出一条日志(任务上下文)。 */
|
||||
void log(debug_log_level_t level, const char *tag, const char *fmt, ...);
|
||||
|
||||
/** @brief 获取当前环形日志缓冲区中的日志条数。 */
|
||||
@@ -74,14 +73,14 @@ public:
|
||||
bool ringPopOldest(char *out, size_t out_size, int *out_len);
|
||||
|
||||
/**
|
||||
* @brief 将环形缓冲区所有日志导出为 JSON 数组字符串。
|
||||
* @brief 将环形缓冲区日志导出为 JSON 数组字符串。
|
||||
* @param out_len 输出 JSON 字符串长度。
|
||||
* @return 动态分配的 JSON 字符串,调用者须 free()。
|
||||
* @return 动态分配的 JSON 字符串,调用者须 free();失败返回 NULL。
|
||||
*/
|
||||
char* ringDrainJson(int *out_len);
|
||||
|
||||
/** @brief 当前日志级别(运行时可改)。 */
|
||||
debug_log_level_t logLevel = DEBUG_LOG_INFO;
|
||||
void setLogLevel(debug_log_level_t level) { m_logLevel = level; }
|
||||
debug_log_level_t getLogLevel() const { return m_logLevel; }
|
||||
|
||||
private:
|
||||
DebugLog() = default;
|
||||
@@ -90,40 +89,59 @@ private:
|
||||
DebugLog& operator=(const DebugLog&) = delete;
|
||||
|
||||
int m_udpSock = -1;
|
||||
struct sockaddr_in m_debugAddr;
|
||||
struct sockaddr_in m_debugAddr = {};
|
||||
bool m_initialized = false;
|
||||
bool m_uartInitialized = false;
|
||||
// 记录 UART 是否真正可用(uartInit 失败时为 false,跳过 uart_write_bytes)。
|
||||
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
|
||||
/** uartInit 成功后才为 true;send() 据此跳过 uart_write_bytes */
|
||||
bool m_uartAvailable = false;
|
||||
debug_log_level_t m_logLevel = DEBUG_LOG_INFO;
|
||||
|
||||
/* 环形日志:OTA 中继期需保留关键 INFO,64×160≈10KB BSS。 */
|
||||
static constexpr int LOG_RING_SLOTS = 64;
|
||||
static constexpr int LOG_RING_SIZE = 160;
|
||||
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE];
|
||||
int m_logRingLens[LOG_RING_SLOTS];
|
||||
/** 单次 /logs drain 上限(与 SLOTS 更接近,减 burst 滞后) */
|
||||
static constexpr int LOG_DRAIN_MAX = 48;
|
||||
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE] = {};
|
||||
int m_logRingLens[LOG_RING_SLOTS] = {};
|
||||
int m_logRingHead = 0;
|
||||
int m_logRingCount = 0;
|
||||
SemaphoreHandle_t m_logRingMutex = NULL;
|
||||
SemaphoreHandle_t m_logRingMutex = nullptr;
|
||||
|
||||
/* 堆门控(防 OTA 时 UDP/格式化抢 LWIP 堆):
|
||||
* free < kDropWarnFree → 非 OTA/SPI 只保留 ERR
|
||||
* free < kDropInfoFree → 非 OTA/SPI 丢 INFO/DBG(OTA/SPI 的 INFO 仍进 ring)
|
||||
* ERR/WARN 永不因堆门控丢弃
|
||||
* free < kDropInfoFree → 非 keep-alive 丢 INFO/DBG;keep-alive 保留 INFO
|
||||
* free < kDropWarnFree → 全局丢 DBG
|
||||
* free < kSkipUdpFree → 不 sendto(仍 UART+ring)
|
||||
* free < kDrainMinFree → /logs 尽量仍返回 ring(降低门槛,避免“沉默失败”) */
|
||||
* free < kDrainMinFree → /logs 返 "[]" 避免大块 malloc
|
||||
* keep-alive tag:OTA 前缀、SPI、WIFI */
|
||||
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
|
||||
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
|
||||
static constexpr uint32_t kSkipUdpFree = 6U * 1024U; // ★ 修复:从 10KB 降至 6KB,sendto 本身不需大块堆
|
||||
static constexpr uint32_t kDrainMinFree = 4U * 1024U; // ★ 修复:从 8KB 降至 4KB,降低 /logs 返空的概率
|
||||
static constexpr uint32_t kSkipUdpFree = 6U * 1024U;
|
||||
static constexpr uint32_t kDrainMinFree = 4U * 1024U;
|
||||
|
||||
/** UDP 跳过/失败告警节流与 socket 重建 */
|
||||
TickType_t m_lastUdpSkipWarnTick = 0;
|
||||
TickType_t m_lastUdpFailWarnTick = 0;
|
||||
uint8_t m_udpFailStreak = 0;
|
||||
portMUX_TYPE m_udpMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
esp_err_t ensureUdpSocket();
|
||||
void closeUdpSocket();
|
||||
bool udpNetifHasIp() const;
|
||||
void ringPush(const char *data, int len);
|
||||
void ringPushLocked(const char *data, int len);
|
||||
bool ringPopOldestLocked(char *out, size_t out_size, int *out_len);
|
||||
int ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const;
|
||||
|
||||
static bool isKeepAliveTag(const char *tag);
|
||||
static char *allocEmptyJson(int *out_len);
|
||||
static int jsonEscapeAppend(char *buf, int pos, int capacity, const char *src, int src_len);
|
||||
};
|
||||
|
||||
// 便捷宏 — 直接调用单例方法
|
||||
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__) ///< 错误级别日志宏
|
||||
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__) ///< 警告级别日志宏
|
||||
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__) ///< 信息级别日志宏
|
||||
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__) ///< 调试级别日志宏
|
||||
// 便捷宏 — 直接调用单例方法(禁止在 ISR 中使用)
|
||||
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
|
||||
|
||||
#endif // DEBUG_LOG_H
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
// 避免 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 constexpr size_t STACK_TASK_MAX = 12; ///< 受监控任务上限(含 spi_worker 等)
|
||||
static bool s_stackWarned[STACK_TASK_MAX] = {}; ///< 各任务是否已告警(去重)
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ota_relay.h"
|
||||
#include "wifi_ota.h"
|
||||
#include "ble_ota.h"
|
||||
#include "spi_master.h"
|
||||
|
||||
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
|
||||
{
|
||||
@@ -30,9 +31,13 @@ size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
|
||||
if (n < max) {
|
||||
out[n++] = { "cpu_stats", CpuStats::instance().getTaskHandle(), TASK_STACK_CPU_STATS };
|
||||
}
|
||||
/* 转发热路径:ota_relay(业务)+ spi_worker(帧缓冲,此前监控缺口) */
|
||||
if (n < max) {
|
||||
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), TASK_STACK_OTA_RELAY };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "spi_worker", SpiMaster::instance().getWorkerTaskHandle(), TASK_STACK_SPI_WORKER };
|
||||
}
|
||||
if (n < max) {
|
||||
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), TASK_STACK_WIFI_OTA };
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* - health_monitor 巡检主动告警(低水位时 DEBUG_ERR,突破 OTA 门控)
|
||||
* - http_handlers /system/status 被动报告(前端每秒轮询展示)
|
||||
* 两处复用同一份任务表,避免散落维护。
|
||||
*
|
||||
* 表内含转发关键:ota_relay、spi_worker(帧缓冲热路径)。
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
|
||||
+11
-39
@@ -172,36 +172,9 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
/* ★ 整分区预擦(非 SEQUENTIAL 边写边擦):
|
||||
* 1) 扇区循环 erase + 喂狗/yield,避免 TASK_WDT 与 WiFi 饿死
|
||||
* 2) esp_ota_resume(erase_size≠SEQUENTIAL) → need_erase=false,写路径纯 write
|
||||
* 3) 显式 invalidate inactive slot(resume 不像 begin 那样自动做)
|
||||
* 预擦会在 begin 阶段占用数秒~数十秒,之后收流写 Flash 更快更稳。 */
|
||||
const size_t part_size = m_partition->size;
|
||||
const size_t sector = m_partition->erase_size ? m_partition->erase_size : 4096U;
|
||||
DEBUG_INFO(TAG, "整分区预擦: %s size=%u sector=%u free=%lu...",
|
||||
m_partition->label, (unsigned)part_size, (unsigned)sector,
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
const int64_t t0 = esp_timer_get_time();
|
||||
for (size_t off = 0; off < part_size; ) {
|
||||
size_t chunk = sector;
|
||||
if (off + chunk > part_size) {
|
||||
chunk = part_size - off;
|
||||
}
|
||||
esp_err_t erase_err = esp_partition_erase_range(m_partition, off, chunk);
|
||||
if (erase_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "整分区预擦失败 @%u: %s",
|
||||
(unsigned)off, esp_err_to_name(erase_err));
|
||||
return beginCleanupAndReturn(erase_err);
|
||||
}
|
||||
off += chunk;
|
||||
(void)esp_task_wdt_reset();
|
||||
taskYIELD();
|
||||
}
|
||||
const int64_t t1 = esp_timer_get_time();
|
||||
DEBUG_INFO(TAG, "整分区预擦完成 耗时 %lld ms",
|
||||
(long long)((t1 - t0) / 1000));
|
||||
|
||||
/* ★ 边写边擦(OTA_WITH_SEQUENTIAL_WRITES),不再整分区预擦:
|
||||
* 旧方案在 HTTP 只收到 JSON 头后同步擦 1.25MB,浏览器 TCP 窗口堵死、进度卡开头。
|
||||
* sequential:begin 立即返回,擦除摊到 esp_ota_write,与收流并行(对 WiFi 更友好)。 */
|
||||
#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) || defined(CONFIG_APP_ROLLBACK_ENABLE)
|
||||
{
|
||||
esp_err_t inv = esp_ota_invalidate_inactive_ota_data_slot();
|
||||
@@ -212,17 +185,16 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* erase_size 传 0(≠ OTA_WITH_SEQUENTIAL_WRITES)→ 写时不再擦扇区 */
|
||||
esp_err_t err = esp_ota_resume(m_partition, /*erase_size=*/0,
|
||||
/*image_offset=*/0, &m_handle);
|
||||
const int64_t t2 = esp_timer_get_time();
|
||||
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_resume 失败: %s (耗时 %lld ms)",
|
||||
esp_err_to_name(err), (long long)((t2 - t1) / 1000));
|
||||
DEBUG_ERR(TAG, "esp_ota_begin(SEQUENTIAL) 失败: %s (耗时 %lld ms)",
|
||||
esp_err_to_name(err), (long long)((t1 - t0) / 1000));
|
||||
return beginCleanupAndReturn(err);
|
||||
}
|
||||
DEBUG_INFO(TAG, "esp_ota_resume 完成 耗时 %lld ms free=%lu",
|
||||
(long long)((t2 - t1) / 1000),
|
||||
DEBUG_INFO(TAG, "esp_ota_begin(SEQUENTIAL) 完成 耗时 %lld ms free=%lu",
|
||||
(long long)((t1 - t0) / 1000),
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
|
||||
m_received = 0;
|
||||
@@ -231,7 +203,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 字节 (full-partition erase)",
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase-on-write)",
|
||||
m_partition->label, (unsigned)total_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* begin → write*(N) → finalize / abort
|
||||
*
|
||||
* 统一以 WiFi 流程为基准:
|
||||
* - begin:整分区预擦后 esp_ota_resume(写路径不再边写边擦)
|
||||
* - begin:esp_ota_begin(OTA_WITH_SEQUENTIAL_WRITES) 边写边擦(避免 HTTP 头卡预擦)
|
||||
* - SHA-256 在 finalize 时从 OTA 分区读回计算(不在上传期间增量计算)
|
||||
* - finalize 顺序: 读回 SHA-256 → 比对前端 SHA → esp_ota_end → esp_ota_set_boot_partition → 可选重启
|
||||
* - m_ended 标志位防止 esp_ota_end 成功后再次 esp_ota_abort 同一 handle
|
||||
|
||||
+84
-103
@@ -9,7 +9,6 @@
|
||||
#include "esp32_ota_core.h"
|
||||
#include "relay/ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "transport/spi_master.h"
|
||||
#include "server/ble_ota.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "esp_system.h"
|
||||
@@ -57,8 +56,8 @@ bool OtaCoordinator::lock() const
|
||||
if (!m_mutex) return false;
|
||||
bool ok = (xSemaphoreTake(m_mutex, kLockTimeout) == pdTRUE);
|
||||
if (!ok) {
|
||||
DEBUG_WARN(TAG, "lock() 超时 (%u ms),本次状态变更被跳过",
|
||||
(unsigned)pdTICKS_TO_MS(kLockTimeout));
|
||||
DEBUG_ERR(TAG, "lock() 超时 (%u ms),本次状态变更被跳过",
|
||||
(unsigned)pdTICKS_TO_MS(kLockTimeout));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -76,16 +75,16 @@ void OtaCoordinator::unlock(bool handle) const
|
||||
// ============================================================
|
||||
void OtaCoordinator::watchdogCheckLocked()
|
||||
{
|
||||
// 仅对"忙"状态(可能卡死的)做检查
|
||||
// 仅对"忙"状态(可能卡死的)做检查。
|
||||
// REBOOTING:已 set_boot,即将 esp_restart,不入狗,避免 abort 成 IDLE 与重启竞态。
|
||||
switch (m_sysState) {
|
||||
case OTA_SYS_GD32_STORING:
|
||||
case OTA_SYS_GD32_RELAYING:
|
||||
case OTA_SYS_ESP32_RECEIVING:
|
||||
case OTA_SYS_ESP32_FINALIZING:
|
||||
case OTA_SYS_ESP32_REBOOTING:
|
||||
break;
|
||||
default:
|
||||
return; // IDLE/STORED/COMPLETE/ERROR 无需检查
|
||||
return; // IDLE/STORED/COMPLETE/ERROR/REBOOTING 无需检查
|
||||
}
|
||||
|
||||
if (m_busySinceTick == 0) {
|
||||
@@ -162,9 +161,7 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
DEBUG_WARN(TAG, "GD32 存储仍在进行中(同通道),中止旧存储并重新开始");
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
doAbortLocked(); // 清 meta + IDLE,与 abort 路径一致
|
||||
// epoch 将在下方设置 GD32_STORING 后递增
|
||||
}
|
||||
|
||||
@@ -221,27 +218,17 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
|
||||
return err;
|
||||
}
|
||||
|
||||
// 3. 整分区预擦(不再边写边擦):prepare 阶段擦完 gd32_ota 分区,
|
||||
// 收流时 flashWriteChunk 仅 write,吞吐更稳。会阻塞数秒~数十秒。
|
||||
// 3. 复位擦除水位;不在 prepare 整区预擦。
|
||||
// 旧方案在 HTTP JSON 头后同步擦 gd32_ota,TCP 堵在开头。
|
||||
// 现由 flashWriteChunk → ensureErasedForWrite 按需擦扇区,与收流交织。
|
||||
Gd32FlashStore::instance().beginWriteSession();
|
||||
{
|
||||
const esp_partition_t *gd32_part = Gd32FlashStore::instance().getPartition();
|
||||
const uint32_t erase_bytes = gd32_part ? (uint32_t)gd32_part->size : 0U;
|
||||
if (erase_bytes == 0U) {
|
||||
DEBUG_ERR(TAG, "prepareGd32Storage: 找不到 gd32_ota 分区");
|
||||
abortCurrent();
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
DEBUG_INFO(TAG, "prepareGd32Storage: 整分区预擦 %u 字节 (fw=%" PRIu32 ")...",
|
||||
(unsigned)erase_bytes, total_size);
|
||||
err = Gd32FlashStore::instance().erasePartition(erase_bytes);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "prepareGd32Storage: 整分区预擦失败: %s", esp_err_to_name(err));
|
||||
abortCurrent();
|
||||
return err;
|
||||
}
|
||||
DEBUG_INFO(TAG, "prepareGd32Storage: 整分区预擦完成");
|
||||
if (Gd32FlashStore::instance().getPartition() == nullptr) {
|
||||
DEBUG_ERR(TAG, "prepareGd32Storage: 找不到 gd32_ota 分区");
|
||||
abortCurrent();
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
DEBUG_INFO(TAG, "prepareGd32Storage: 会话就绪 fw=%" PRIu32 "(按需擦除,无整区预擦)",
|
||||
total_size);
|
||||
|
||||
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
|
||||
clearGd32Meta();
|
||||
@@ -284,55 +271,39 @@ esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
|
||||
return finalizeGd32Checksum();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GD32 存储操作(传输层统一入口)
|
||||
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
|
||||
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// GD32 中继阶段
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::triggerGd32Relay()
|
||||
{
|
||||
bool h = lock();
|
||||
if (!h) {
|
||||
DEBUG_ERR(TAG, "triggerGd32Relay: lock 超时");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// ★ 卡死自愈检查
|
||||
if (h) watchdogCheckLocked();
|
||||
watchdogCheckLocked();
|
||||
|
||||
// 校验:必须处于"已存储"或"已中继完成"状态
|
||||
if (h && m_sysState != OTA_SYS_GD32_STORED && m_sysState != OTA_SYS_IDLE) {
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
DEBUG_WARN(TAG, "GD32 正在存储中,拒绝触发中继");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
DEBUG_WARN(TAG, "GD32 中继已在进行中");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 进行中,拒绝中继触发");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
/* 白名单:仅 STORED / IDLE(NVS 有完整固件)/ COMPLETE(允许再中继)可触发 */
|
||||
if (m_sysState != OTA_SYS_GD32_STORED &&
|
||||
m_sysState != OTA_SYS_IDLE &&
|
||||
m_sysState != OTA_SYS_GD32_COMPLETE) {
|
||||
DEBUG_WARN(TAG, "拒绝触发中继: 当前状态 %s (%d) 不在白名单",
|
||||
stateToString(m_sysState), (int)m_sysState);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 验证固件是否确实已存储(通过 NVS 元数据)
|
||||
ota_meta_t meta;
|
||||
if (h && (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK ||
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK ||
|
||||
meta.total_size == 0 ||
|
||||
meta.stored_size < meta.total_size)) {
|
||||
meta.stored_size < meta.total_size) {
|
||||
DEBUG_WARN(TAG, "GD32 固件未完整存储,拒绝触发中继");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 检查 OtaRelay 是否空闲
|
||||
if (h && OtaRelay::getState() != RELAY_IDLE &&
|
||||
if (OtaRelay::getState() != RELAY_IDLE &&
|
||||
OtaRelay::getState() != RELAY_DONE &&
|
||||
OtaRelay::getState() != RELAY_ERROR) {
|
||||
DEBUG_WARN(TAG, "OtaRelay 正忙 (state=%d),拒绝触发", (int)OtaRelay::getState());
|
||||
@@ -340,47 +311,42 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 向 OtaRelay task 发送启动消息
|
||||
QueueHandle_t dq = OtaRelay::getDispatchQueue();
|
||||
if (h && !dq) {
|
||||
if (!dq) {
|
||||
DEBUG_ERR(TAG, "OtaRelay dispatchQueue 未就绪");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (!h) {
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||||
/* 先入队再标 RELAYING:失败时状态与 CAN 反馈都不变 */
|
||||
pauseCanFeedbackTask(true);
|
||||
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||||
|
||||
uint32_t cmd = 1;
|
||||
BaseType_t ret = xQueueSend(dq, &cmd, pdMS_TO_TICKS(100));
|
||||
if (ret != pdTRUE) {
|
||||
DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败");
|
||||
m_sysState = OTA_SYS_GD32_STORED;
|
||||
m_busySinceTick = 0; // 回退到 STORED(非忙)
|
||||
pauseCanFeedbackTask(false);
|
||||
unlock(h);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount();
|
||||
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||||
unlock(h);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:GD32 中继已开始,更新系统状态为 GD32_RELAYING。 */
|
||||
/** @brief 中继 task 已真正开始:若 trigger 已标 RELAYING 则只补 pause;否则兜底设状态。 */
|
||||
void OtaCoordinator::notifyRelayStarted()
|
||||
{
|
||||
bool h = lock();
|
||||
if (h && m_sysState != OTA_SYS_GD32_RELAYING) {
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
|
||||
if (h) {
|
||||
if (m_sysState != OTA_SYS_GD32_RELAYING) {
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount();
|
||||
DEBUG_INFO(TAG, "中继已开始(兜底), 系统状态 → GD32_RELAYING");
|
||||
}
|
||||
pauseCanFeedbackTask(true);
|
||||
DEBUG_INFO(TAG, "中继已开始, 系统状态 → GD32_RELAYING");
|
||||
}
|
||||
unlock(h);
|
||||
}
|
||||
@@ -466,9 +432,7 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 仍在接收中(同通道),中止旧会话并重新开始");
|
||||
Esp32OtaCore::instance().abort();
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
doAbortLocked(); // abort core + IDLE + 恢复 CAN 反馈
|
||||
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
|
||||
}
|
||||
|
||||
@@ -558,6 +522,12 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
|
||||
void OtaCoordinator::setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg)
|
||||
{
|
||||
m_sysState = s;
|
||||
/* REBOOTING 不参与 busy watchdog,清零起点避免误中止 */
|
||||
if (s == OTA_SYS_ESP32_REBOOTING) {
|
||||
m_busySinceTick = 0;
|
||||
} else if (s == OTA_SYS_ESP32_FINALIZING && m_busySinceTick == 0) {
|
||||
m_busySinceTick = xTaskGetTickCount();
|
||||
}
|
||||
DEBUG_INFO(TAG, "%s", dbg_msg);
|
||||
}
|
||||
|
||||
@@ -667,12 +637,24 @@ void OtaCoordinator::healOrphanStates()
|
||||
bool need_resume = false;
|
||||
bool h = lock();
|
||||
if (h) {
|
||||
watchdogCheckLocked();
|
||||
ota_system_state_t before = m_sysState;
|
||||
watchdogCheckLocked(); // 超时则 doAbortLocked → IDLE
|
||||
// finalize 失败后 core 已空、协调器仍 FINALIZING 的假占用:status 轮询即可自愈
|
||||
if (m_sysState == OTA_SYS_ESP32_FINALIZING &&
|
||||
!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "status 路径发现 ESP32_FINALIZING 残留,自愈 → IDLE");
|
||||
doAbortLocked();
|
||||
}
|
||||
/* ERROR 非忙:status 轮询软清到 IDLE,避免 UI 长期卡 ERROR(新 begin 本也可覆盖) */
|
||||
if (m_sysState == OTA_SYS_ERROR) {
|
||||
DEBUG_INFO(TAG, "status 路径 ERROR → IDLE(软清)");
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
m_gd32StorageDone = false;
|
||||
pauseCanFeedbackTask(false);
|
||||
}
|
||||
// 任意自愈到 IDLE:恢复 WiFi OTA 期间挂起的 BLE(watchdog 旧路径漏 resume)
|
||||
if (before != OTA_SYS_IDLE && m_sysState == OTA_SYS_IDLE) {
|
||||
need_resume = true;
|
||||
}
|
||||
unlock(h);
|
||||
@@ -806,28 +788,27 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
||||
// ============================================================
|
||||
// GD32 APP 探测
|
||||
// ============================================================
|
||||
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
|
||||
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
|
||||
// 故此处保留独立实现。
|
||||
esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime)
|
||||
{
|
||||
/* 统一走 OtaRelay 公开探测,避免与 probeAndCacheAppAlive 双份逻辑 */
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ver = {};
|
||||
|
||||
esp_err_t err = SpiMaster::instance().appPing(500, &uptime, &ver);
|
||||
if (err == ESP_OK) {
|
||||
OtaRelay::setGd32AppAlive(true);
|
||||
OtaRelay::setGd32Version(ver);
|
||||
if (out_version) *out_version = ver;
|
||||
if (out_uptime) *out_uptime = uptime;
|
||||
if (OtaRelay::probeAndCacheAppAlive(500, &uptime)) {
|
||||
if (out_version) {
|
||||
*out_version = OtaRelay::getSnapshot().version;
|
||||
}
|
||||
if (out_uptime) {
|
||||
*out_uptime = uptime;
|
||||
}
|
||||
DEBUG_INFO(TAG, "GD32 APP 探测成功: alive, uptime=%" PRIu32 "s", uptime);
|
||||
} else {
|
||||
OtaRelay::setGd32AppAlive(false);
|
||||
if (out_version) *out_version = ver;
|
||||
if (out_uptime) *out_uptime = 0;
|
||||
DEBUG_WARN(TAG, "GD32 APP 探测失败: %s", esp_err_to_name(err));
|
||||
return ESP_OK;
|
||||
}
|
||||
return err;
|
||||
if (out_version) {
|
||||
*out_version = gd32_version_t{};
|
||||
}
|
||||
if (out_uptime) {
|
||||
*out_uptime = 0;
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -835,12 +816,12 @@ esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t
|
||||
// ============================================================
|
||||
void OtaCoordinator::reset()
|
||||
{
|
||||
// ★ 与 abortCurrent 共用 doAbortLocked:清 Esp32OtaCore / relay / meta,勿只改枚举
|
||||
bool h = lock();
|
||||
if (h) {
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0; // ★ 复位
|
||||
DEBUG_INFO(TAG, "协调器已完全复位");
|
||||
doAbortLocked();
|
||||
DEBUG_INFO(TAG, "协调器已完全复位(含子系统 abort)");
|
||||
}
|
||||
unlock(h);
|
||||
wifiOtaResumeBleIfNeeded();
|
||||
}
|
||||
|
||||
+11
-11
@@ -124,7 +124,8 @@ public:
|
||||
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 整分区预擦 + 清空 meta + 写入初始 meta。
|
||||
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 复位擦除水位 + 清空 meta + 写入初始 meta。
|
||||
* 扇区擦除由 flashWriteChunk/ensureErasedForWrite 按需进行(避免 HTTP 头卡整区预擦)。
|
||||
* 收流时 write 路径只写 Flash(ensureErased 仅作水位兜底)。
|
||||
* WiFi/BLE 上传入口共用,任意步骤失败时内部 abortCurrent,调用方无需再清理。
|
||||
* @param total_size 固件总字节数。
|
||||
@@ -337,9 +338,9 @@ public:
|
||||
|
||||
/**
|
||||
* @brief 自愈孤儿忙态(供 /system/status 等轮询路径调用)。
|
||||
* - 跑忙状态 watchdog(超时完整 abort → IDLE)
|
||||
* - ESP32_FINALIZING 但 Esp32OtaCore 已空闲:判定为 SHA 失败等残留,复位 IDLE
|
||||
* 不影响真正正在 finalize(core.isActive())的会话。
|
||||
* - 忙状态 watchdog(超时完整 abort → IDLE;不含 REBOOTING)
|
||||
* - ESP32_FINALIZING 但 core 已空闲:残留 → IDLE
|
||||
* - ERROR → IDLE 软清,避免 UI 长期卡错误态
|
||||
*/
|
||||
void healOrphanStates();
|
||||
|
||||
@@ -476,9 +477,7 @@ private:
|
||||
bool lock() const;
|
||||
void unlock(bool handle) const;
|
||||
|
||||
// ★ 卡死自愈检查:若忙状态停留超过 watchdog 超时,强制转为 ERROR。
|
||||
// 用于在中继 task 因任何原因(崩溃/SPI 死锁/看门狗复位残留)未能回调
|
||||
// notifyRelayComplete/notifyEsp32*Complete 时,让协调器自动脱离卡死状态。
|
||||
// ★ 卡死自愈检查:忙状态超时则 doAbortLocked → IDLE(不含 REBOOTING)。
|
||||
// 必须在已持锁状态下调用。
|
||||
void watchdogCheckLocked();
|
||||
|
||||
@@ -513,11 +512,12 @@ private:
|
||||
|
||||
// BLE 共存监听器(由 WifiOta 实现并注册)
|
||||
IBleCoexListener *m_bleCoexListener = nullptr;
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100);
|
||||
static constexpr TickType_t kLockTimeout =
|
||||
pdMS_TO_TICKS(OTA_COORD_LOCK_TIMEOUT_MS);
|
||||
|
||||
// ★ 忙状态 watchdog 超时。GD32 中继理论最大耗时 ≈ 60s(全局超时) + 15s(APP存活轮询) + 1s(bootloader) ≈ 76s,
|
||||
// 取 90 s,正常流程远不会触及,仅在 task 卡死时触发自愈。
|
||||
static constexpr TickType_t kBusyWatchdogTicks = pdMS_TO_TICKS(90U * 1000U);
|
||||
// ★ 忙状态 watchdog:须覆盖最长中继预算;数值见 app_config.h
|
||||
static constexpr TickType_t kBusyWatchdogTicks =
|
||||
pdMS_TO_TICKS(OTA_COORD_BUSY_WATCHDOG_MS);
|
||||
};
|
||||
|
||||
#endif // OTA_COORDINATOR_H
|
||||
|
||||
+222
-68
@@ -49,8 +49,8 @@ static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
|
||||
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
|
||||
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
||||
static constexpr uint32_t kLastPacketAckSeq = 0xF0F0F0F0U; // 末包 ACK 序号约定
|
||||
// 10 秒无成功帧停刷:跨包级别的背压/卡死检测,比单包 15s 超时更早触发。
|
||||
static constexpr uint32_t kStallTimeoutMs = 8000U;
|
||||
// 10 秒无成功帧停刷:跨包级别的背压/卡死检测,比单包 kAckWaitMs 更早触发。
|
||||
static constexpr uint32_t kStallTimeoutMs = 10000U;
|
||||
|
||||
// ============================================================
|
||||
// 静态成员定义
|
||||
@@ -315,7 +315,6 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
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);
|
||||
@@ -452,13 +451,29 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
/* 中继全程禁止 worker 把 OTA ACK/WAIT 当 EVENT 偷读 */
|
||||
SpiMaster::instance().setSuppressRingService(true);
|
||||
|
||||
// 固件完整性校验
|
||||
/* 完整性门控(避免与 /ota/relay·BLE 触发前的 verifyGd32Firmware 二次全盘 SHA):
|
||||
* - meta 必须完整 stored≥total
|
||||
* - checksum 非全零:触发路径已 finalize+比对,此处信任 meta,跳过再扫 Flash
|
||||
* - checksum 全零:兜底做 metaVerifyChecksum(或 finalize 后再比) */
|
||||
if (meta.total_size == 0U || meta.stored_size < meta.total_size) {
|
||||
DEBUG_ERR("OTA", "固件未完整存储 stored=%" PRIu32 " total=%" PRIu32,
|
||||
meta.stored_size, meta.total_size);
|
||||
reset();
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
{
|
||||
esp_err_t verify_err = Gd32FlashStore::instance().metaVerifyChecksum();
|
||||
if (verify_err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "固件完整性校验失败,中止转发");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_CRC;
|
||||
static const uint8_t kZeroSha[32] = {0};
|
||||
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
|
||||
DEBUG_WARN("OTA", "meta.checksum 为空,中继前补做全盘 SHA 校验");
|
||||
esp_err_t verify_err = Gd32FlashStore::instance().metaVerifyChecksum();
|
||||
if (verify_err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "固件完整性校验失败,中止转发");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_CRC;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("OTA", "跳过二次全盘 SHA(meta.checksum 已就绪,%" PRIu32 " B)",
|
||||
meta.total_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,11 +511,22 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
// ============================================================
|
||||
void OtaRelay::clearSessionCryptoState()
|
||||
{
|
||||
/* 抗优化清零:避免编译器去掉「无读回」的敏感缓冲擦除 */
|
||||
auto wipe = [](void *p, size_t n) {
|
||||
if (p == nullptr || n == 0U) {
|
||||
return;
|
||||
}
|
||||
volatile uint8_t *v = static_cast<volatile uint8_t *>(p);
|
||||
while (n > 0U) {
|
||||
*v++ = 0;
|
||||
--n;
|
||||
}
|
||||
};
|
||||
Protocol::sessionKeyClear(&m_sessionCtx);
|
||||
memset(&m_sessionCtx, 0, sizeof(session_ctx_t));
|
||||
memset(m_kSession, 0, sizeof(m_kSession));
|
||||
memset(m_nonceServer, 0, sizeof(m_nonceServer));
|
||||
memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime));
|
||||
wipe(&m_sessionCtx, sizeof(m_sessionCtx));
|
||||
wipe(m_kSession, sizeof(m_kSession));
|
||||
wipe(m_nonceServer, sizeof(m_nonceServer));
|
||||
wipe(m_gd32UidRuntime, sizeof(m_gd32UidRuntime));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -515,7 +541,6 @@ void OtaRelay::reset()
|
||||
m_transferTotalPackets = 0;
|
||||
m_transferCurrentSeq = 0;
|
||||
m_transferFlashOffset = 0;
|
||||
m_transferRetryCount = 0;
|
||||
m_transferDeadline = 0;
|
||||
clearSessionCryptoState();
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
@@ -567,10 +592,19 @@ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in,
|
||||
*/
|
||||
void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len)
|
||||
{
|
||||
/* 默认 logLevel=INFO 时 DBG 不输出;勿先建 hex[193] 白占 ota_relay 栈 */
|
||||
if (DebugLog::instance().getLogLevel() < DEBUG_LOG_DBG) {
|
||||
return;
|
||||
}
|
||||
char hex[193] = {0};
|
||||
size_t n = (len > 32U) ? 32U : len;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]);
|
||||
if (payload) {
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]);
|
||||
}
|
||||
} else {
|
||||
n = 0;
|
||||
}
|
||||
DEBUG_DBG("OTA", "%s resp: type=0x%02X len=%u payload[%u]=%s%s",
|
||||
stage, resp_type, (unsigned)len, (unsigned)n,
|
||||
n > 0 ? hex : "<empty>", len > n ? "..." : "");
|
||||
@@ -763,8 +797,23 @@ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_
|
||||
*/
|
||||
bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (aes128EcbEncryptBlockLocal(key, in + i * 16, out + i * 16) != 0) return false;
|
||||
/* 单次 init/setkey 加密 3 块,避免每块重建 AES 上下文(转发热路径) */
|
||||
if (!key || !in || !out) {
|
||||
return false;
|
||||
}
|
||||
esp_aes_context ctx;
|
||||
esp_aes_init(&ctx);
|
||||
if (esp_aes_setkey(&ctx, key, 128) != 0) {
|
||||
esp_aes_free(&ctx);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (esp_aes_crypt_ecb(&ctx, ESP_AES_ENCRYPT, in + i * 16, out + i * 16) != 0) {
|
||||
esp_aes_free(&ctx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
esp_aes_free(&ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1011,22 +1060,34 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 协议子步骤:逐包转发固件数据给 GD32。
|
||||
* 流程:从 Flash 读取 → AES 加密 → SPI 发送 → 等待 ACK → 失败则重发。
|
||||
* 每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包执行一次健康检查。
|
||||
* @brief 准备单包:读 Flash → AES → 填 64B tx_buf。
|
||||
* flash_offset 显式传入,便于在等 ACK 时预组下一包(不推进 m_transferFlashOffset)。
|
||||
*/
|
||||
ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, uint8_t tx_buf[64], size_t *out_chunk_size)
|
||||
ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, uint32_t flash_offset, 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);
|
||||
if (!tx_buf) {
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
size_t chunk_size = GD32_OTA_PACKET_SIZE;
|
||||
if (seq + 1U >= m_transferTotalPackets) {
|
||||
const uint32_t done = seq * GD32_OTA_PACKET_SIZE;
|
||||
if (done >= meta.total_size) {
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
chunk_size = meta.total_size - done;
|
||||
}
|
||||
|
||||
uint8_t plain48[GD32_OTA_PACKET_SIZE];
|
||||
/* 仅末包需要 0xFF 填充;满包直接读满 48B */
|
||||
if (chunk_size < GD32_OTA_PACKET_SIZE) {
|
||||
memset(plain48, 0xFF, sizeof(plain48));
|
||||
}
|
||||
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(flash_offset, plain48, chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", flash_offset, err);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
@@ -1042,7 +1103,7 @@ ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta,
|
||||
return OTA_RESULT_ERR_AUTH;
|
||||
}
|
||||
|
||||
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
|
||||
const uint32_t if_end = (seq + 1U >= m_transferTotalPackets) ? 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);
|
||||
@@ -1053,12 +1114,15 @@ ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta,
|
||||
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;
|
||||
/* 不在此写 m_transferCurrentSeq:预组下一包时不应污染“当前发送序号” */
|
||||
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)
|
||||
bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size,
|
||||
ota_result_t *out_result, NextPacketPrep *next_prep)
|
||||
{
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[32] = {0};
|
||||
@@ -1069,18 +1133,37 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
* - GD32 ISR 收 DATA 后立刻回 WAIT,主循环 AES/Flash 后再 ACK。
|
||||
* - 一旦见过 WAIT,说明包已进从机;再发 DATA = DUP-SEQ,只会反复 WAIT。
|
||||
* - 因此:见 WAIT 后只 poll;仅「完全无响应」才允许重发 DATA。
|
||||
* - 等 ACK 期间可预组下一包(与从机处理重叠)。
|
||||
*/
|
||||
constexpr uint32_t kMaxDataSends = 3U;
|
||||
constexpr uint32_t kAckWaitMs = 6000U; /* 单次发送后等 ACK 上限(覆盖 GD32 增量 drain) */
|
||||
constexpr uint32_t kPollSliceMs = 80U;
|
||||
constexpr uint32_t kPollSliceMs = 50U; /* 略紧:DATA_READY 驱动,缩短空切片 */
|
||||
|
||||
TickType_t pkt_start = xTaskGetTickCount();
|
||||
bool saw_wait = false;
|
||||
|
||||
auto maybe_prep_next = [&]() {
|
||||
if (!next_prep || !next_prep->ready || *next_prep->ready) {
|
||||
return;
|
||||
}
|
||||
if (!next_prep->meta || !next_prep->buf || !next_prep->chunk_size) {
|
||||
return;
|
||||
}
|
||||
if (next_prep->next_seq >= m_transferTotalPackets) {
|
||||
return;
|
||||
}
|
||||
ota_result_t pr = prepareRelayPacket(next_prep->next_seq, next_prep->next_flash_offset,
|
||||
*next_prep->meta, next_prep->buf, next_prep->chunk_size);
|
||||
if (pr == OTA_RESULT_OK) {
|
||||
*next_prep->ready = true;
|
||||
}
|
||||
/* 预组失败不致命:外层会再 prepare */
|
||||
};
|
||||
|
||||
for (uint32_t send_i = 0; send_i < kMaxDataSends; ++send_i) {
|
||||
/* 已见 WAIT:禁止再发 DATA,只延长 poll */
|
||||
if (saw_wait && send_i > 0U) {
|
||||
DEBUG_INFO("OTA", "包 %" PRIu32 " 已见 WAIT,继续 poll 不重发", seq);
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 已见 WAIT,继续 poll 不重发", seq);
|
||||
} else {
|
||||
resp_len = 0;
|
||||
resp_type = 0;
|
||||
@@ -1090,6 +1173,8 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
saw_wait = true;
|
||||
/* 从机已收下本包,立刻利用等待时间预组下一包 */
|
||||
maybe_prep_next();
|
||||
}
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK &&
|
||||
isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
@@ -1109,6 +1194,9 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
last_hb = xTaskGetTickCount();
|
||||
}
|
||||
|
||||
/* 优先在 poll 间隙预组,再读 SPI(READY 未拉时也能吃掉 Flash/AES 时间) */
|
||||
maybe_prep_next();
|
||||
|
||||
esp_err_t err = SpiMaster::instance().otaPollResponse(
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, kPollSliceMs);
|
||||
|
||||
@@ -1132,6 +1220,10 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
return true;
|
||||
}
|
||||
saw_wait = false; /* 允许带错误码的重发 */
|
||||
/* 结构化错误后下一包内容可能变化,丢弃预组 */
|
||||
if (next_prep && next_prep->ready) {
|
||||
*next_prep->ready = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* 陈旧 ACK,继续 poll */
|
||||
@@ -1143,7 +1235,6 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
if (send_i + 1U >= kMaxDataSends) {
|
||||
break;
|
||||
}
|
||||
/* 把下一轮当作「只 poll」:send_i 增加但 saw_wait 使跳过 transfer */
|
||||
continue;
|
||||
}
|
||||
if (send_i + 1U < kMaxDataSends) {
|
||||
@@ -1170,13 +1261,28 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
uint8_t tx_buf[64] = {0};
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
/* 双缓冲:当前发送槽 + 预组槽(等 ACK 时填充) */
|
||||
uint8_t tx_slot[2][64] = {};
|
||||
size_t chunk_slot[2] = {0, 0};
|
||||
int cur = 0;
|
||||
bool next_ready = false;
|
||||
size_t next_chunk = 0;
|
||||
|
||||
uint32_t flash_off = 0;
|
||||
m_transferFlashOffset = 0;
|
||||
|
||||
ota_result_t result = prepareRelayPacket(0, flash_off, meta, tx_slot[cur], &chunk_slot[cur]);
|
||||
if (result != OTA_RESULT_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
TickType_t last_success_tick = xTaskGetTickCount();
|
||||
/* 进度:首包 / 每 8 包 / 末包更新,减 status 锁竞争 */
|
||||
constexpr uint32_t kSeqReportMask = 7U;
|
||||
|
||||
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
|
||||
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
|
||||
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == m_transferTotalPackets - 1) {
|
||||
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 ||
|
||||
seq == 0 || seq + 1U >= m_transferTotalPackets) {
|
||||
health_status_t health = HealthMonitor::instance().checkBeforeOtaRelay();
|
||||
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
|
||||
DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
|
||||
@@ -1184,38 +1290,65 @@ 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;
|
||||
}
|
||||
|
||||
// 停刷检测:自上一包成功 ACK 起超过 kStallTimeoutMs 则中止
|
||||
if ((xTaskGetTickCount() - last_success_tick) >= pdMS_TO_TICKS(kStallTimeoutMs)) {
|
||||
DEBUG_ERR("OTA", "10 秒无成功帧, 中止转发: seq=%" PRIu32 "/%" PRIu32, seq, m_transferTotalPackets);
|
||||
DEBUG_ERR("OTA", "全局超时: 包 %" PRIu32 "/%" PRIu32 " 已超限,中止转发",
|
||||
seq, m_transferTotalPackets);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
size_t chunk_size = 0;
|
||||
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
|
||||
if (result != OTA_RESULT_OK) {
|
||||
|
||||
if ((xTaskGetTickCount() - last_success_tick) >= pdMS_TO_TICKS(kStallTimeoutMs)) {
|
||||
DEBUG_ERR("OTA", "%" PRIu32 " ms 无成功帧, 中止转发: seq=%" PRIu32 "/%" PRIu32,
|
||||
kStallTimeoutMs, seq, m_transferTotalPackets);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
/* 当前包必须已在 cur 槽;若预组未命中则现场准备 */
|
||||
if (seq > 0U) {
|
||||
if (next_ready) {
|
||||
cur ^= 1;
|
||||
chunk_slot[cur] = next_chunk;
|
||||
next_ready = false;
|
||||
} else {
|
||||
result = prepareRelayPacket(seq, flash_off, meta, tx_slot[cur], &chunk_slot[cur]);
|
||||
if (result != OTA_RESULT_OK) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_transferCurrentSeq = seq;
|
||||
if ((seq & kSeqReportMask) == 0U || seq + 1U >= m_transferTotalPackets) {
|
||||
setRelaySeq(seq);
|
||||
}
|
||||
|
||||
NextPacketPrep prep{};
|
||||
const int prep_idx = cur ^ 1;
|
||||
prep.next_seq = seq + 1U;
|
||||
prep.next_flash_offset = flash_off + (uint32_t)chunk_slot[cur];
|
||||
prep.meta = &meta;
|
||||
prep.buf = tx_slot[prep_idx];
|
||||
prep.chunk_size = &next_chunk;
|
||||
prep.ready = &next_ready;
|
||||
NextPacketPrep *prep_arg = (seq + 1U < m_transferTotalPackets) ? &prep : nullptr;
|
||||
|
||||
if (sendRelayPacketWithRetry(seq, tx_slot[cur], chunk_slot[cur], &result, prep_arg)) {
|
||||
DEBUG_ERR("OTA", "中止于包 %" PRIu32 "/%" PRIu32 " result=%d",
|
||||
seq, m_transferTotalPackets, (int)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 先写入「正在发第 N 包」,前端 /system/status 能看到卡在哪 */
|
||||
setRelaySeq(seq);
|
||||
flash_off += (uint32_t)chunk_slot[cur];
|
||||
/* 与 sendRelayPacketWithRetry 内累加对齐(绝对赋值,防漂移) */
|
||||
m_transferFlashOffset = flash_off;
|
||||
|
||||
// 发送单包并等待 ACK(内含重试循环)
|
||||
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
|
||||
DEBUG_ERR("OTA", "中止于包 %" PRIu32 "/%" PRIu32 " result=%d",
|
||||
seq, m_transferTotalPackets, (int)result);
|
||||
return result; // 中止
|
||||
if ((seq & kSeqReportMask) == 0U || seq + 1U >= m_transferTotalPackets) {
|
||||
setRelaySeq(seq + 1U);
|
||||
}
|
||||
|
||||
setRelaySeq(seq + 1);
|
||||
last_success_tick = xTaskGetTickCount();
|
||||
}
|
||||
setRelaySeq(m_transferTotalPackets);
|
||||
DEBUG_INFO("OTA", "全部数据包已转发: %" PRIu32 " 包", m_transferTotalPackets);
|
||||
return result;
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1398,12 +1531,31 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 启动时初始 GD32 探测:读取 GD32 APP 版本号与存活状态
|
||||
// GD32 APP 启动需要一定时间,给其 3 秒准备窗口后发起探测
|
||||
/* 启动 GD32 探测(原 main.cpp 与此处双份;现统一在本任务):
|
||||
* APP 完整 init 约 5–6s;先 hello 建 v2 session,再 appPing。 */
|
||||
{
|
||||
DEBUG_INFO("OTA", "启动初始 GD32 探测...");
|
||||
vTaskDelay(pdMS_TO_TICKS(3000)); // 等待 GD32 APP 就绪
|
||||
probeAndCacheAppAlive(2000);
|
||||
DEBUG_INFO("OTA", "启动初始 GD32 探测 (hello+probe)...");
|
||||
bool session_ok = false;
|
||||
bool alive = false;
|
||||
for (uint32_t attempt = 0; attempt < 20U; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
if (!session_ok) {
|
||||
if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK) {
|
||||
session_ok = true;
|
||||
} else {
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP... (%" PRIu32 "/20)", attempt + 1U);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (probeAndCacheAppAlive(2000)) {
|
||||
alive = true;
|
||||
break;
|
||||
}
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/20)", attempt + 1U);
|
||||
}
|
||||
if (!alive) {
|
||||
DEBUG_WARN("OTA", "启动 GD32 探测未成功(中继前仍可再 hello)");
|
||||
}
|
||||
}
|
||||
|
||||
while (1) {
|
||||
@@ -1425,10 +1577,12 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
if (result == OTA_RESULT_OK) {
|
||||
// 成功日志已在 doWaitForAppAlive 输出,此处不再重复
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
} else if (result == OTA_RESULT_ERR_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "OTA 超时中止 (TIMEOUT=%d,含全局预算/停刷/单包)", (int)result);
|
||||
} else if (result == OTA_RESULT_ERR_USER_ABORT) {
|
||||
DEBUG_ERR("OTA", "OTA 全局超时中止 (OTA_RESULT_ERR_USER_ABORT=%d)", result);
|
||||
DEBUG_ERR("OTA", "OTA 用户中止 (USER_ABORT=%d)", (int)result);
|
||||
} else {
|
||||
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
||||
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", (int)result);
|
||||
}
|
||||
instance().reset();
|
||||
// 通过 IRelayListener 接口回调通知协调器中继结束
|
||||
|
||||
+27
-15
@@ -207,6 +207,14 @@ public:
|
||||
*/
|
||||
static QueueHandle_t getResultQueue();
|
||||
|
||||
/**
|
||||
* @brief 探测 GD32 APP 存活并缓存版本(公开,供 OtaCoordinator / 启动探测共用)。
|
||||
* @param timeout_ms appPing 超时(毫秒)。
|
||||
* @param out_uptime 可选,输出 GD32 uptime(秒)。
|
||||
* @return true 探测成功,false 失败。
|
||||
*/
|
||||
static bool probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime = nullptr);
|
||||
|
||||
// ── 线程安全锁 ──
|
||||
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
||||
@@ -320,26 +328,39 @@ private:
|
||||
/**
|
||||
* @brief 准备单包:读 Flash + 组装 tx_buf + 加密 + CRC(doRelayPackets 子步骤)。
|
||||
* @param seq 当前包序号。
|
||||
* @param flash_offset 本包在 gd32_ota 分区中的起始偏移(不依赖 m_transferFlashOffset,便于预组下一包)。
|
||||
* @param meta 固件元数据(用于计算末包 chunk_size)。
|
||||
* @param tx_buf 输出缓冲区(64 字节)。
|
||||
* @param out_chunk_size 输出本包有效字节数(用于 sendRelayPacketWithRetry 推进 offset)。
|
||||
* @param out_chunk_size 输出本包有效字节数。
|
||||
* @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);
|
||||
ota_result_t prepareRelayPacket(uint32_t seq, uint32_t flash_offset, const ota_meta_t &meta,
|
||||
uint8_t tx_buf[64], size_t *out_chunk_size);
|
||||
|
||||
/** 等 ACK 期间可选预组下一包(与 GD32 处理重叠,隐藏本端 Flash/AES 延迟)。 */
|
||||
struct NextPacketPrep {
|
||||
uint32_t next_seq = 0;
|
||||
uint32_t next_flash_offset = 0;
|
||||
const ota_meta_t *meta = nullptr;
|
||||
uint8_t *buf = nullptr; ///< 64 字节
|
||||
size_t *chunk_size = nullptr;
|
||||
bool *ready = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 发送单包并等待 ACK(doRelayPackets 子步骤)。
|
||||
*
|
||||
* 内含重试循环,使用以下命名常量:
|
||||
* kMaxResendOnNack=10(ACK 失败/轮询耗尽重发上限)
|
||||
* kMaxResendOnStructError=3(结构化错误重发上限)
|
||||
* 见 WAIT 后只 poll 不重发 DATA。可选 next_prep:在等 ACK 的 poll 间隙预组下一包。
|
||||
*
|
||||
* @param seq 当前包序号。
|
||||
* @param tx_buf 已组装好的 64 字节发送缓冲区。
|
||||
* @param chunk_size 本包有效字节数(成功时用于推进 m_transferFlashOffset)。
|
||||
* @param[out] out_result 失败时写入错误码(OK 时不修改)。
|
||||
* @param next_prep 可选;非空则在 poll 循环内至多预组一次下一包。
|
||||
* @return true 应中止外层循环(result 已设);false 继续下一包。
|
||||
*/
|
||||
bool sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size, ota_result_t *out_result);
|
||||
bool sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size,
|
||||
ota_result_t *out_result, NextPacketPrep *next_prep = nullptr);
|
||||
/**
|
||||
* @brief 发送结束指令并验证 APP 存活(顺序调用 doSendFinishOta + doWaitForAppAlive).
|
||||
* @return OTA_RESULT_OK 成功,负值表示错误。
|
||||
@@ -361,20 +382,11 @@ private:
|
||||
uint32_t m_transferTotalPackets = 0;
|
||||
uint32_t m_transferCurrentSeq = 0;
|
||||
uint32_t m_transferFlashOffset = 0;
|
||||
uint32_t m_transferRetryCount = 0;
|
||||
TickType_t m_transferDeadline = 0;
|
||||
|
||||
// ── 加密/工具方法(不变)──
|
||||
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
|
||||
/**
|
||||
* @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,
|
||||
|
||||
+17
-14
@@ -57,17 +57,12 @@
|
||||
#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 被其他通道占用
|
||||
#define BLE_OTA_FINISH_ERR_MUTEX 0x83 // 互斥拒绝: OTA 被其他通道占用
|
||||
#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小
|
||||
#define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85 // OTA SHA-256 不匹配
|
||||
#define BLE_OTA_FINISH_ERR_GD32_RELAY 0x86 // GD32 SPI OTA 转发失败
|
||||
|
||||
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
|
||||
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
|
||||
// 深度略大于窗口:seq=0 整分区预擦期间前端若仍误发,可多扛几批,减少“队列满丢包→重传风暴”。
|
||||
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW * 2 + 8)
|
||||
// BLE_OTA_ACK_WINDOW / BLE_OTA_DATA_QUEUE_LEN / BLE_OTA_REORDER_WINDOW → app_config.h
|
||||
|
||||
struct BleOtaDataBatch {
|
||||
uint8_t *data;
|
||||
@@ -105,6 +100,8 @@ static uint32_t packBleFinishValue(uint16_t session_id, uint16_t code);
|
||||
static void shaBytesToHexUpper(const uint8_t sha[32], char out_hex[65]);
|
||||
static void sendSha256Notify(const uint8_t sha[32], uint16_t session_id);
|
||||
static void esp32_reorder_free();
|
||||
static void ble_ota_session_reset(void);
|
||||
static void ble_ota_restore_wifi_ps(void);
|
||||
static void ble_ota_data_worker_task(void *pvParameters);
|
||||
static void ble_gd32_relay_wait_task(void *pvParameters);
|
||||
|
||||
@@ -182,7 +179,9 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 1));
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
esp32_reorder_free();
|
||||
// 无效会话:协调器可能仍假占用,走统一会话复位
|
||||
ble_ota_session_reset();
|
||||
ble_ota_restore_wifi_ps();
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
@@ -217,18 +216,22 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, packBleFinishValue(session_id, 0));
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// ★ finalize 内部已 abort 并释放 mutex,这里补充通知协调器复位状态机,
|
||||
// 避免 m_sysState 卡在 ESP32_FINALIZING 导致下次 OTA 被假拒绝。
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
// ★ finalizeForReboot 失败路径已 abortCurrent(协调器→IDLE + core 释放)。
|
||||
// 此处只清 BLE 会话侧 reorder,避免二次 abort 干扰新会话。
|
||||
esp32_reorder_free();
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
BleOta::instance().m_esp32LastCmd = 0;
|
||||
ble_ota_restore_wifi_ps();
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "[FINISH] OTA 收尾失败: %s", esp_err_to_name(fr.err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 1));
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// ★ 同上:finalize 已 abort core,这里通知协调器复位状态机
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
// ★ 同上:协调器已由 finalizeForReboot 复位,仅清 BLE 侧残留
|
||||
esp32_reorder_free();
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
BleOta::instance().m_esp32LastCmd = 0;
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
|
||||
vTaskDelete(NULL);
|
||||
@@ -1222,7 +1225,7 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
|
||||
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
|
||||
//
|
||||
// ★ seq=0 强制 ACK:begin 含整分区预擦,可能耗时数秒~数十秒。
|
||||
// ★ seq=0 强制 ACK:begin 后首包需尽快 ACK,前端才能继续窗口。
|
||||
// 前端必须等“首包已落盘”ACK 后再发窗口内其余包,否则队列满丢包→重传极慢。
|
||||
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
|
||||
BLE_OTA_CHUNK_PAYLOAD;
|
||||
@@ -1390,7 +1393,7 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = seq;
|
||||
const bool transfer_complete = stored_size >= meta.total_size;
|
||||
// seq=0:prepare 含整分区预擦,写完首包必须 ACK,前端才能继续发后续窗口
|
||||
// seq=0:prepare 后写完首包必须 ACK,前端才能继续发后续窗口
|
||||
const bool checkpoint =
|
||||
(seq == 0U) ||
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
|
||||
|
||||
+216
-57
@@ -53,6 +53,36 @@ static void bytesToHexUpper(const uint8_t *in, size_t n, char *out)
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 统一 JSON 错误响应(避免前端 resp.json() 被 httpd 明文坑)。
|
||||
* @param http_status 如 "400 Bad Request" / "403 Forbidden" / "500 Internal Server Error"
|
||||
* @param code 机器可读错误码(snake_case)
|
||||
* @param message 人类可读说明(勿含未转义双引号)
|
||||
*/
|
||||
static esp_err_t http_send_json_error(httpd_req_t *req, const char *http_status,
|
||||
const char *code, const char *message)
|
||||
{
|
||||
char body[320];
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"status\":\"error\",\"code\":\"%s\",\"message\":\"%s\"}",
|
||||
code ? code : "error",
|
||||
message ? message : "error");
|
||||
httpd_resp_set_status(req, http_status ? http_status : "500 Internal Server Error");
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, body, HTTPD_RESP_USE_STRLEN);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/** @brief OTA 收流:堆紧时让 LWIP 回收,再 httpd_req_recv。 */
|
||||
static void otaUploadPaceIfHeapTight(void)
|
||||
{
|
||||
const uint32_t free_pre = esp_get_free_heap_size();
|
||||
if (free_pre < 8192U) {
|
||||
(void)esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 1U));
|
||||
}
|
||||
}
|
||||
|
||||
void http_set_cors_headers(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
@@ -74,8 +104,9 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req)
|
||||
http_set_cors_headers(req);
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
if (!core.isActive()) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
|
||||
return ESP_FAIL;
|
||||
/* 常见场景:finalize 已成功并重启 → 新固件无 OTA 会话 → 迟到的 /ota/update。 */
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"ota_not_started", "OTA not started");
|
||||
}
|
||||
|
||||
char frontend_sha_hex[65] = {0};
|
||||
@@ -114,8 +145,8 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req)
|
||||
esp_err_t sha_err = core.computeDeviceSha(device_sha_hex);
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "verify-only: SHA 计算失败: %s", esp_err_to_name(sha_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(sha_err));
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"sha_compute_failed", esp_err_to_name(sha_err));
|
||||
}
|
||||
char ok_resp[256] = {0};
|
||||
snprintf(ok_resp, sizeof(ok_resp),
|
||||
@@ -153,7 +184,8 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req)
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, err_resp, strlen(err_resp));
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(fr.err));
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"finalize_failed", esp_err_to_name(fr.err));
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -214,8 +246,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
char peek;
|
||||
int peek_ret = httpd_req_recv(req, &peek, 1);
|
||||
if (peek_ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"recv_failed", "receive failed");
|
||||
}
|
||||
|
||||
bool is_esp32_ota = false;
|
||||
@@ -257,8 +289,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
if (is_esp32_ota) {
|
||||
if (total_size == 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"missing_total_size", "missing total_size");
|
||||
}
|
||||
|
||||
/* ESP32 路径不再需要 pre_read(JSON 已消费,固件从流继续读) */
|
||||
@@ -267,11 +299,11 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
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 http_send_json_error(req, "403 Forbidden",
|
||||
"ota_occupied", "ESP32 OTA occupied by another channel");
|
||||
}
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"ota_begin_failed", "OTA begin failed");
|
||||
}
|
||||
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
@@ -290,9 +322,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
? (req->content_len - json_header_len) : 0);
|
||||
|
||||
/* 浏览器 upload 进度到 100% 只表示 TCP 发完;设备仍在 recv+Flash。
|
||||
* Flash 扇区擦除期间 cache 暂停,必须让 WiFi/LWIP 有机会跑,否则 TCP 卡死。
|
||||
* C3 上 WiFi+BLE 双栈堆极紧:free 过低时 pbuf 申请失败 → 半路卡住/超时。
|
||||
* 因此 pacing 按 free heap 自适应,而不是固定每 N 块 delay。 */
|
||||
* sequential 擦写期间仍需让 LWIP 有机会跑;堆紧时 pacing。 */
|
||||
const int64_t t_recv0 = esp_timer_get_time();
|
||||
uint32_t write_chunks = 0U;
|
||||
|
||||
@@ -302,14 +332,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
to_read = OTA_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
/* 堆过紧时先让 LWIP 回收 TCP 窗口,再 recv,避免 pbuf 分配失败 */
|
||||
{
|
||||
const uint32_t free_pre = esp_get_free_heap_size();
|
||||
if (free_pre < 8192U) {
|
||||
(void)esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 1));
|
||||
}
|
||||
}
|
||||
otaUploadPaceIfHeapTight();
|
||||
|
||||
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||
if (ret <= 0) {
|
||||
@@ -317,8 +340,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
ret, (unsigned)total_received, req->content_len,
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"http_recv_failed", "HTTP recv failed");
|
||||
}
|
||||
|
||||
err = core.write(fw_buf, (size_t)ret);
|
||||
@@ -327,8 +350,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
esp_err_to_name(err), (unsigned)total_firmware_written,
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"ota_write_failed", "OTA write failed");
|
||||
}
|
||||
|
||||
total_firmware_written += (size_t)ret;
|
||||
@@ -389,18 +412,16 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
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);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"firmware_too_large", "firmware too large or alignment overflow");
|
||||
} 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");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "403 Forbidden",
|
||||
"system_busy", "system busy with another OTA operation");
|
||||
} 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);
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"prepare_failed", esp_err_to_name(prep_err));
|
||||
}
|
||||
|
||||
// ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler
|
||||
@@ -426,10 +447,9 @@ 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");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"gd32_write_failed", "write gd32_ota failed");
|
||||
}
|
||||
received = first_chunk_len;
|
||||
gd32_offset += (uint32_t)first_chunk_len;
|
||||
@@ -443,24 +463,24 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
size_t to_read = req->content_len - received;
|
||||
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
||||
|
||||
otaUploadPaceIfHeapTight();
|
||||
|
||||
int ret = httpd_req_recv(req, (char *)buf, to_read);
|
||||
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");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"upload_interrupted", "upload interrupted");
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(gd32_offset, buf, (size_t)ret);
|
||||
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");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"gd32_write_failed", "write gd32_ota failed");
|
||||
}
|
||||
|
||||
received += ret;
|
||||
@@ -481,7 +501,13 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
// 与 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));
|
||||
/* 存盘已完成,仍回 status=ok;relay 阶段会再比前端 SHA。
|
||||
* 此处必须醒目告警,避免只当 INFO 被忽略。 */
|
||||
DEBUG_WARN("WIFI",
|
||||
"GD32 上传完成但 SHA-256 预计算失败: %s(仍返回 stored ok,/ota/relay 将再校验)",
|
||||
esp_err_to_name(sha_err));
|
||||
DEBUG_ERR("WIFI", "GD32 finishGd32StorageAfterReceive failed: %s",
|
||||
esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,8 +538,8 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
ota_meta_t meta;
|
||||
memset(&meta, 0, sizeof(meta));
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "firmware not fully stored");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"firmware_not_stored", "firmware not fully stored");
|
||||
}
|
||||
|
||||
char frontend_sha_hex[65] = {0};
|
||||
@@ -535,9 +561,8 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
}
|
||||
}
|
||||
if (frontend_sha_hex[0] == '\0') {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
||||
"missing sha256 in body (frontend SHA-256 required)");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"missing_sha256", "missing sha256 in body (frontend SHA-256 required)");
|
||||
}
|
||||
|
||||
uint8_t frontend_sha[32] = {0};
|
||||
@@ -562,15 +587,15 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
httpd_resp_send(req, err_resp, strlen(err_resp));
|
||||
return ESP_FAIL;
|
||||
} else if (verify_err != ESP_OK) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "SHA compute failed");
|
||||
return ESP_FAIL;
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"sha_compute_failed", "SHA compute failed");
|
||||
}
|
||||
|
||||
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
|
||||
if (trigger_err != ESP_OK) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"relay_trigger_failed",
|
||||
"relay trigger failed - system busy or firmware not ready");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char resp[256];
|
||||
@@ -807,6 +832,140 @@ esp_err_t can_send_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief POST /can/send_batch — 同通道连续发送多帧,单次 HTTP 完成。
|
||||
*
|
||||
* JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] }
|
||||
* 返回: { "status":"ok", "results":[ { "status":"ok"|"error", ... }, ... ] }
|
||||
* 帧间无人为延时;单帧 SPI 超时 400ms(批量优先吞吐)。
|
||||
*/
|
||||
esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
constexpr int kMaxBody = 12288;
|
||||
constexpr int kMaxFrames = 48;
|
||||
constexpr uint32_t kPerFrameTimeoutMs = 400U;
|
||||
|
||||
int content_len = req->content_len;
|
||||
if (content_len <= 0 || content_len > kMaxBody) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "body 长度无效或过大");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char *buf = static_cast<char *>(malloc(static_cast<size_t>(content_len) + 1U));
|
||||
if (!buf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存不足");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int received = 0;
|
||||
while (received < content_len) {
|
||||
int ret = httpd_req_recv(req, buf + received, content_len - received);
|
||||
if (ret <= 0) {
|
||||
free(buf);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received += ret;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
|
||||
cJSON *frames = cJSON_GetObjectItem(root, "frames");
|
||||
if (!ch_item || !cJSON_IsArray(frames)) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "需要 ch 与 frames 数组");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
can_channel_t ch = static_cast<can_channel_t>(cJSON_GetNumberValue(ch_item));
|
||||
const int frame_count = cJSON_GetArraySize(frames);
|
||||
if (frame_count <= 0 || frame_count > kMaxFrames) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "frames 数量无效(1~48)");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *out_root = cJSON_CreateObject();
|
||||
cJSON *results = cJSON_CreateArray();
|
||||
cJSON_AddStringToObject(out_root, "status", "ok");
|
||||
cJSON_AddItemToObject(out_root, "results", results);
|
||||
|
||||
for (int i = 0; i < frame_count; i++) {
|
||||
cJSON *fr = cJSON_GetArrayItem(frames, i);
|
||||
cJSON *item = cJSON_CreateObject();
|
||||
cJSON_AddItemToArray(results, item);
|
||||
|
||||
if (!cJSON_IsObject(fr)) {
|
||||
cJSON_AddStringToObject(item, "status", "error");
|
||||
cJSON_AddNumberToObject(item, "code", -1);
|
||||
cJSON_AddNumberToObject(item, "gd32_err", 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON *id_item = cJSON_GetObjectItem(fr, "id");
|
||||
cJSON *data_item = cJSON_GetObjectItem(fr, "data");
|
||||
cJSON *dlc_item = cJSON_GetObjectItem(fr, "dlc");
|
||||
if (!id_item) {
|
||||
cJSON_AddStringToObject(item, "status", "error");
|
||||
cJSON_AddNumberToObject(item, "code", -1);
|
||||
cJSON_AddNumberToObject(item, "gd32_err", 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t ext_id = static_cast<uint32_t>(cJSON_GetNumberValue(id_item));
|
||||
uint8_t dlc = dlc_item ? static_cast<uint8_t>(cJSON_GetNumberValue(dlc_item)) : 0U;
|
||||
uint8_t tx_data[CAN_MAX_DLC] = {0};
|
||||
uint8_t tx_dlc = 0U;
|
||||
if (data_item && cJSON_IsString(data_item)) {
|
||||
tx_dlc = static_cast<uint8_t>(parseHexToBytes(cJSON_GetStringValue(data_item),
|
||||
tx_data, CAN_MAX_DLC));
|
||||
}
|
||||
if (tx_dlc == 0U && dlc > 0U) {
|
||||
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
|
||||
}
|
||||
|
||||
uint8_t resp_data[CAN_MAX_DLC] = {0};
|
||||
uint8_t gd32_err = 0U;
|
||||
int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc,
|
||||
resp_data, sizeof(resp_data), kPerFrameTimeoutMs, &gd32_err);
|
||||
|
||||
if (resp_len >= 0) {
|
||||
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
|
||||
bytesToHexUpper(resp_data, static_cast<size_t>(resp_len), resp_hex);
|
||||
cJSON_AddStringToObject(item, "status", "ok");
|
||||
cJSON_AddNumberToObject(item, "sent", tx_dlc);
|
||||
cJSON_AddNumberToObject(item, "recv_len", resp_len);
|
||||
cJSON_AddStringToObject(item, "recv", resp_hex);
|
||||
} else {
|
||||
cJSON_AddStringToObject(item, "status", "error");
|
||||
cJSON_AddNumberToObject(item, "code", resp_len);
|
||||
cJSON_AddNumberToObject(item, "gd32_err", gd32_err);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
char *printed = cJSON_PrintUnformatted(out_root);
|
||||
cJSON_Delete(out_root);
|
||||
if (!printed) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON 输出失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, printed, strlen(printed));
|
||||
free(printed);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t spi_send_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
@@ -1019,8 +1178,8 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
size_t default_heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
|
||||
// 任务栈指标复用 health_monitor 共享的任务表(collectTaskStackMetrics),
|
||||
// 避免与巡检逻辑两地维护;http_server 在本 handler 上下文中,句柄即当前任务。
|
||||
TaskStackMetric task_metrics[8];
|
||||
size_t task_metrics_count = collectTaskStackMetrics(task_metrics, 8);
|
||||
TaskStackMetric task_metrics[12];
|
||||
size_t task_metrics_count = collectTaskStackMetrics(task_metrics, 12);
|
||||
// http_server 行的句柄在 httpd 线程中取当前任务更准确
|
||||
for (size_t i = 0; i < task_metrics_count; ++i) {
|
||||
if (strcmp(task_metrics[i].name, "http_server") == 0) {
|
||||
|
||||
@@ -16,6 +16,8 @@ esp_err_t restart_handler(httpd_req_t *req);
|
||||
// Utility handlers
|
||||
esp_err_t logs_handler(httpd_req_t *req);
|
||||
esp_err_t can_send_handler(httpd_req_t *req);
|
||||
/** @brief 批量 CAN 发送:一次 HTTP 连续发多帧,减少帧间 HTTP 往返延迟。 */
|
||||
esp_err_t can_send_batch_handler(httpd_req_t *req);
|
||||
esp_err_t spi_send_handler(httpd_req_t *req);
|
||||
esp_err_t system_status_handler(httpd_req_t *req);
|
||||
esp_err_t system_probe_handler(httpd_req_t *req);
|
||||
|
||||
@@ -446,6 +446,7 @@ esp_err_t WifiOta::httpServerStart()
|
||||
{"/system/status", HTTP_GET, system_status_handler},
|
||||
{"/system/probe", HTTP_GET, system_probe_handler},
|
||||
{"/can/send", HTTP_POST, can_send_handler},
|
||||
{"/can/send_batch", HTTP_POST, can_send_batch_handler},
|
||||
{"/can/feedback", HTTP_GET, can_feedback_handler},
|
||||
{"/can/mode", HTTP_GET, can_mode_handler},
|
||||
{"/can/mode", HTTP_POST, can_mode_handler},
|
||||
@@ -468,7 +469,7 @@ esp_err_t WifiOta::httpServerStart()
|
||||
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, /can/feedback, /can/mode");
|
||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /can/send_batch, /spi/send, /can/feedback, /can/mode");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ esp_err_t Gd32FlashStore::ensureErasedForWrite(uint32_t offset, size_t len)
|
||||
addr += chunk;
|
||||
m_erasedUpTo = addr;
|
||||
(void)esp_task_wdt_reset();
|
||||
taskYIELD(); /* 长时间兜底擦时让 WiFi/LWIP 有机会跑 */
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -121,6 +122,13 @@ esp_err_t Gd32FlashStore::ensureErasedForWrite(uint32_t offset, size_t len)
|
||||
/** @brief 向 gd32_ota 分区写入数据块,自动处理非 4 字节对齐的读写。 */
|
||||
esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (len == 0U) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (data == nullptr) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const esp_partition_t *part = getPartition();
|
||||
if (!part) return ESP_ERR_NOT_FOUND;
|
||||
|
||||
@@ -136,7 +144,7 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
// 正常路径:prepare 已整分区预擦;此处仅兜底(水位未覆盖时才擦)
|
||||
// 按需擦除目标扇区(与收流交织;prepare 不再整区预擦)
|
||||
esp_err_t erase_err = ensureErasedForWrite(aligned_offset, aligned_len);
|
||||
if (erase_err != ESP_OK) {
|
||||
return erase_err;
|
||||
@@ -192,8 +200,22 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
||||
/** @brief 从 gd32_ota 分区读取数据块。 */
|
||||
esp_err_t Gd32FlashStore::flashReadChunk(uint32_t offset, uint8_t *data, size_t len)
|
||||
{
|
||||
if (len == 0U) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (data == nullptr) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const esp_partition_t *part = getPartition();
|
||||
if (!part) return ESP_ERR_NOT_FOUND;
|
||||
|
||||
/* 防坏 meta / 错误 offset 读越界 */
|
||||
if (offset >= part->size || len > (size_t)(part->size - offset)) {
|
||||
DEBUG_ERR("OTA", "flashReadChunk越界: offset=%" PRIu32 " len=%zu part=%" PRIu32,
|
||||
offset, len, part->size);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return esp_partition_read(part, offset, data, len);
|
||||
}
|
||||
|
||||
@@ -206,7 +228,7 @@ esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
DEBUG_ERR("OTA", "erasePartition: size=%" PRIu32 " > partition=%" PRIu32, size, part->size);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
// 按扇区擦除并喂狗,避免大块 erase 长时间占住调用任务
|
||||
// 按扇区擦除并喂狗/yield,避免大块 erase 长时间占住调用任务饿死 WiFi
|
||||
uint32_t done = 0;
|
||||
while (done < size) {
|
||||
uint32_t chunk = kFlashSectorSize;
|
||||
@@ -215,6 +237,8 @@ esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
}
|
||||
esp_err_t err = esp_partition_erase_range(part, done, chunk);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "erasePartition: erase @%" PRIu32 " len=%" PRIu32 " failed: %s",
|
||||
done, chunk, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
done += chunk;
|
||||
@@ -222,6 +246,7 @@ esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
m_erasedUpTo = done;
|
||||
}
|
||||
(void)esp_task_wdt_reset();
|
||||
taskYIELD();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief 新一轮 GD32 固件写入会话开始时调用:复位擦除水位。
|
||||
* prepareGd32Storage 随后会 erasePartition(整分区)。
|
||||
* 后续 flashWriteChunk 经 ensureErasedForWrite 按需擦扇区。
|
||||
*/
|
||||
void beginWriteSession();
|
||||
|
||||
@@ -152,8 +152,8 @@ private:
|
||||
|
||||
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
||||
|
||||
// ── 擦除水位:prepare 整分区预擦后 m_erasedUpTo==part->size;
|
||||
// flashWriteChunk 内 ensureErased 为安全兜底(水位已覆盖则立即返回)。
|
||||
// ── 擦除水位:beginWriteSession 后从 0 起;
|
||||
// flashWriteChunk → ensureErasedForWrite 按需推进(不再依赖 prepare 整区预擦)。
|
||||
static constexpr uint32_t kFlashSectorSize = 4096U;
|
||||
uint32_t m_erasedUpTo = 0;
|
||||
esp_err_t ensureErasedForWrite(uint32_t offset, size_t len);
|
||||
|
||||
+116
-43
@@ -1,5 +1,14 @@
|
||||
/*
|
||||
* canfd_bridge.cpp - CANFD 3通道通信桥接实现
|
||||
*
|
||||
* SPI 业务类型:
|
||||
* 0xC0 发 CAN 帧(canFdSend)
|
||||
* 0xC1 反馈页解析(canFdParseFeedbackPage;由 GD32 主动推送,本文件不轮询)
|
||||
* 0xC2 设 RX 模式(canFdSetRxMode)
|
||||
*
|
||||
* 0xC1 线格式与 GD32 app_spi_cmd_can_feedback.cpp 对齐:
|
||||
* 固定头 28B + 条目 id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc)
|
||||
* 业务 payload 上限 256 - V2_OVERHEAD(20) = 236
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "canfd_bridge.h"
|
||||
@@ -8,8 +17,19 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kSpiCanFrameType = 0xC0U;
|
||||
constexpr uint8_t kSpiCanFrameType = 0xC0U;
|
||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||
constexpr uint8_t kFeedbackFixedHeader = 28U;
|
||||
constexpr uint8_t kFeedbackMaxPage = 1U; /* GD32 仅 page 0/1 */
|
||||
|
||||
/**
|
||||
* @brief 经 SPI 发送一帧 CAN,并解析 ACK/ERROR。
|
||||
*
|
||||
* 两步法:transact 发命令(丢弃延迟一拍陈旧响应)+ poll 真实响应。
|
||||
* CAN 帧非幂等,命令只发一次,不重发。
|
||||
*
|
||||
* @return >=0 ACK 时响应负载长度;-2 SPI 失败;-3 GD32 ERROR。
|
||||
*/
|
||||
static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
@@ -24,61 +44,71 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 16) & 0xFFU);
|
||||
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 8) & 0xFFU);
|
||||
tx_buf[tx_len++] = static_cast<uint8_t>(ext_id & 0xFFU);
|
||||
if (dlc > 0U) {
|
||||
if (dlc > 0U && data != nullptr) {
|
||||
memcpy(&tx_buf[tx_len], data, dlc);
|
||||
tx_len += dlc;
|
||||
}
|
||||
|
||||
/* 独立缓冲收响应:调用方 recv_buf 为空时也能拿到 ERROR 细分码。 */
|
||||
uint8_t local_resp[72];
|
||||
uint8_t *spi_rx = (recv_buf != nullptr && recv_max > 0U) ? recv_buf : local_resp;
|
||||
const size_t spi_rx_max = (recv_buf != nullptr && recv_max > 0U)
|
||||
? static_cast<size_t>(recv_max)
|
||||
: sizeof(local_resp);
|
||||
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
/* 两步法:transact 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
|
||||
* CAN 帧非幂等,命令只发一次,不重发。 */
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiCanFrameType,
|
||||
tx_buf, tx_len,
|
||||
&resp_type, recv_buf, recv_max, &resp_len, timeout_ms);
|
||||
&resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s",
|
||||
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret));
|
||||
if (gd32_err_out) *gd32_err_out = 0U; /* SPI 失败时清零 */
|
||||
return -2; /* SPI 通信失败 */
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
if (resp_type == SPI_OTA_RSP_ACK) {
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
if (recv_buf == nullptr || recv_max == 0U) {
|
||||
if (gd32_err_out) *gd32_err_out = 0U;
|
||||
return static_cast<int>(resp_len);
|
||||
}
|
||||
const size_t copied_len = (resp_len < recv_max) ? resp_len : recv_max;
|
||||
if (gd32_err_out) *gd32_err_out = 0U;
|
||||
return static_cast<int>(copied_len);
|
||||
}
|
||||
/* resp_type == ERROR: 读取 GD32 返回的错误码 */
|
||||
uint8_t gd32_err = (resp_len > 0 && recv_buf) ? recv_buf[0] : 0xFF;
|
||||
if (gd32_err_out) *gd32_err_out = gd32_err; /* 透传细分错误码 */
|
||||
|
||||
/* ERROR:细分码在 payload[0];不依赖调用方是否提供 recv_buf */
|
||||
const uint8_t gd32_err = (resp_len > 0U) ? spi_rx[0] : 0xFFU;
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = gd32_err;
|
||||
}
|
||||
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X",
|
||||
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err);
|
||||
return -3; /* GD32 返回 ERROR */
|
||||
return -3;
|
||||
}
|
||||
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||
constexpr size_t kFeedbackPayloadLen = 123U; // 与 GD32 协议表对齐 (kTxBufSize=128 - overhead=5)
|
||||
|
||||
static inline uint32_t readU32Le(const uint8_t *p) {
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
/** @brief 小端读 uint32(0xC1 固定头/条目 id)。 */
|
||||
static inline uint32_t readU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 发送一帧 CANFD 数据,可选等待并读取响应。
|
||||
* 通过 SPI 协议帧发送给 GD32 APP,并读取 ACK/ERROR 响应。
|
||||
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
|
||||
* gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。
|
||||
*
|
||||
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
|
||||
* gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。
|
||||
* dlc==0 时允许 data 为 nullptr(仅发 ID 的远程帧/空数据帧)。
|
||||
*/
|
||||
int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
@@ -86,21 +116,31 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
uint32_t timeout_ms,
|
||||
uint8_t *gd32_err_out)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
|
||||
if (gd32_err_out) *gd32_err_out = 0U;
|
||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC) {
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms, gd32_err_out);
|
||||
/* dlc>0 时 data 必填;dlc==0 允许空指针 */
|
||||
if (dlc > 0U && data == nullptr) {
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return sendCanFrameOverSpi(channel, ext_id, data, dlc,
|
||||
recv_buf, recv_max, timeout_ms, gd32_err_out);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||
*
|
||||
* GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后
|
||||
* 调用本函数解析:
|
||||
* page=0 → 重置 out,填充 28 字节固定头 + 首批条目;
|
||||
* page=1 → 在已有条目后追加。
|
||||
* 调用方按通道维护 entryIdx,page=0 时会被重置为 0。
|
||||
* page=0 → 清空 entries、重置 entryIdx,填充 28 字节固定头 + 首批条目;
|
||||
* page=1 → 校验 channel 一致后在已有条目后追加。
|
||||
* 调用方按通道维护 entryIdx。
|
||||
*
|
||||
* @return 0 成功;-1 参数/页号/通道/截断导致无法解析。
|
||||
*/
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx)
|
||||
@@ -109,8 +149,7 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
return -1;
|
||||
}
|
||||
|
||||
constexpr uint8_t kFixedHeader = 28U;
|
||||
if (len < kFixedHeader) {
|
||||
if (len < kFeedbackFixedHeader) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -119,10 +158,15 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
if (channel >= CAN_CHANNEL_MAX) {
|
||||
return -1;
|
||||
}
|
||||
/* GD32 仅 page 0/1;非法页拒绝,避免当追加污染缓冲 */
|
||||
if (page > kFeedbackMaxPage) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* page=0:重置并解析固定头 */
|
||||
if (page == 0U) {
|
||||
/* 防 total 变少时 HTTP 读到上一轮残留 ID */
|
||||
*entryIdx = 0U;
|
||||
memset(out->entries, 0, sizeof(out->entries));
|
||||
out->stats.channel = channel;
|
||||
out->stats.mode = payload[4];
|
||||
out->stats.snapshot_seq = readU32Le(&payload[5]);
|
||||
@@ -134,13 +178,21 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
out->stats.rec = payload[26];
|
||||
out->stats.bus_state = payload[27];
|
||||
out->total_count = payload[2];
|
||||
} else {
|
||||
/* page>=1:必须与 page0 已建立的通道一致 */
|
||||
if (out->stats.channel != channel) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 解析本页条目并追加 */
|
||||
const uint8_t entryCount = payload[3];
|
||||
uint16_t offset = kFixedHeader;
|
||||
const uint8_t idx_before = *entryIdx;
|
||||
uint16_t offset = kFeedbackFixedHeader;
|
||||
|
||||
for (uint8_t i = 0U; i < entryCount && *entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
||||
if (offset + 8U > len) break;
|
||||
if (offset + 8U > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
id_snapshot_entry_t &e = out->entries[*entryIdx];
|
||||
e.id = readU32Le(&payload[offset]);
|
||||
@@ -149,11 +201,16 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
||||
/* 线协议不携带 per-ID seq/rx_count,显式置 0 避免脏读 */
|
||||
e.seq = 0U;
|
||||
e.rx_count = 0U;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
if (offset + entrySize > len) break;
|
||||
if (offset + entrySize > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(e.data, &payload[offset + 8U], dataLen);
|
||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
||||
@@ -164,15 +221,26 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
(*entryIdx)++;
|
||||
}
|
||||
|
||||
/* 声明了条目但一个都没完整解析 → 截断/坏帧 */
|
||||
if (entryCount > 0U && *entryIdx == idx_before) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out->valid = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
|
||||
* @return 0 成功;-1 参数非法或 SPI/应答失败。
|
||||
*/
|
||||
int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (mode != CAN_RX_MODE_ALL_PASS && mode != CAN_RX_MODE_FILTER_ID && mode != CAN_RX_MODE_BRIDGE_0_TO_12) {
|
||||
if (mode != CAN_RX_MODE_ALL_PASS && mode != CAN_RX_MODE_FILTER_ID
|
||||
&& mode != CAN_RX_MODE_BRIDGE_0_TO_12) {
|
||||
DEBUG_WARN("CAN", "SET_MODE 非法 mode=%u", (unsigned)mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -181,8 +249,7 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
/* 两步法破解延迟一拍:发命令一次 + poll 真实响应。
|
||||
* 0xC2 幂等,但两步法避免无效重试。 */
|
||||
/* 两步法:发命令一次 + poll 真实响应。0xC2 幂等,但避免无效重试。 */
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiCanSetModeType,
|
||||
&req, 1U,
|
||||
@@ -194,5 +261,11 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_WARN("CAN", "SET_MODE 失败: want=%u spi=%s resp=0x%02X len=%u",
|
||||
(unsigned)mode,
|
||||
esp_err_to_name(ret),
|
||||
(unsigned)resp_type,
|
||||
(unsigned)resp_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ typedef struct {
|
||||
can_channel_t channel; ///< CAN 通道 (0~2)
|
||||
uint32_t ext_id; ///< 扩展帧 ID (29bit, Big-Endian 存储)
|
||||
uint8_t data[CAN_MAX_DLC]; ///< CAN 数据负载 (最大 64 字节)
|
||||
uint8_t dlc; ///< 数据长度 (1~64)
|
||||
uint8_t dlc; ///< 数据长度 (0~64)
|
||||
} can_frame_t;
|
||||
|
||||
/**
|
||||
@@ -38,8 +38,8 @@ typedef struct {
|
||||
* 内部通过 SpiMaster 的公共 SPI 接口完成传输。
|
||||
* @param channel CAN 通道编号(0~2)。
|
||||
* @param ext_id 扩展帧 ID(29 位)。
|
||||
* @param data CAN 数据负载。
|
||||
* @param dlc 数据长度。
|
||||
* @param data CAN 数据负载;dlc==0 时可为 nullptr。
|
||||
* @param dlc 数据长度(0~64);0 表示空数据帧,仍发送 ID。
|
||||
* @param recv_buf 接收数据缓冲区(可为 nullptr)。
|
||||
* @param recv_max 接收缓冲区最大长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
@@ -61,11 +61,14 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
|
||||
/**
|
||||
* @brief 单个不重复 ID 的最新帧快照条目。
|
||||
*
|
||||
* 0xC1 线协议条目:id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc)。
|
||||
* seq / rx_count 未上线,解析后恒为 0(保留字段便于日后扩协议)。
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint32_t seq;
|
||||
uint32_t rx_count;
|
||||
uint32_t id; ///< CAN ID
|
||||
uint32_t seq; ///< 线协议未携带,解析后恒 0
|
||||
uint32_t rx_count; ///< 线协议未携带,解析后恒 0
|
||||
uint8_t dlc;
|
||||
uint8_t fd_flag;
|
||||
uint8_t brs;
|
||||
@@ -77,8 +80,6 @@ typedef struct {
|
||||
// CAN 接收过滤模式 & 1Hz 反馈快照
|
||||
// 与 GD32 DCU_APP SPI 协议帧 0xC1/0xC2 配套
|
||||
// ============================================================
|
||||
#define SPI_CAN_FEEDBACK_LAST_DATA_BYTES 93 ///< 反馈快照中最近一帧数据字节数(受 SPI 128B 应答帧约束(kTxBufSize=128 - overhead=5 - 前缀30 = 93))
|
||||
|
||||
typedef enum {
|
||||
CAN_RX_MODE_ALL_PASS = 0, ///< 默认:禁用 ID 过滤,三路全部接收
|
||||
CAN_RX_MODE_FILTER_ID = 1, ///< 按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧过滤
|
||||
@@ -115,24 +116,27 @@ typedef struct {
|
||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||
*
|
||||
* GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后
|
||||
* 调用本函数解析。page=0 时重置 out 并填充固定头 + 首批条目;
|
||||
* page=1 时在已有条目后追加。每页解析后置 out->valid=1。
|
||||
* 调用本函数解析:
|
||||
* - page=0:清空 entries、重置 entryIdx,填固定头 + 首批条目;
|
||||
* - page=1:校验 channel 与 page0 一致后追加;
|
||||
* - page>1 或声明条目却截断无法解析 → 失败。
|
||||
* 成功时置 out->valid=1。
|
||||
*
|
||||
* @param payload GD32 推送帧的 payload([channel, page, total, count, mode, seq, ...])。
|
||||
* @param len payload 长度。
|
||||
* @param out 累积结果(跨 page 0/1 累积)。
|
||||
* @param entryIdx 当前已填充条目数(page 0 时被重置为 0,调用方按通道维护)。
|
||||
* @return 0 成功,<0 失败(payload 过短或通道非法)。
|
||||
* @return 0 成功,<0 失败(payload 过短、页号/通道非法或条目截断)。
|
||||
*/
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx);
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式。
|
||||
* @param mode 目标模式(0=全收,1=按 ID 过滤)。
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
|
||||
* @param mode 目标模式(0=全收,1=按 ID 过滤,2=CAN0 桥接到 1/2)。
|
||||
* @param actual 可选输出,GD32 实际生效模式。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @return 0 成功,<0 失败。
|
||||
* @return 0 成功,<0 失败(失败时打 WARN 日志)。
|
||||
*/
|
||||
int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
|
||||
+81
-31
@@ -1,18 +1,16 @@
|
||||
/*
|
||||
* protocol.cpp - 协议层实现
|
||||
* T2.1, T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
* protocol.cpp - OTA 握手辅助实现
|
||||
*
|
||||
* 加密实现使用 ESP32-C3 硬件 AES 加速器(通过 mbedtls port)
|
||||
* 当前保留的接口:
|
||||
* - init :硬件 RNG 填充扰码常数
|
||||
* - scrambleGenerate :server nonce 生成
|
||||
* - sessionKeyInit :会话密钥写入
|
||||
* - sessionKeyClear :会话密钥安全擦除
|
||||
*
|
||||
* 当前保留的接口:init、scrambleGenerate(nonce 生成)、
|
||||
* sessionKeyInit / sessionKeyClear(会话密钥生命周期管理)。
|
||||
* 帧构建、AES-CBC 加解密、CRC16 等中间件代码已移除。
|
||||
* 帧构建、AES-ECB/CBC 加解密、CRC16 等中间件代码已迁出(见 ota_relay / spi_link_v2)。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "esp_random.h"
|
||||
#include "aes/esp_aes.h"
|
||||
#include "app_config.h"
|
||||
#include "protocol.h"
|
||||
#include "monitor/debug_log.h"
|
||||
@@ -20,8 +18,9 @@
|
||||
// ============================================================
|
||||
// Protocol 单例
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @brief 获取 Protocol 单例实例。
|
||||
* @brief 获取 Protocol 单例实例(Meyers 单例)。
|
||||
* @return Protocol 单例引用。
|
||||
*/
|
||||
Protocol& Protocol::instance()
|
||||
@@ -31,23 +30,45 @@ Protocol& Protocol::instance()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化协议层,打印初始化信息并设置初始化标志。
|
||||
* @brief 抗优化密钥/敏感缓冲擦除。
|
||||
*
|
||||
* 普通 memset 可能被编译器当作「无后续读取」而消除;
|
||||
* 通过 volatile 逐字节写 0,保证运行时真正清零。
|
||||
*
|
||||
* @param p 起始地址;nullptr 忽略。
|
||||
* @param n 字节数。
|
||||
*/
|
||||
void Protocol::secureWipe(void *p, size_t n)
|
||||
{
|
||||
if (p == nullptr || n == 0U) {
|
||||
return;
|
||||
}
|
||||
volatile uint8_t *v = static_cast<volatile uint8_t *>(p);
|
||||
while (n > 0U) {
|
||||
*v++ = 0;
|
||||
--n;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化协议层,打印初始化信息并设置初始化标志。
|
||||
*
|
||||
* 使用硬件 RNG 填充 m_scrambleConst。完整常数不再以 INFO 打 hex,
|
||||
* 避免 UDP/串口日志侧信道;需要核对时开 DEBUG_DBG。
|
||||
*/
|
||||
void Protocol::init()
|
||||
{
|
||||
if (m_initialized) return;
|
||||
if (m_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO("PROT", "=== 初始化协议层 ===");
|
||||
DEBUG_INFO("PROT", "协议层功能: nonce 扰码 + 会话密钥管理");
|
||||
DEBUG_INFO("PROT", "=== 初始化协议层(nonce 扰码 + 会话密钥管理)===");
|
||||
|
||||
esp_fill_random(m_scrambleConst, SCRAMBLE_CONST_LEN);
|
||||
|
||||
{
|
||||
char hex[SCRAMBLE_CONST_LEN * 2 + 1] = {0};
|
||||
for (size_t i = 0; i < SCRAMBLE_CONST_LEN; ++i)
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", m_scrambleConst[i]);
|
||||
DEBUG_INFO("PROT", "SCRAMBLE_CONST (RNG): %s", hex);
|
||||
}
|
||||
/* 不把完整扰码常数打到 INFO/UDP:减小日志侧信道;需要调试时开 DBG。 */
|
||||
DEBUG_DBG("PROT", "SCRAMBLE_CONST ready (%u B from HW RNG)",
|
||||
(unsigned)SCRAMBLE_CONST_LEN);
|
||||
|
||||
m_initialized = true;
|
||||
}
|
||||
@@ -57,13 +78,28 @@ void Protocol::init()
|
||||
* @param timestamp 当前时间戳(tick)。
|
||||
* @param mem_addr 内存地址(增加随机性)。
|
||||
* @param rand_seed 随机种子。
|
||||
* @param nonce_out 输出 16 字节 nonce 缓冲区。
|
||||
* @param nonce_out 输出 16 字节 nonce 缓冲区;为 nullptr 时直接返回。
|
||||
*
|
||||
* 混合步骤概要:
|
||||
* 1) state = timestamp|mem_addr|rand_seed|scrambleConst[0..3](小端)
|
||||
* 2) 3 轮:逐字节 XOR 全表 scrambleConst + 循环左移 1
|
||||
* 3) 2 轮:按 32-bit 字 XOR 完整 4B scrambleConst(memcpy 避免未对齐访问)
|
||||
*
|
||||
* @note 非标准 KDF;仅 OTA 握手用。未 init 时自动 init()。
|
||||
*/
|
||||
void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out)
|
||||
{
|
||||
if (nonce_out == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!m_initialized) {
|
||||
init();
|
||||
}
|
||||
|
||||
uint8_t state[16];
|
||||
|
||||
/* 宿主小端布局;ESP32 / GD32 均为 LE,与对端一致。 */
|
||||
memcpy(&state[0], ×tamp, 4);
|
||||
memcpy(&state[4], &mem_addr, 4);
|
||||
memcpy(&state[8], &rand_seed, 4);
|
||||
@@ -72,14 +108,22 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
for (int round = 0; round < 3; round++) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
state[i] ^= m_scrambleConst[i];
|
||||
state[i] = (state[i] << 1) | (state[i] >> 7);
|
||||
state[i] = (uint8_t)((state[i] << 1) | (state[i] >> 7));
|
||||
}
|
||||
}
|
||||
|
||||
/* 按 32-bit 字再混一轮完整 4B 扰码(memcpy 避免未对齐 uint32_t*)。 */
|
||||
for (int round = 0; round < 2; round++) {
|
||||
uint32_t *words = (uint32_t *)state;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
words[i] ^= m_scrambleConst[i * 4 % 16] | ((uint32_t)m_scrambleConst[i * 4 + 1] << 8);
|
||||
uint32_t w = 0;
|
||||
memcpy(&w, &state[i * 4], 4);
|
||||
const uint32_t mix =
|
||||
(uint32_t)m_scrambleConst[i * 4]
|
||||
| ((uint32_t)m_scrambleConst[i * 4 + 1] << 8)
|
||||
| ((uint32_t)m_scrambleConst[i * 4 + 2] << 16)
|
||||
| ((uint32_t)m_scrambleConst[i * 4 + 3] << 24);
|
||||
w ^= mix;
|
||||
memcpy(&state[i * 4], &w, 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,24 +135,30 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
* @param sess 会话上下文指针。
|
||||
* @param k_session 16 字节会话密钥。
|
||||
* @return 0 成功,-1 参数无效。
|
||||
*
|
||||
* 先 secureWipe 再拷贝,避免旧 key 残留。
|
||||
*/
|
||||
int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
|
||||
{
|
||||
if (!sess || !k_session) return -1;
|
||||
|
||||
memset(sess, 0, sizeof(session_ctx_t));
|
||||
memcpy(sess->key, k_session, 16);
|
||||
if (sess == nullptr || k_session == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
secureWipe(sess, sizeof(*sess));
|
||||
memcpy(sess->key, k_session, SESSION_KEY_LEN);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
* @param sess 会话上下文指针;为 nullptr 时忽略。
|
||||
*
|
||||
* 使用 secureWipe,避免编译器优化掉「无读回的」密钥清零。
|
||||
*/
|
||||
void Protocol::sessionKeyClear(session_ctx_t *sess)
|
||||
{
|
||||
if (sess) {
|
||||
memset(sess->key, 0, 16);
|
||||
if (sess == nullptr) {
|
||||
return;
|
||||
}
|
||||
secureWipe(sess->key, SESSION_KEY_LEN);
|
||||
}
|
||||
|
||||
+60
-27
@@ -1,75 +1,108 @@
|
||||
/*
|
||||
* protocol.h - 帧格式、CRC16、AES 加密解密
|
||||
* T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
* protocol.h - OTA 握手辅助(nonce 扰码 + 会话密钥生命周期)
|
||||
*
|
||||
* Protocol 类封装协议层功能。
|
||||
* 本模块刻意保持轻量(原「帧格式 / CRC16 / AES-CBC」中间件已迁出):
|
||||
* - 不负责 SPI Link v2 帧 / CRC32
|
||||
* - 不负责 AES 加解密(OTA 数据包 AES-ECB 在 ota_relay 内)
|
||||
* - 仅提供:RNG 扰码常数、server nonce 生成、session key 存清
|
||||
*
|
||||
* AES 主密钥 MASTER_KEY[] 见 app_config.h。
|
||||
*/
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "app_config.h"
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16; ///< AES-128 主密钥长度(字节)
|
||||
inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(字节)
|
||||
/** @brief AES-128 / 会话密钥长度(字节)。 */
|
||||
inline constexpr size_t SESSION_KEY_LEN = 16;
|
||||
|
||||
// AES 会话上下文
|
||||
/**
|
||||
* @brief OTA 会话密钥上下文。
|
||||
*
|
||||
* 仅缓存协商得到的 K_session;真正加解密在 ota_relay 中用该 key 做 AES-ECB。
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t key[16]; // 会话密钥 K_session
|
||||
uint8_t key[SESSION_KEY_LEN]; ///< 会话密钥 K_session(16 字节)
|
||||
} session_ctx_t;
|
||||
|
||||
/**
|
||||
* @brief 协议层类(单例)
|
||||
* @brief OTA 握手辅助类(单例)
|
||||
*
|
||||
* 封装 CRC16、AES-128-CBC、帧构建/解析、会话管理等功能。
|
||||
* 大部分方法为静态工具方法,单例仅持有初始化标志。
|
||||
* 持有 per-boot 扰码常数 m_scrambleConst;
|
||||
* sessionKeyInit / sessionKeyClear 为无状态静态工具,不依赖单例成员。
|
||||
*/
|
||||
class Protocol {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取 Protocol 单例实例。
|
||||
* @brief 获取 Protocol 单例实例。
|
||||
* @return Protocol 单例引用。
|
||||
*/
|
||||
static Protocol& instance();
|
||||
|
||||
/**
|
||||
* @brief 初始化协议层。
|
||||
* @brief 初始化协议层(幂等)。
|
||||
*
|
||||
* 使用硬件 RNG 填充 m_scrambleConst,供 scrambleGenerate 混合使用。
|
||||
* 可重复调用;已初始化则直接返回。
|
||||
*/
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief 生成基于时间戳、地址和随机种子的扰码 nonce。
|
||||
* @param timestamp 时间戳。
|
||||
* @param mem_addr 内存地址。
|
||||
* @brief 生成基于时间戳、地址和随机种子的 16 字节 server nonce。
|
||||
* @param timestamp 时间戳(如 FreeRTOS tick)。
|
||||
* @param mem_addr 内存地址(增加熵,通常取栈/缓冲区地址)。
|
||||
* @param rand_seed 随机种子。
|
||||
* @param nonce_out 输出的 16 字节 nonce 缓冲区。
|
||||
* @param nonce_out 输出缓冲区,至少 16 字节;为 nullptr 时直接返回。
|
||||
*
|
||||
* @note 非标准 KDF,仅作 OTA 握手 nonce,勿当通用密钥派生。
|
||||
* @note 若尚未 init(),内部会自动调用 init()。
|
||||
*/
|
||||
void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
|
||||
/**
|
||||
* @brief 会话密钥初始化,将密钥拷贝到会话上下文。
|
||||
* @brief 会话密钥初始化,将密钥拷贝到会话上下文。
|
||||
* @param sess 会话上下文指针。
|
||||
* @param k_session 会话密钥数据。
|
||||
* @param k_session 会话密钥数据(至少 SESSION_KEY_LEN 字节)。
|
||||
* @return 成功返回 0,参数无效返回 -1。
|
||||
*/
|
||||
static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session);
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
* @brief 清除会话上下文中的密钥数据(抗优化擦除)。
|
||||
* @param sess 会话上下文指针;为 nullptr 时忽略。
|
||||
*/
|
||||
static void sessionKeyClear(session_ctx_t *sess);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 私有默认构造函数。
|
||||
*/
|
||||
Protocol() = default;
|
||||
/**
|
||||
* @brief 私有析构函数。
|
||||
*/
|
||||
~Protocol() = default;
|
||||
/**
|
||||
* @brief 禁止复制构造。
|
||||
*/
|
||||
Protocol(const Protocol&) = delete;
|
||||
/**
|
||||
* @brief 禁止复制赋值。
|
||||
*/
|
||||
Protocol& operator=(const Protocol&) = delete;
|
||||
|
||||
uint8_t m_scrambleConst[SCRAMBLE_CONST_LEN] = {0};
|
||||
bool m_initialized = false;
|
||||
/**
|
||||
* @brief 抗编译器优化的内存擦除(volatile 逐字节写 0)。
|
||||
* @param p 起始地址;nullptr 忽略。
|
||||
* @param n 字节数。
|
||||
*/
|
||||
static void secureWipe(void *p, size_t n);
|
||||
|
||||
uint8_t m_scrambleConst[SCRAMBLE_CONST_LEN] = {0}; ///< per-boot 扰码常数(init 时 RNG 填充)
|
||||
bool m_initialized = false; ///< 是否已 init
|
||||
};
|
||||
|
||||
#endif // PROTOCOL_H
|
||||
#endif // PROTOCOL_H
|
||||
|
||||
+135
-206
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/queue.h"
|
||||
@@ -29,13 +31,16 @@
|
||||
|
||||
static constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||
|
||||
/** @brief submitJob 堆 Job 与 worker 之间的 finished/abandoned 交接。 */
|
||||
static portMUX_TYPE s_spiJobMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
/**
|
||||
* @brief 发送路径 CS 时钟字节数。
|
||||
* Boot TX DMA 按满缓冲预装(kTxBufSize=128),主机过短时钟会:
|
||||
* - 截断延迟一拍的响应;
|
||||
* - 两段变长读若中途释放 CS,Boot 会把 s_txPending 当已交付清掉 → 后续全 0xFF。
|
||||
* 下限 64:覆盖 HELLO_ACK(~26)/短 ACK;上限 SPI_OTA_FRAME_MAX。
|
||||
* App 大帧仍可由 poll 路径按 128/256 读。
|
||||
* 下限 64:覆盖 HELLO_ACK(~26)/短 ACK;上限 SPI_OTA_FRAME_MAX(256)。
|
||||
* APP 侧 TX 满缓冲 256:poll/ring 读预算见 spiRespReadBudget()。
|
||||
*/
|
||||
static size_t spiXferBytes(size_t frame_len)
|
||||
{
|
||||
@@ -49,10 +54,19 @@ static size_t spiXferBytes(size_t frame_len)
|
||||
return n;
|
||||
}
|
||||
|
||||
/** @brief 读响应时单 CS 时钟预算(Boot 满缓冲 128;App 可达 256)。 */
|
||||
/**
|
||||
* @brief 读响应时单 CS 时钟预算(按最近 HELLO 的 GD32 role 分支)。
|
||||
* - BOOT:TX DMA 满缓冲 128 → 读 128,避免过长时钟 underrun
|
||||
* - APP / unknown:TX DMA 满缓冲 256 → 读 256,避免 0xC1 等长 EVENT 被截断 CRC
|
||||
* unknown 偏 APP:宁长勿短(BOOT 短帧在 256 时钟下尾部 0xFF,仍可 parse)。
|
||||
*/
|
||||
static size_t spiRespReadBudget(void)
|
||||
{
|
||||
return 128U;
|
||||
const uint8_t role = SpiMaster::instance().gd32Role();
|
||||
if (role == V2_ROLE_BOOT) {
|
||||
return SPI_BOOT_TX_BUF_SIZE;
|
||||
}
|
||||
return SPI_APP_TX_BUF_SIZE;
|
||||
}
|
||||
|
||||
/** @brief ISR-safe GPIO 电平读取(不调用 Flash 中的 gpio_get_level)。
|
||||
@@ -63,7 +77,7 @@ static int IRAM_ATTR readReadyLevelRaw(void)
|
||||
return (REG_READ(GPIO_IN_REG) >> SPI_READY_GPIO) & 0x1;
|
||||
}
|
||||
|
||||
/* 前向声明:waitSpiDataReady 定义在文件下方(waitSpiReady 之后),
|
||||
/* 前向声明:waitSpiDataReady 定义在文件下方,
|
||||
* 但 serviceRingOnce / otaPollResponseUnlocked 等更早出现的方法需要它。 */
|
||||
static esp_err_t waitSpiDataReady(uint32_t timeout_ms);
|
||||
|
||||
@@ -244,15 +258,37 @@ void SpiMaster::executeJob(Job &job)
|
||||
case JobKind::WaitBoot:
|
||||
job.result = waitForBootloaderReady(job.timeout_ms);
|
||||
break;
|
||||
case JobKind::ServiceRing:
|
||||
job.result = serviceRingOnce();
|
||||
break;
|
||||
default:
|
||||
job.result = ESP_ERR_INVALID_ARG;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpiMaster::finishHeapJob(Job *job)
|
||||
{
|
||||
if (job == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 先 give,再标 finished;提交方 take 成功后会 spin 等 finished 再 free。 */
|
||||
if (job->done) {
|
||||
(void)xSemaphoreGive(job->done);
|
||||
}
|
||||
if (!job->heap_allocated) {
|
||||
return;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
job->finished = true;
|
||||
const bool free_it = job->abandoned;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (free_it) {
|
||||
if (job->done) {
|
||||
vSemaphoreDelete(job->done);
|
||||
job->done = nullptr;
|
||||
}
|
||||
free(job);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t SpiMaster::submitJob(Job &job)
|
||||
{
|
||||
if (m_jobQueue == nullptr || m_workerTask == nullptr || onWorker()) {
|
||||
@@ -260,31 +296,72 @@ esp_err_t SpiMaster::submitJob(Job &job)
|
||||
return job.result;
|
||||
}
|
||||
|
||||
StaticSemaphore_t done_buf;
|
||||
job.done = xSemaphoreCreateBinaryStatic(&done_buf);
|
||||
if (job.done == nullptr) {
|
||||
executeJob(job);
|
||||
return job.result;
|
||||
/* 堆 Job + 动态 binary sem:避免调用方超时返回后栈上 Job/done 被 worker UAF。 */
|
||||
Job *heap_job = static_cast<Job *>(calloc(1, sizeof(Job)));
|
||||
if (heap_job == nullptr) {
|
||||
DEBUG_ERR("SPI", "submitJob: calloc failed kind=%u", (unsigned)job.kind);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
*heap_job = job;
|
||||
heap_job->heap_allocated = true;
|
||||
heap_job->finished = false;
|
||||
heap_job->abandoned = false;
|
||||
heap_job->result = ESP_FAIL;
|
||||
heap_job->done = xSemaphoreCreateBinary();
|
||||
if (heap_job->done == nullptr) {
|
||||
free(heap_job);
|
||||
DEBUG_ERR("SPI", "submitJob: done sem create failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
(void)xSemaphoreTake(job.done, 0);
|
||||
|
||||
Job *ptr = &job;
|
||||
const TickType_t qwait = pdMS_TO_TICKS(
|
||||
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 2000U);
|
||||
if (xQueueSend(m_jobQueue, &ptr, qwait) != pdTRUE) {
|
||||
if (xQueueSend(m_jobQueue, &heap_job, qwait) != pdTRUE) {
|
||||
DEBUG_WARN("SPI", "worker job queue full/timeout kind=%u", (unsigned)job.kind);
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
free(heap_job);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
/* 唤醒可能阻塞在 notify wait 的 worker */
|
||||
xTaskNotify(m_workerTask, 0x10U, eSetBits);
|
||||
|
||||
const TickType_t dwait = pdMS_TO_TICKS(
|
||||
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 10000U);
|
||||
if (xSemaphoreTake(job.done, dwait) != pdTRUE) {
|
||||
DEBUG_ERR("SPI", "worker job done timeout kind=%u", (unsigned)job.kind);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
if (xSemaphoreTake(heap_job->done, dwait) == pdTRUE) {
|
||||
const esp_err_t result = heap_job->result;
|
||||
/* worker 在 give 后才写 finished;等其退出临界区后再由本侧独占 free。 */
|
||||
for (;;) {
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
const bool fin = heap_job->finished;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (fin) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
heap_job->done = nullptr;
|
||||
free(heap_job);
|
||||
return result;
|
||||
}
|
||||
return job.result;
|
||||
|
||||
/* 超时:标记 abandoned;若 worker 已跑完则本侧 free,否则 worker finishHeapJob free。 */
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
heap_job->abandoned = true;
|
||||
const bool already_done = heap_job->finished;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
|
||||
if (already_done) {
|
||||
(void)xSemaphoreTake(heap_job->done, 0);
|
||||
const esp_err_t result = heap_job->result;
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
heap_job->done = nullptr;
|
||||
free(heap_job);
|
||||
return result;
|
||||
}
|
||||
|
||||
DEBUG_ERR("SPI", "worker job done timeout kind=%u(堆 Job 交 worker 释放)",
|
||||
(unsigned)job.kind);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
void SpiMaster::workerEntry(void *arg)
|
||||
@@ -312,9 +389,7 @@ void SpiMaster::workerLoop()
|
||||
Job *job = nullptr;
|
||||
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
|
||||
executeJob(*job);
|
||||
if (job->done) {
|
||||
xSemaphoreGive(job->done);
|
||||
}
|
||||
finishHeapJob(job);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -333,76 +408,38 @@ void SpiMaster::workerLoop()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief GPIO 边沿中断回调(SPIready 线跳变)。
|
||||
*
|
||||
* - 下降沿:GD32 主动按铃(请求 ESP32 来读)。若当前空闲则置
|
||||
* m_gd32RequestPending 并唤醒 SPI 任务。
|
||||
* - 上升沿:GD32 释放 SPIready(WAITING_RESPONSE 态),唤醒等待任务。
|
||||
* @brief GPIO 边沿中断回调(DATA_READY / PB6)。
|
||||
*
|
||||
* 仅处理下降沿:GD32 有待读数据(按铃)。若本端 IDLE 则置
|
||||
* m_gd32RequestPending 并唤醒 SPI worker。上升沿不再使用(旧 waitSpiReady 已删)。
|
||||
* ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。
|
||||
*/
|
||||
void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
|
||||
{
|
||||
SpiMaster *self = static_cast<SpiMaster *>(arg);
|
||||
if (!self) { return; }
|
||||
int level = readReadyLevelRaw();
|
||||
if (level == 0) {
|
||||
/* 下降沿 */
|
||||
if (self->m_suppressRingService) {
|
||||
/* OTA 中继:PB6 低是 WAIT/ACK 就绪,留给业务 poll,不按 EVENT 处理 */
|
||||
} else if (self->m_linkState.acceptRing()) {
|
||||
/* GD32 按铃:仅 IDLE 态接受 */
|
||||
self->m_gd32RequestPending = true;
|
||||
if (self->m_spiTaskHandle) {
|
||||
BaseType_t hp = pdFALSE;
|
||||
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x01U,
|
||||
eSetBits, &hp);
|
||||
portYIELD_FROM_ISR(hp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 上升沿 */
|
||||
if (self->m_linkState.state() == HandshakeState::WAITING_RESPONSE) {
|
||||
/* GD32 释放 SPIready,可发起新 CS */
|
||||
if (self->m_spiTaskHandle) {
|
||||
BaseType_t hp = pdFALSE;
|
||||
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x02U,
|
||||
eSetBits, &hp);
|
||||
portYIELD_FROM_ISR(hp);
|
||||
}
|
||||
}
|
||||
/* 只关心低电平(下降沿后采样):有数据待读 */
|
||||
if (readReadyLevelRaw() != 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 等 SPIready 适合发起下一笔主机事务(v2 DATA_READY 语义)。
|
||||
*
|
||||
* v2:PB6 低 = GD32 有待读数据;高 = 队列空/空闲。
|
||||
* - 发新命令前:若 PB6 已高,立即返回;若仍低,可能是未读走的响应,
|
||||
* 短暂等待其升高(BOOT 交付后应拉高)。超时不阻塞——CS 本身互斥。
|
||||
* - 注意:poll 读响应路径不应调用本函数等 HIGH;应等 LOW 或直接读。
|
||||
*/
|
||||
esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms)
|
||||
{
|
||||
if (timeout_ms == 0U) { return ESP_OK; }
|
||||
|
||||
/* 已高,立即返回 */
|
||||
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
||||
|
||||
/* PB6 低:可能有未读响应。短等升高;BOOT 修好交付清 pending 后应很快。
|
||||
* 超时返回 ESP_ERR_TIMEOUT,禁止调用方假装“已就绪”后盲发。 */
|
||||
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||
while (xTaskGetTickCount() < deadline) {
|
||||
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
||||
vTaskDelay(pdMS_TO_TICKS(1) > 0 ? pdMS_TO_TICKS(1) : 1);
|
||||
if (self->m_suppressRingService) {
|
||||
/* OTA 中继:PB6 低是 WAIT/ACK 就绪,留给业务 poll,不按 EVENT 处理 */
|
||||
return;
|
||||
}
|
||||
if (!self->m_linkState.acceptRing()) {
|
||||
return;
|
||||
}
|
||||
self->m_gd32RequestPending = true;
|
||||
if (self->m_spiTaskHandle) {
|
||||
BaseType_t hp = pdFALSE;
|
||||
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x01U, eSetBits, &hp);
|
||||
portYIELD_FROM_ISR(hp);
|
||||
}
|
||||
m_stats.ready_timeouts++;
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 等 GD32 拉低 PB6(有待读响应)。
|
||||
* v2 DATA_READY:低 = 有数据,与 waitSpiReady(等高)语义相反。
|
||||
* v2 DATA_READY:低 = 有数据;高 = 队列空。TX 前不等 HIGH(CS 即互斥)。
|
||||
* @return ESP_OK 已低;ESP_ERR_TIMEOUT 超时仍为高(调用方可再决定是否盲 poll)。
|
||||
*/
|
||||
static esp_err_t waitSpiDataReady(uint32_t timeout_ms)
|
||||
@@ -538,7 +575,7 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
|
||||
*
|
||||
* 不用两段 KEEP_ACTIVE:Boot 在 CS 释放时无论是否读完都清 s_txPending,
|
||||
* 中途断 CS 会丢 HELLO_ACK,后续 MISO 全 0xFF(线上已复现)。
|
||||
* 预算 128B 对齐 Boot TX DMA 满缓冲;App 大 EVENT 仍可到 256(out_max)。
|
||||
* 时钟预算见 spiRespReadBudget():BOOT=128,APP=256。
|
||||
*/
|
||||
esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
|
||||
uint32_t timeout_ms)
|
||||
@@ -762,12 +799,12 @@ esp_err_t SpiMaster::init()
|
||||
ready_cfg.mode = GPIO_MODE_INPUT; /* v2: 仅输入,不再开漏输出 */
|
||||
ready_cfg.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
ready_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
ready_cfg.intr_type = GPIO_INTR_ANYEDGE; /* 双边沿中断 */
|
||||
ready_cfg.intr_type = GPIO_INTR_NEGEDGE; /* 仅下降沿:有数据待读 */
|
||||
esp_err_t gpio_ret = gpio_config(&ready_cfg);
|
||||
if (gpio_ret != ESP_OK) {
|
||||
DEBUG_WARN("SPI", "SPI Ready 握手引脚 GPIO%d 配置失败: %s", SPI_READY_GPIO, esp_err_to_name(gpio_ret));
|
||||
} else {
|
||||
DEBUG_INFO("SPI", "SPI Ready 开漏握手已配置: GPIO%d (线与 GD32 PB6)", SPI_READY_GPIO);
|
||||
DEBUG_INFO("SPI", "DATA_READY 输入已配置: GPIO%d (← GD32 PB6,低=有数据)", SPI_READY_GPIO);
|
||||
}
|
||||
|
||||
/* 安装 GPIO ISR 服务(全局仅安装一次)并注册边沿回调 */
|
||||
@@ -883,15 +920,9 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
|
||||
}
|
||||
}
|
||||
|
||||
/* 同步单 CS 传输:按帧长发送;延迟一拍下本笔 RX 为陈旧响应。 */
|
||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||
/* v2 延迟一拍:PB6 低表示 GD32 有上一笔响应待读,step1 的 transferClocked
|
||||
* 会自然把它当陈旧响应读走,不需要额外 drain(drain 会多消耗一个 CS 周期,
|
||||
* 可能误吃掉主循环刚排好的 ACK 导致后续包永久丢 ACK)。
|
||||
*
|
||||
* ★ 不调 waitSpiReady(等 PB6 变高):v2 DATA_READY 下 PB6 低 = GD32 有待发
|
||||
* 响应,OTA 中继期间几乎持续为低。旧代码每包忙等 200ms 超时,21 包累计
|
||||
* ~4.2s 纯浪费,最终拖垮转发预算导致卡死。CS 本身即互斥,无需额外就绪等待。 */
|
||||
/* 同步单 CS 传输:按帧长发送;延迟一拍下本笔 RX 为陈旧响应。
|
||||
* v2:PB6 低 = 有待读;陈旧响应由本笔 transferClocked 自然读走(勿额外 drain,
|
||||
* 以免误吃主循环刚排好的 ACK)。CS 即互斥,TX 前不等 PB6 HIGH。 */
|
||||
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
|
||||
|
||||
const size_t xfer_len = spiXferBytes(tx_len);
|
||||
@@ -1161,17 +1192,23 @@ bool SpiMaster::takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 循环等待:信号量到达后重新持锁取,取空则继续等(防虚假唤醒) */
|
||||
const TickType_t deadline_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||
/* 用绝对截止时间:虚假唤醒/抢事件后按剩余时间等,避免每次重新等满 timeout。 */
|
||||
const TickType_t start = xTaskGetTickCount();
|
||||
const TickType_t total = pdMS_TO_TICKS(timeout_ms);
|
||||
while (true) {
|
||||
if (xSemaphoreTake(m_eventSem, deadline_ticks) != pdTRUE) {
|
||||
return false; /* 超时 */
|
||||
const TickType_t elapsed = xTaskGetTickCount() - start;
|
||||
if (elapsed >= total) {
|
||||
return false;
|
||||
}
|
||||
const TickType_t remain = total - elapsed;
|
||||
if (xSemaphoreTake(m_eventSem, remain) != pdTRUE) {
|
||||
return false;
|
||||
}
|
||||
LockGuard lock(m_mutex);
|
||||
if (popEvent(type, payload, max_len, len)) {
|
||||
return true;
|
||||
}
|
||||
/* 信号量被消费但队列空(可能被并发 takeEvent 抢走),继续等 */
|
||||
/* 信号量被消费但队列空(可能被并发 takeEvent 抢走),继续等剩余时间 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,112 +1250,6 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
/*expected_type=*/0xFFU);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 带重试退避的事务(SPEC §9.2)。
|
||||
*
|
||||
* 同序号重试最多 3 次,退避 10/30/90ms;
|
||||
* 3 次失败后 HELLO 重同步,再试 1 次。
|
||||
* 调用者须已持有锁。
|
||||
*/
|
||||
esp_err_t SpiMaster::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)
|
||||
{
|
||||
/* 保存原始序号用于重发时恢复 */
|
||||
const uint8_t saved_tx_seq = m_txSeq;
|
||||
static constexpr uint32_t kBackoffMs[] = { 10U, 30U, 90U };
|
||||
static constexpr uint32_t kMaxRetries = 3U;
|
||||
|
||||
for (uint32_t retry = 0U; retry <= kMaxRetries; ++retry) {
|
||||
if (retry > 0U) {
|
||||
const uint32_t backoff = kBackoffMs[(retry - 1U) < 3U ? (retry - 1U) : 2U];
|
||||
vTaskDelay(pdMS_TO_TICKS(backoff));
|
||||
m_stats.retries++;
|
||||
/* 重发时恢复原序号 */
|
||||
m_txSeq = saved_tx_seq;
|
||||
}
|
||||
|
||||
/* 使用两步法(持锁已由调用者完成,用 Unlocked 版本) */
|
||||
const uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
|
||||
esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len,
|
||||
nullptr, nullptr, 0U, nullptr, send_timeout,
|
||||
/*expect_stale=*/true);
|
||||
if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) {
|
||||
if (retry < kMaxRetries) { continue; }
|
||||
return send_ret;
|
||||
}
|
||||
|
||||
const uint32_t poll_timeout = (timeout_ms > send_timeout)
|
||||
? (timeout_ms - send_timeout) : 500U;
|
||||
esp_err_t ret = otaPollResponseUnlocked(resp_type, resp_payload,
|
||||
resp_max, resp_len, poll_timeout,
|
||||
/*expected_type=*/0xFFU);
|
||||
if (ret == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (ret == ESP_ERR_TIMEOUT) {
|
||||
m_stats.ready_timeouts++;
|
||||
}
|
||||
|
||||
/* 3 次重试后尝试 HELLO 重同步(最后一次循环) */
|
||||
if (retry == kMaxRetries - 1U) {
|
||||
DEBUG_WARN("SPI", "transactWithRetry: %u 次失败, HELLO 重同步...", (unsigned)retry);
|
||||
m_v2Active = false;
|
||||
/* HELLO 需要锁——但调用者已持锁。
|
||||
* hello() 内部有 LockGuard,会递归死锁。
|
||||
* 因此用 hello 的核心逻辑但不加锁:两阶段(发 + dummy 收)。 */
|
||||
m_sessionId = esp_random() | 0x00000001U;
|
||||
m_txSeq = 0;
|
||||
m_lastSentSeq = 0;
|
||||
m_rxAckedSeq = 0;
|
||||
m_expectedRxSeq = 0;
|
||||
|
||||
uint8_t hello_frame[V2_MAX_FRAME];
|
||||
size_t hello_len = 0;
|
||||
if (v2_build_hello_frame(m_sessionId, 0U, hello_frame, &hello_len)) {
|
||||
/* 延迟一拍连发 2 次,避免 DATA_READY/短读导致全 0xFF */
|
||||
const size_t xfer_len = spiXferBytes(hello_len);
|
||||
for (uint32_t hi = 0U; hi < 2U; ++hi) {
|
||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
|
||||
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
|
||||
uint8_t tx_p[SPI_OTA_FRAME_MAX];
|
||||
uint8_t rx_r[SPI_OTA_FRAME_MAX];
|
||||
memset(tx_p, 0xFF, xfer_len);
|
||||
memcpy(tx_p, hello_frame, hello_len);
|
||||
memset(rx_r, 0xFF, xfer_len);
|
||||
esp_err_t hr = transferClocked(tx_p, xfer_len, rx_r, xfer_len, 2000U);
|
||||
m_linkState.exitBusy();
|
||||
if (hr != ESP_OK) {
|
||||
continue;
|
||||
}
|
||||
v2_header_t hdr;
|
||||
const uint8_t *pl = nullptr;
|
||||
if (v2_parse_frame(rx_r, xfer_len, &hdr, &pl) &&
|
||||
hdr.type == V2_TYPE_LINK_HELLO &&
|
||||
(hdr.flags & V2_FLAG_RESPONSE) &&
|
||||
hdr.payload_len >= 6U && pl &&
|
||||
v2_get_u32le(&pl[0]) == m_sessionId) {
|
||||
m_gd32Role = pl[4];
|
||||
m_localCredit = pl[5];
|
||||
m_remoteCredit = hdr.credit;
|
||||
m_v2Active = true;
|
||||
m_stats.resyncs++;
|
||||
DEBUG_INFO("SPI", "transactWithRetry: HELLO 重同步成功 role=%u",
|
||||
m_gd32Role);
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief 通知 GD32 APP 设置 OTA 标志并软复位进入 Bootloader(v2 标准帧)。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
@@ -1713,8 +1644,6 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
|
||||
while (xTaskGetTickCount() <= deadline) {
|
||||
++attempt;
|
||||
|
||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
|
||||
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
|
||||
|
||||
uint8_t tx_padded[SPI_OTA_FRAME_MAX];
|
||||
|
||||
+31
-53
@@ -4,12 +4,10 @@
|
||||
*
|
||||
* SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。
|
||||
*
|
||||
* 握手架构(线与 SPIready,GPIO10<->GD32 PB6 开漏单线):
|
||||
* - 空闲态线 HIGH(双方释放,靠上拉)。
|
||||
* - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉,
|
||||
* GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高),
|
||||
* ESP32 GPIO 上升沿中断唤醒后发起 CS#2 读响应。
|
||||
* - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务。
|
||||
* 握手架构(v2 DATA_READY,GPIO10 ← GD32 PB6 开漏):
|
||||
* - PB6 仅由 GD32 驱动:低 = 有待读数据,高 = 队列空。ESP32 只输入、不拉线。
|
||||
* - 帧同步/许可在帧内 SEQ/ACK_SEQ/CREDIT;CS 本身即互斥,TX 前不再等 HIGH。
|
||||
* - GD32 有数据:拉低 DATA_READY → ESP32 下降沿/轮询 → CS 读响应。
|
||||
*/
|
||||
#ifndef SPI_MASTER_H
|
||||
#define SPI_MASTER_H
|
||||
@@ -22,21 +20,16 @@
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include "app_config.h" /* SPI_MASTER_CS_GPIO / SPI_READY_GPIO / SPI_OTA_FRAME_MAX 等 */
|
||||
|
||||
// SPI 配置常量
|
||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||
#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ // 默认 4MHz,可在 menuconfig 手动提速
|
||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||
#define SPI_QUEUE_SIZE (4) ///< SPI 事务队列深度
|
||||
#define SPI_READY_GPIO (10) // 线与 SPIready 握手引脚(开漏,GPIO10<->GD32 PB6)
|
||||
#define SPI_READY_TIMEOUT_MS (200) // 等待 SPIready 上升沿超时(毫秒)
|
||||
#define SPI_OTA_FRAME_MAX (256) ///< SPI 同步帧最大字节数
|
||||
// SPI 时钟来自 menuconfig;其余可调项在 app_config.h
|
||||
#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ
|
||||
|
||||
// GD32 SPI 同步帧类型
|
||||
// GD32 SPI 业务 TYPE(与 SpiDefs / v2 TYPE 字段一致)
|
||||
#define SPI_OTA_CMD_REQ (0x01)
|
||||
#define SPI_OTA_CMD_DATA (0x02)
|
||||
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
|
||||
#define SPI_APP_ENTER_BOOT_ACK_OK (0xB5) // GD32 APP 固件须同步修改 ACK 字节为 0xB5
|
||||
#define SPI_APP_ENTER_BOOT_ACK_OK (0xB5)
|
||||
#define SPI_OTA_RSP_ACK (0x80)
|
||||
#define SPI_OTA_RSP_WAIT (0x81)
|
||||
#define SPI_OTA_RSP_ERROR (0x82)
|
||||
@@ -103,6 +96,9 @@ public:
|
||||
/** @brief 读取本端剩余可接收 credit(SPEC §10.1,/system/status 展示用)。 */
|
||||
uint8_t localCredit() const { return m_localCredit; }
|
||||
|
||||
/** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */
|
||||
TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; }
|
||||
|
||||
/**
|
||||
* @brief OTA 中继期间禁止 worker 按铃读。
|
||||
*
|
||||
@@ -215,24 +211,20 @@ private:
|
||||
spi_device_handle_t m_spiDevice = NULL;
|
||||
SemaphoreHandle_t m_mutex = NULL;
|
||||
|
||||
// ── 线与 SPIready 握手状态机 ──
|
||||
/** 握手状态机枚举。 */
|
||||
// ── 本端事务态(非 PB6 电平门控;PB6 仅 DATA_READY 通知)──
|
||||
/** 本端 SPI 事务状态:仅用于 acceptRing(忙时不接按铃)。 */
|
||||
enum class HandshakeState : uint8_t {
|
||||
IDLE = 0, ///< 空闲,线 HIGH
|
||||
MASTER_SENDING, ///< ESP32 已下拉 SPIready,正在 CS#1 发送请求
|
||||
WAITING_RESPONSE, ///< CS#1 完成,等待 GD32 释放 SPIready(上升沿唤醒)
|
||||
SERVING_GD32_RING, ///< 响应 GD32 按铃,正在读 GD32 请求帧
|
||||
IDLE = 0, ///< 空闲,可接受 GD32 按铃
|
||||
MASTER_SENDING, ///< 主机正在 CS 发送
|
||||
WAITING_RESPONSE, ///< 主机正在 poll/读响应
|
||||
SERVING_GD32_RING, ///< 正在读 GD32 EVENT
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief SPI 链路状态机:封装握手状态 + 完成信号逻辑。
|
||||
* @brief 本端事务占用标记(v2)。
|
||||
*
|
||||
* 线状态语义(对齐 OTA):
|
||||
* - 线 HIGH = 空闲(双方释放),可发起新事务
|
||||
* - 线 LOW = 忙碌(某方下拉占用),禁止发起新事务
|
||||
*
|
||||
* ESP32 作为主动方(transact/appPing):enterBusy() 占用线 → transferClocked → exitBusy() 释放线
|
||||
* ESP32 作为按铃响应方(takeEvent):同上
|
||||
* DATA_READY(PB6)由 GD32 独占:低=有待读,高=队列空。
|
||||
* 本状态机只表示 ESP32 是否正占用 SPI 总线,避免与按铃读并发。
|
||||
*/
|
||||
class SpiLinkState {
|
||||
public:
|
||||
@@ -279,7 +271,7 @@ private:
|
||||
uint8_t m_eventTail = 0;
|
||||
uint8_t m_eventCount = 0;
|
||||
|
||||
// ── 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
|
||||
// ── 作业提交(跨任务:堆 Job + binary done;worker 串行执行)──
|
||||
enum class JobKind : uint8_t {
|
||||
Transact = 0,
|
||||
Poll,
|
||||
@@ -287,7 +279,6 @@ private:
|
||||
EnterBoot,
|
||||
Ping,
|
||||
WaitBoot,
|
||||
ServiceRing,
|
||||
};
|
||||
struct Job {
|
||||
JobKind kind = JobKind::Transact;
|
||||
@@ -305,12 +296,18 @@ private:
|
||||
gd32_version_t *out_version = nullptr;
|
||||
esp_err_t result = ESP_FAIL;
|
||||
SemaphoreHandle_t done = nullptr;
|
||||
/** submitJob 堆分配的 Job:超时放弃后由 worker 释放,成功路径由提交方释放。 */
|
||||
bool heap_allocated = false;
|
||||
volatile bool finished = false; ///< worker 已 execute + give
|
||||
volatile bool abandoned = false; ///< 提交方已超时返回,worker 负责 free
|
||||
};
|
||||
static constexpr UBaseType_t kJobQueueLen = 6;
|
||||
|
||||
bool onWorker() const;
|
||||
esp_err_t submitJob(Job &job);
|
||||
void executeJob(Job &job);
|
||||
/** @brief worker 完成堆 Job 后的生命周期收尾(give done + 可能 free)。 */
|
||||
void finishHeapJob(Job *job);
|
||||
static void workerEntry(void *arg);
|
||||
void workerLoop();
|
||||
esp_err_t serviceRingOnce();
|
||||
@@ -348,18 +345,6 @@ private:
|
||||
SemaphoreHandle_t m_lock;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。
|
||||
*
|
||||
* 调用前须将 m_linkState 置为 WAITING_RESPONSE(enterBusy),ISR 上升沿检测到该状态
|
||||
* 后发 0x02 通知唤醒本任务。GD32 写 Flash 期间保持 PB6 低,本函数阻塞,
|
||||
* 写完拉高 → 唤醒 → 继续,自然避免 overrun。
|
||||
*
|
||||
* @param timeout_ms 超时(毫秒),0 表示不等待。
|
||||
* @return ESP_OK 线已高;ESP_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
esp_err_t waitSpiReady(uint32_t timeout_ms);
|
||||
|
||||
/** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */
|
||||
static void IRAM_ATTR onReadyEdgeISR(void *arg);
|
||||
|
||||
@@ -386,12 +371,12 @@ private:
|
||||
uint32_t timeout_ms, uint32_t trans_flags);
|
||||
|
||||
/**
|
||||
* @brief 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
|
||||
* 调用者须已持锁;仅在 DATA_READY 已低(有数据)时调用,禁止盲读。
|
||||
* @brief 读一帧响应:单 CS 固定时钟预算(BOOT=128 / APP=256),再 v2_parse 截真实长度。
|
||||
* 调用者须已持锁。不用两段 KEEP_ACTIVE(Boot 中途断 CS 会清 pending → 全 0xFF)。
|
||||
* @param out_frame 完整帧输出(含头+payload+CRC)
|
||||
* @param out_max 输出缓冲容量
|
||||
* @param out_len 实际帧长
|
||||
* @param timeout_ms 单段 SPI 超时
|
||||
* @param timeout_ms 单次 SPI 超时
|
||||
*/
|
||||
esp_err_t readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
|
||||
uint32_t timeout_ms);
|
||||
@@ -417,13 +402,6 @@ private:
|
||||
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 bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *out, size_t *out_len);
|
||||
|
||||
Reference in New Issue
Block a user