diff --git a/main/core/main.cpp b/main/core/main.cpp index cfd9861..d3c03f7 100644 --- a/main/core/main.cpp +++ b/main/core/main.cpp @@ -1,6 +1,8 @@ -/* +/* * ESP32-C3FH4 OTA 中继 - 程序入口 - * 三个 FreeRTOS 任务:OTA Relay / WiFi Server / BLE GATT + * 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT + * + * 使用各模块的单例类进行初始化和任务创建。 */ #include #include "freertos/FreeRTOS.h" @@ -19,7 +21,7 @@ #include "monitor/debug_log.h" /** - * @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。 + * @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。 */ extern "C" void app_main(void) { @@ -31,13 +33,12 @@ extern "C" void app_main(void) ESP_ERROR_CHECK(esp_event_loop_create_default()); // 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建) - debug_log_init(); + DebugLog::instance().init(); // ★ WiFi 提前初始化(调度器启动前) - // 包括:NVS → netif 创建 → event handler 注册 → WiFi driver init → 配置 → connect - bool wifi_sta_ok = wifi_early_init(); + bool wifi_sta_ok = WifiOta::instance().earlyInit(); - health_monitor_init(); + HealthMonitor::instance().init(); DEBUG_INFO("MAIN", "健康监控状态已初始化"); DEBUG_INFO("MAIN", "========================================"); DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v1.0 启动"); @@ -50,7 +51,7 @@ extern "C" void app_main(void) // 健康监控任务(温度 + WiFi RSSI) BaseType_t xRet; xRet = xTaskCreatePinnedToCore( - health_monitor_task, + HealthMonitor::taskEntry, "health_mon", 4096, NULL, @@ -64,10 +65,10 @@ extern "C" void app_main(void) DEBUG_INFO("MAIN", "健康监控任务已创建"); } - protocol_init(); + Protocol::instance().init(); DEBUG_INFO("MAIN", "协议层初始化完成"); - esp_err_t ret = spi_master_init(); + esp_err_t ret = SpiMaster::instance().init(); if (ret != ESP_OK) { DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret); } else { @@ -76,7 +77,7 @@ extern "C" void app_main(void) // OTA 中继任务(中高优先级) xRet = xTaskCreatePinnedToCore( - ota_relay_task, + OtaRelay::taskEntry, "ota_relay", 8192, NULL, @@ -92,7 +93,7 @@ extern "C" void app_main(void) // WiFi OTA 服务器任务 xRet = xTaskCreatePinnedToCore( - wifi_ota_task, + WifiOta::taskEntry, "wifi_ota", 8192, NULL, @@ -108,7 +109,7 @@ extern "C" void app_main(void) // BLE OTA 任务 xRet = xTaskCreatePinnedToCore( - ble_ota_task, + BleOta::taskEntry, "ble_ota", 8192, NULL, @@ -124,8 +125,7 @@ extern "C" void app_main(void) DEBUG_INFO("MAIN", "========================================"); DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中"); - DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)", + DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)", DEBUG_LOG_IP, DEBUG_LOG_PORT); DEBUG_INFO("MAIN", "========================================"); -} - +} \ No newline at end of file diff --git a/main/include/app_config.h b/main/include/app_config.h index badbd5b..b33aa5b 100644 --- a/main/include/app_config.h +++ b/main/include/app_config.h @@ -10,7 +10,7 @@ // ============================================================ // 固件版本 // ============================================================ -inline constexpr const char *FW_VERSION = "0.2.11"; +inline constexpr const char *FW_VERSION = "0.3.0"; // ============================================================ // WiFi 工作模式选择 diff --git a/main/monitor/debug_log.cpp b/main/monitor/debug_log.cpp index b2d988b..9b80905 100644 --- a/main/monitor/debug_log.cpp +++ b/main/monitor/debug_log.cpp @@ -1,5 +1,5 @@ -/* - * debug_log.c - 调试日志输出实现 +/* + * debug_log.cpp - 调试日志输出实现 * * 日志会同时写入三处: * 1. UART0 串口(本地串口调试) @@ -11,7 +11,7 @@ * * 时序说明: * esp_netif_init() 创建 tcpip 线程后才可安全使用 socket - * 本模块在首次 debug_log_send() 时才创建 socket(延迟创建) + * 本模块在首次 send() 时才创建 socket(延迟创建) * WiFi 连接前 sendto() 会失败(无路由),日志仍可写入 UART 与 HTTP 日志缓冲 * WiFi 连接后 UDP 立即生效,无需额外操作 */ @@ -30,60 +30,39 @@ static const char *TAG = "DEBUG_LOG"; -/** @brief 当前调试日志过滤级别,运行时可动态调整。 */ -debug_log_level_t g_debug_log_level = DEBUG_LOG_INFO; - -/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */ -static int s_udp_sock = -1; -/** @brief UDP 调试日志目标地址。 */ -static struct sockaddr_in s_debug_addr; -/** @brief 日志模块初始化标志。 */ -static bool s_initialized = false; +// ============================================================ +// 单例 +// ============================================================ +DebugLog& DebugLog::instance() +{ + static DebugLog inst; + return inst; +} // ============================================================ // 日志环形缓冲区(供 HTTP /logs 拉取) // ============================================================ -#define LOG_RING_SLOTS 64 -#define LOG_RING_SIZE 256 -static char s_log_ring[LOG_RING_SLOTS][LOG_RING_SIZE]; -static int s_log_ring_lens[LOG_RING_SLOTS]; -static int s_log_ring_head = 0; // 下次写入位置 -static int s_log_ring_count = 0; // 已存条数(超过 LOG_RING_SLOTS 后不再增长,覆盖旧数据) -/** @brief 环形缓冲区互斥锁,保护多线程并发读写。 */ -static SemaphoreHandle_t s_log_ring_mutex = NULL; - -/** - * @brief 获取日志缓冲区中的条数 - * @return 当前日志条数。 - */ -int debug_log_ring_count(void) +int DebugLog::ringCount() { - if (!s_log_ring_mutex) return 0; - xSemaphoreTake(s_log_ring_mutex, portMAX_DELAY); - int count = s_log_ring_count; - xSemaphoreGive(s_log_ring_mutex); + if (!m_logRingMutex) return 0; + xSemaphoreTake(m_logRingMutex, portMAX_DELAY); + int count = m_logRingCount; + xSemaphoreGive(m_logRingMutex); return count; } -// 读取并清空日志缓冲区,返回 JSON 数组字符串 -// 调用者负责 free 返回值 -/** - * @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串。 - * @param out_len 输出 JSON 字符串长度(不含末尾 '\0')。 - * @return 成功返回需由调用者 free 的字符串;无日志或失败返回 NULL。 - */ -char *debug_log_ring_drain_json(int *out_len) +char *DebugLog::ringDrainJson(int *out_len) { - if (!s_log_ring_mutex) { + if (!m_logRingMutex) { *out_len = 0; return NULL; } - xSemaphoreTake(s_log_ring_mutex, portMAX_DELAY); + xSemaphoreTake(m_logRingMutex, portMAX_DELAY); - int count = s_log_ring_count; + int count = m_logRingCount; if (count == 0) { - xSemaphoreGive(s_log_ring_mutex); + xSemaphoreGive(m_logRingMutex); *out_len = 0; return NULL; } @@ -92,7 +71,7 @@ char *debug_log_ring_drain_json(int *out_len) size_t buf_size = (size_t)count * 280 + 16; char *json = (char *)malloc(buf_size); if (!json) { - xSemaphoreGive(s_log_ring_mutex); + xSemaphoreGive(m_logRingMutex); *out_len = 0; return NULL; } @@ -101,17 +80,17 @@ char *debug_log_ring_drain_json(int *out_len) json[pos++] = '['; // 从最旧的开始读 - int start = (count < LOG_RING_SLOTS) ? 0 : s_log_ring_head; + int start = (count < LOG_RING_SLOTS) ? 0 : m_logRingHead; for (int i = 0; i < count && pos < (int)buf_size - 200; i++) { int idx = (start + i) % LOG_RING_SLOTS; - int len = s_log_ring_lens[idx]; + int len = m_logRingLens[idx]; if (len <= 0) continue; if (i > 0) json[pos++] = ','; pos += snprintf(json + pos, buf_size - pos, "\""); for (int j = 0; j < len && pos < (int)buf_size - 10; j++) { - char c = s_log_ring[idx][j]; + char c = m_logRing[idx][j]; if (c == '"' || c == '\\') { json[pos++] = '\\'; json[pos++] = c; @@ -129,109 +108,85 @@ char *debug_log_ring_drain_json(int *out_len) json[pos] = '\0'; // 清空缓冲区 - s_log_ring_head = 0; - /** @brief 环形缓冲区中已存储的日志条数。 */ - s_log_ring_count = 0; + m_logRingHead = 0; + m_logRingCount = 0; - xSemaphoreGive(s_log_ring_mutex); + xSemaphoreGive(m_logRingMutex); *out_len = pos; return json; } -/** - * @brief 写入一条日志到环形缓冲区 - * @param data - * @param len - */ -static void log_ring_push(const char *data, int len) +void DebugLog::ringPush(const char *data, int len) { - if (!s_log_ring_mutex || len <= 0) return; + if (!m_logRingMutex || len <= 0) return; - xSemaphoreTake(s_log_ring_mutex, portMAX_DELAY); + xSemaphoreTake(m_logRingMutex, portMAX_DELAY); int copy_len = len; if (copy_len >= LOG_RING_SIZE) copy_len = LOG_RING_SIZE - 1; - memcpy(s_log_ring[s_log_ring_head], data, copy_len); - s_log_ring[s_log_ring_head][copy_len] = '\0'; - s_log_ring_lens[s_log_ring_head] = copy_len; + memcpy(m_logRing[m_logRingHead], data, copy_len); + m_logRing[m_logRingHead][copy_len] = '\0'; + m_logRingLens[m_logRingHead] = copy_len; - /** @brief 环形缓冲区下一个写入位置。 */ - s_log_ring_head = (s_log_ring_head + 1) % LOG_RING_SLOTS; - if (s_log_ring_count < LOG_RING_SLOTS) s_log_ring_count++; + m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS; + if (m_logRingCount < LOG_RING_SLOTS) m_logRingCount++; - xSemaphoreGive(s_log_ring_mutex); + xSemaphoreGive(m_logRingMutex); } -// UART0 状态 -static bool s_uart_initialized = false; -/** @brief 调试串口使用的 UART 端口号。 */ -static uart_port_t s_uart_port = UART_NUM_0; - // ============================================================ -// UART0 串口调试 +// UART 调试日志 // ============================================================ -/** - * @brief 初始化 UART0 调试串口,配置波特率和引脚映射。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t uart_debug_init(void) +esp_err_t DebugLog::uartInit() { - if (s_uart_initialized) { + if (m_uartInitialized) { return ESP_OK; } - uart_config_t uart_config; - memset(&uart_config, 0, sizeof(uart_config)); + const uart_port_t uart_port = UART_NUM_0; + uart_config_t uart_config = {}; uart_config.baud_rate = UART_DEBUG_BAUD_RATE; uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = UART_PARITY_DISABLE; + uart_config.parity = UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; - uart_config.rx_flow_ctrl_thresh = 0; - esp_err_t ret = uart_driver_install(s_uart_port, 1024, 0, 0, NULL, 0); + 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)); return ret; } - ret = uart_param_config(s_uart_port, &uart_config); + ret = uart_param_config(uart_port, &uart_config); if (ret != ESP_OK) { ESP_LOGE(TAG, "UART0 参数配置失败: %s", esp_err_to_name(ret)); return ret; } - ret = uart_set_pin(s_uart_port, UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, + ret = uart_set_pin(uart_port, UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); if (ret != ESP_OK) { ESP_LOGE(TAG, "UART0 引脚配置失败: %s", esp_err_to_name(ret)); return ret; } - /** @brief UART0 初始化标志。 */ - s_uart_initialized = true; + m_uartInitialized = true; ESP_LOGI(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_OK 成功,其他失败。 - */ -esp_err_t uart_debug_send(const uint8_t *data, size_t len) +esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len) { - if (!s_uart_initialized) { + if (!m_uartInitialized) { return ESP_ERR_INVALID_STATE; } - int ret = uart_write_bytes(s_uart_port, data, len); + int ret = uart_write_bytes(UART_NUM_0, data, len); if (ret < 0) { return ESP_FAIL; } @@ -240,50 +195,38 @@ esp_err_t uart_debug_send(const uint8_t *data, size_t len) } // ============================================================ -// UDP 调试日志 +// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建) // ============================================================ - -/** - * @brief 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建) - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t debug_log_init(void) +esp_err_t DebugLog::init() { - if (s_initialized) { + if (m_initialized) { return ESP_OK; } - uart_debug_init(); + uartInit(); - /** @brief 环形缓冲区互斥锁,保护多线程并发读写。 */ - s_log_ring_mutex = xSemaphoreCreateMutex(); + m_logRingMutex = xSemaphoreCreateMutex(); // 目标地址:单播到上位机 - memset(&s_debug_addr, 0, sizeof(s_debug_addr)); - s_debug_addr.sin_family = AF_INET; - s_debug_addr.sin_port = htons(DEBUG_LOG_PORT); - s_debug_addr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP); + memset(&m_debugAddr, 0, sizeof(m_debugAddr)); + m_debugAddr.sin_family = AF_INET; + m_debugAddr.sin_port = htons(DEBUG_LOG_PORT); + m_debugAddr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP); - /** @brief 日志模块初始化标志。 */ - s_initialized = true; + m_initialized = true; ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT); return ESP_OK; } -/** - * @brief 延迟创建 UDP socket(首次发送时调用) - * @return - */ -static esp_err_t ensure_udp_socket(void) +esp_err_t DebugLog::ensureUdpSocket() { - if (s_udp_sock >= 0) { + if (m_udpSock >= 0) { return ESP_OK; } - /** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */ - s_udp_sock = socket(AF_INET, SOCK_DGRAM, 0); - if (s_udp_sock < 0) { + m_udpSock = socket(AF_INET, SOCK_DGRAM, 0); + if (m_udpSock < 0) { ESP_LOGE(TAG, "创建 UDP socket 失败"); return ESP_FAIL; } @@ -292,32 +235,26 @@ static esp_err_t ensure_udp_socket(void) return ESP_OK; } -/** - * @brief 发送原始数据(UDP 单播 + UART 双通道) - * @param data 日志数据指针。 - * @param len 数据长度。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t debug_log_send(const uint8_t *data, size_t len) +esp_err_t DebugLog::send(const uint8_t *data, size_t len) { esp_err_t udp_ret = ESP_FAIL; esp_err_t uart_ret = ESP_FAIL; // UART 通道 - if (s_uart_initialized) { - uart_ret = uart_debug_send(data, len); + if (m_uartInitialized) { + uart_ret = uartSend(data, len); } // UDP 通道(WiFi 未连接时 sendto 失败,静默跳过) - if (s_initialized) { - if (s_udp_sock < 0) { - ensure_udp_socket(); + if (m_initialized) { + if (m_udpSock < 0) { + ensureUdpSocket(); } - if (s_udp_sock >= 0) { + if (m_udpSock >= 0) { size_t send_len = len > 1024 ? 1024 : len; - int ret = sendto(s_udp_sock, data, send_len, 0, - (struct sockaddr *)&s_debug_addr, sizeof(s_debug_addr)); + int ret = ::sendto(m_udpSock, data, send_len, 0, + (struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr)); if (ret >= 0) { udp_ret = ESP_OK; } @@ -325,7 +262,7 @@ esp_err_t debug_log_send(const uint8_t *data, size_t len) } // 同时写入环形缓冲区(供 HTTP /logs 拉取) - log_ring_push((const char *)data, (int)len); + ringPush((const char *)data, (int)len); if (udp_ret == ESP_OK || uart_ret == ESP_OK) { return ESP_OK; @@ -333,19 +270,13 @@ esp_err_t debug_log_send(const uint8_t *data, size_t len) return ESP_FAIL; } -/** - * @brief 格式化日志输出 - * @param level 日志级别。 - * @param tag 日志标签。 - * @param fmt 格式化字符串。 - */ -void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...) +void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...) { - if (!s_initialized) { - debug_log_init(); + if (!m_initialized) { + init(); } - if (level > g_debug_log_level) { + if (level > logLevel) { return; } @@ -369,5 +300,5 @@ void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...) char log_buf[600]; int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg); - debug_log_send((const uint8_t *)log_buf, total); -} + send((const uint8_t *)log_buf, total); +} \ No newline at end of file diff --git a/main/monitor/debug_log.h b/main/monitor/debug_log.h index 495fa51..527712a 100644 --- a/main/monitor/debug_log.h +++ b/main/monitor/debug_log.h @@ -1,4 +1,4 @@ -/* +/* * debug_log.h - 调试日志输出 * * 当前日志会并行写入: @@ -17,7 +17,11 @@ #include #include #include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "app_config.h" +#include +#include // 调试日志配置(DEBUG_LOG_IP/PORT/ENABLED 已在 app_config.h 中定义) @@ -36,57 +40,67 @@ typedef enum { DEBUG_LOG_ALL = 5 // 全部 } debug_log_level_t; -// 当前日志级别(运行时可改) -extern debug_log_level_t g_debug_log_level; - /** - * @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 - * @return ESP_OK 成功,其他失败。 + * @brief 调试日志类(单例) + * + * 封装 UART0、UDP 和环形缓冲区三路日志输出通道, + * 所有内部状态(socket、缓冲区、互斥锁等)均为私有成员。 */ -esp_err_t debug_log_init(void); +class DebugLog { +public: + static DebugLog& instance(); -/** - * @brief 初始化 UART0 调试串口。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t uart_debug_init(void); + /** @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 */ + esp_err_t init(); -/** - * @brief 通过 UART0 直接发送一段原始字节数据。 - * @param data 待发送的数据指针。 - * @param len 数据长度。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t uart_debug_send(const uint8_t *data, size_t len); + /** @brief 初始化 UART0 调试串口。 */ + esp_err_t uartInit(); -/** - * @brief 发送原始日志数据到底层输出通道。 - * @param data 日志数据指针。 - * @param len 数据长度。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t debug_log_send(const uint8_t *data, size_t len); + /** @brief 通过 UART0 直接发送一段原始字节数据。 */ + esp_err_t uartSend(const uint8_t *data, size_t len); -/** - * @brief 按日志级别与标签格式化输出一条日志。 - * @param level 日志级别。 - * @param tag 日志标签。 - * @param fmt 格式化字符串。 - */ -void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...); + /** @brief 发送原始日志数据到底层输出通道。 */ + esp_err_t send(const uint8_t *data, size_t len); -/** - * @brief 获取当前环形日志缓冲区中的日志条数。 - * @return 当前日志条数。 - */ -int debug_log_ring_count(void); + /** @brief 按日志级别与标签格式化输出一条日志。 */ + void log(debug_log_level_t level, const char *tag, const char *fmt, ...); -// 读取并清空环形日志缓冲区,返回 JSON 数组字符串,需由调用者 free。 -char *debug_log_ring_drain_json(int *out_len); // 调用者 free + /** @brief 获取当前环形日志缓冲区中的日志条数。 */ + int ringCount(); -#define DEBUG_ERR(tag, fmt, ...) debug_log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__) -#define DEBUG_WARN(tag, fmt, ...) debug_log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__) -#define DEBUG_INFO(tag, fmt, ...) debug_log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__) -#define DEBUG_DBG(tag, fmt, ...) debug_log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__) + /** @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串(需调用者 free)。 */ + char* ringDrainJson(int *out_len); + + /** @brief 当前日志级别(运行时可改)。 */ + debug_log_level_t logLevel = DEBUG_LOG_INFO; + +private: + DebugLog() = default; + ~DebugLog() = default; + DebugLog(const DebugLog&) = delete; + DebugLog& operator=(const DebugLog&) = delete; + + int m_udpSock = -1; + struct sockaddr_in m_debugAddr; + bool m_initialized = false; + bool m_uartInitialized = false; + + static constexpr int LOG_RING_SLOTS = 64; + static constexpr int LOG_RING_SIZE = 256; + 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; + + esp_err_t ensureUdpSocket(); + void ringPush(const char *data, int 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__) #endif // DEBUG_LOG_H \ No newline at end of file diff --git a/main/monitor/health_monitor.cpp b/main/monitor/health_monitor.cpp index 3a16c13..714e557 100644 --- a/main/monitor/health_monitor.cpp +++ b/main/monitor/health_monitor.cpp @@ -1,6 +1,8 @@ /* * health_monitor.cpp - 系统健康监控 * 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控 + * + * 所有功能封装为 HealthMonitor 类方法。 */ #include #include @@ -16,207 +18,173 @@ #include "debug_log.h" #include "server/wifi_ota.h" -/** @brief 全局健康状态,受互斥锁保护。 */ -health_state_t g_health_state = SYS_NORMAL; +// ============================================================ +// 静态成员 +// ============================================================ +health_state_t HealthMonitor::healthState = SYS_NORMAL; -/** @brief 健康状态互斥锁。 */ -static SemaphoreHandle_t g_health_mutex = NULL; -/** @brief 内置温度传感器句柄。 */ -static temperature_sensor_handle_t s_temp_handle = NULL; -/** @brief 最近一次采样的芯片温度(摄氏度)。 */ -static float s_last_temperature = 0.0f; -/** @brief 最近一次健康监控采样得到的 WiFi RSSI。 */ -static int8_t s_last_wifi_rssi = -127; -/** @brief 最近一次 WiFi RSSI 采样时间(毫秒)。 */ -static int64_t s_last_wifi_rssi_ms = 0; - -// ---------------------------------------- -// 健康状态管理 -/** - * @brief 初始化健康监控模块,创建互斥锁和基础状态。 - */ -void health_monitor_init(void) +// ============================================================ +// 单例 +// ============================================================ +HealthMonitor& HealthMonitor::instance() { - /** @brief 健康状态互斥锁。 */ - g_health_mutex = xSemaphoreCreateMutex(); - if (g_health_mutex == NULL) { + static HealthMonitor inst; + return inst; +} + +// ============================================================ +// 初始化 +// ============================================================ +void HealthMonitor::init() +{ + m_mutex = xSemaphoreCreateMutex(); + if (m_mutex == NULL) { DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败"); } } -/** - * @brief 获取当前系统健康状态。 - * @return 当前健康状态枚举值。 - */ -health_state_t health_get_current(void) +// ============================================================ +// 获取当前健康状态 +// ============================================================ +health_state_t HealthMonitor::getCurrent() { health_state_t state; - if (g_health_mutex) { - xSemaphoreTake(g_health_mutex, portMAX_DELAY); + if (m_mutex) { + xSemaphoreTake(m_mutex, portMAX_DELAY); } - state = g_health_state; - if (g_health_mutex) { - xSemaphoreGive(g_health_mutex); + state = healthState; + if (m_mutex) { + xSemaphoreGive(m_mutex); } return state; } -/** - * @brief 返回最近一次采样到的芯片温度(摄氏度)。 - * @return 最近一次温度采样值(摄氏度)。 - */ -float health_get_last_temperature(void) +float HealthMonitor::getLastTemperature() { - /** @brief 最近一次采样的芯片温度(摄氏度)。 */ - return s_last_temperature; + return m_lastTemperature; } -/** - * @brief 获取最近一次采样到的 WiFi RSSI 及其年龄。 - * @param out_rssi 输出 RSSI 指针,可为 NULL。 - * @param out_age_ms 输出采样距今毫秒数指针,可为 NULL。 - * @return 有有效采样返回 true,否则返回 false。 - */ -bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms) +bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms) { - if (s_last_wifi_rssi_ms <= 0) { + if (m_lastWifiRssiMs <= 0) { return false; } int64_t now_ms = esp_timer_get_time() / 1000; - uint32_t age_ms = (now_ms > s_last_wifi_rssi_ms) ? (uint32_t)(now_ms - s_last_wifi_rssi_ms) : 0; + uint32_t age_ms = (now_ms > m_lastWifiRssiMs) ? (uint32_t)(now_ms - m_lastWifiRssiMs) : 0; - if (out_rssi) *out_rssi = s_last_wifi_rssi; + if (out_rssi) *out_rssi = m_lastWifiRssi; if (out_age_ms) *out_age_ms = age_ms; return true; } -// ---------------------------------------- +// ============================================================ // 温度监控(定时器回调) -/** - * @brief 温度定时回调,采样并根据阈值更新健康状态。 - * @param arg - */ -static void temp_monitor_callback(void *arg) +// ============================================================ +void HealthMonitor::tempMonitorCallback(void *arg) { + auto& self = instance(); float temp = 0.0f; - esp_err_t err = temperature_sensor_get_celsius(s_temp_handle, &temp); + esp_err_t err = temperature_sensor_get_celsius(self.m_tempHandle, &temp); if (err != ESP_OK) { DEBUG_WARN("TEMP", "读取温度失败: %s", esp_err_to_name(err)); return; } - /** @brief 最近一次采样的芯片温度(摄氏度)。 */ - s_last_temperature = temp; + self.m_lastTemperature = temp; if (temp >= TEMP_CRITICAL) { - /** @brief 全局健康状态,受互斥锁保护。 */ - g_health_state = SYS_SAFE_SHUTDOWN; + healthState = SYS_SAFE_SHUTDOWN; DEBUG_ERR("TEMP", "!!! 危险 !!! 温度 %.1f°C >= %d°C,触发安全关机", temp, TEMP_CRITICAL); DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN"); } else if (temp >= TEMP_WARNING) { - if (g_health_state < SYS_WARNING) { - /** @brief 全局健康状态,受互斥锁保护。 */ - g_health_state = SYS_WARNING; + if (healthState < SYS_WARNING) { + healthState = SYS_WARNING; DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING); DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING"); } } } -// ---------------------------------------- -// 温度监控任务入口 -/** - * @brief 启动温度监控定时器,周期性巡检系统健康。 - * @param pvParameters FreeRTOS 任务参数,未使用。 - */ -void health_monitor_task(void *pvParameters) +// ============================================================ +// FreeRTOS 任务入口 +// ============================================================ +void HealthMonitor::taskEntry(void *pvParameters) { + auto& self = instance(); DEBUG_INFO("MON", "=== 健康监控任务启动 ==="); temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80); - esp_err_t err = temperature_sensor_install(&temp_config, &s_temp_handle); + esp_err_t err = temperature_sensor_install(&temp_config, &self.m_tempHandle); if (err != ESP_OK) { DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err)); } else { - ESP_ERROR_CHECK(temperature_sensor_enable(s_temp_handle)); + ESP_ERROR_CHECK(temperature_sensor_enable(self.m_tempHandle)); DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)"); } // 定时器,每1秒读取一次温度 esp_timer_handle_t temp_timer; esp_timer_create_args_t timer_args = {}; - timer_args.callback = &temp_monitor_callback; + timer_args.callback = &tempMonitorCallback; timer_args.arg = nullptr; timer_args.name = "temp_monitor"; timer_args.dispatch_method = ESP_TIMER_TASK; - timer_args.skip_unhandled_events = false; - esp_timer_create(&timer_args, &temp_timer); - esp_timer_start_periodic(temp_timer, 1 * 1000000); // 1秒 - DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 1 秒)"); + if (esp_timer_create(&timer_args, &temp_timer) == ESP_OK) { + esp_timer_start_periodic(temp_timer, 1000000); // 1秒 + DEBUG_INFO("MON", "温度监控定时器已启动 (1秒间隔)"); + } else { + DEBUG_ERR("MON", "创建温度监控定时器失败"); + } - // 保活循环:定时器由 esp_timer 任务驱动,此任务只需保持存活 - // 状态汇报已由 status_monitor_task 负责,此处只在异常时告警 + // 周期性巡检 while (1) { - int8_t rssi = wifi_get_rssi(); - if (wifi_is_connected() && rssi != -127) { - s_last_wifi_rssi = rssi; - s_last_wifi_rssi_ms = esp_timer_get_time() / 1000; - } - uint32_t free_heap = esp_get_free_heap_size(); if (free_heap < HEAP_WARNING_MIN) { - DEBUG_WARN("MON", "空闲堆内存不足: %lu < %d", (unsigned long)free_heap, HEAP_WARNING_MIN); + DEBUG_WARN("MON", "堆内存不足: %lu < %lu", free_heap, HEAP_WARNING_MIN); } - if (g_health_state >= SYS_CRITICAL) { - DEBUG_ERR("MON", "健康状态危险: %d", g_health_state); + if (healthState >= SYS_CRITICAL) { + DEBUG_ERR("MON", "健康状态危险: %d", healthState); } vTaskDelay(pdMS_TO_TICKS(5000)); } } -// ---------------------------------------- +// ============================================================ // 健康事件日志 -/** - * @brief 记录一条健康监控事件日志。 - * @param event 事件名称字符串。 - * @param value 事件关联的数值。 - */ -void health_log_event(const char *event, int value) +// ============================================================ +void HealthMonitor::logEvent(const char *event, int value) { DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value); } -// ---------------------------------------- -// OTA 传输前的健康检查 -/** - * @brief OTA 传输前执行健康检查,决定是否允许传输。 - * @return 健康检查结果,决定是否允许 OTA。 - */ -health_status_t health_check_before_ota(void) +// ============================================================ +// OTA 健康检查 +// ============================================================ +health_status_t HealthMonitor::checkBeforeOta() { health_status_t status = HEALTH_MON_OK; - if (g_health_state >= SYS_SAFE_SHUTDOWN) { + if (healthState >= SYS_SAFE_SHUTDOWN) { DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输"); return HEALTH_MON_SHUTDOWN; - } else if (g_health_state >= SYS_CRITICAL) { + } else if (healthState >= SYS_CRITICAL) { DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输"); return HEALTH_MON_CRITICAL; - } else if (g_health_state >= SYS_WARNING) { + } else if (healthState >= SYS_WARNING) { status = HEALTH_MON_WARNING; DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意"); } - if (!wifi_is_connected()) { + if (!WifiOta::isConnected()) { DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接"); return HEALTH_MON_CRITICAL; } int8_t last_rssi = -127; uint32_t age_ms = 0; - if (health_get_last_wifi_rssi(&last_rssi, &age_ms)) { + if (getLastWifiRssi(&last_rssi, &age_ms)) { if (age_ms <= OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS) { if (last_rssi < OTA_WIFI_RSSI_CRITICAL) { DEBUG_WARN("HEALTH", "OTA 前检查: 最近 RSSI %d < %d (样本年龄 %lu ms)", @@ -239,21 +207,21 @@ health_status_t health_check_before_ota(void) return status; } -health_status_t health_check_before_ota_relay(void) +health_status_t HealthMonitor::checkBeforeOtaRelay() { health_status_t status = HEALTH_MON_OK; - if (g_health_state >= SYS_SAFE_SHUTDOWN) { + if (healthState >= SYS_SAFE_SHUTDOWN) { DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_SAFE_SHUTDOWN,禁止传输"); return HEALTH_MON_SHUTDOWN; - } else if (g_health_state >= SYS_CRITICAL) { + } else if (healthState >= SYS_CRITICAL) { /* SYS_CRITICAL 可能是由 WiFi RSSI 过低导致的, * 但 SPI 中继阶段不依赖 WiFi,只检查温度/硬件等本地状态。 * 温度达到 TEMP_CRITICAL(100°C) 时 g_health_state 会被设为 * SYS_SAFE_SHUTDOWN,在上面的分支已经拦截。 * 所以 SYS_CRITICAL 在中继阶段只可能是 WiFi 导致的,允许继续。 */ DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_CRITICAL (WiFi 相关),SPI 中继允许继续"); - } else if (g_health_state >= SYS_WARNING) { + } else if (healthState >= SYS_WARNING) { status = HEALTH_MON_WARNING; DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_WARNING,继续传输"); } @@ -269,35 +237,30 @@ health_status_t health_check_before_ota_relay(void) return status; } -/** - * @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。 - * @param rssi 当前 WiFi 信号强度(dBm)。 - * @return 健康检查结果。 - */ -health_status_t health_update_from_wifi_rssi(int8_t rssi) +// ============================================================ +// WiFi RSSI 更新 +// ============================================================ +health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi) { if (rssi < WIFI_RSSI_CRITICAL) { - /** @brief 全局健康状态,受互斥锁保护。 */ - g_health_state = SYS_CRITICAL; + healthState = SYS_CRITICAL; DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL); - health_log_event("WIFI危险", rssi); + logEvent("WIFI危险", rssi); return HEALTH_MON_CRITICAL; } else if (rssi < WIFI_RSSI_NORMAL) { - if (g_health_state < SYS_WARNING) { - /** @brief 全局健康状态,受互斥锁保护。 */ - g_health_state = SYS_WARNING; + if (healthState < SYS_WARNING) { + healthState = SYS_WARNING; DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL); - health_log_event("WIFI警告", rssi); + logEvent("WIFI警告", rssi); } return HEALTH_MON_WARNING; } - if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) { - /** @brief 全局健康状态,受互斥锁保护。 */ - g_health_state = SYS_NORMAL; + if (healthState == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) { + healthState = SYS_NORMAL; DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL); - health_log_event("WIFI恢复", rssi); + logEvent("WIFI恢复", rssi); } return HEALTH_MON_OK; -} +} \ No newline at end of file diff --git a/main/monitor/health_monitor.h b/main/monitor/health_monitor.h index c68e11e..036d6be 100644 --- a/main/monitor/health_monitor.h +++ b/main/monitor/health_monitor.h @@ -1,9 +1,14 @@ /* * health_monitor.h - 系统健康监控头文件 + * + * HealthMonitor 类封装健康状态管理、温度监控和 WiFi RSSI 监控。 */ #pragma once #include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "driver/temperature_sensor.h" #include "app_config.h" // T9.3: 健康状态等级 @@ -21,69 +26,60 @@ typedef enum { HEALTH_MON_SHUTDOWN = 3 } health_status_t; -// 外部共享状态(互斥锁保护) -extern health_state_t g_health_state; - // WiFi RSSI 阈值(来自 app_config.h) #define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING #define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL /** - * @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 - */ -void health_monitor_init(void); - -/** - * @brief 健康监控任务入口,负责启动温度定时检测并周期性巡检系统状态。 - * @param pvParameters FreeRTOS 任务参数,未使用。 - */ -void health_monitor_task(void *pvParameters); - -/** - * @brief 在 OTA 传输开始前执行健康检查,返回当前是否允许传输。 - * @return 健康检查结果,决定是否允许 OTA。 - */ -health_status_t health_check_before_ota(void); - -/** - * @brief 在 OTA 中继转发阶段执行健康检查,跳过 WiFi 相关检查。 + * @brief 系统健康监控类(单例) * - * SPI 中继阶段固件已存储在 ESP32 Flash 中,不依赖 WiFi 连接, - * 因此跳过 WiFi 连接状态和 RSSI 检查,只检查温度等本地状态。 - * @return 健康检查结果,决定是否允许 OTA。 + * 封装健康状态管理、温度定时监控和 WiFi RSSI 监控。 */ -health_status_t health_check_before_ota_relay(void); +class HealthMonitor { +public: + static HealthMonitor& instance(); -/** - * @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。 - * @param event 事件名称字符串。 - * @param value 事件关联的数值。 - */ -void health_log_event(const char *event, int value); + /** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */ + void init(); -/** - * @brief 获取当前系统健康状态。 - * @return 当前健康状态枚举值。 - */ -health_state_t health_get_current(void); + /** @brief 健康监控 FreeRTOS 任务入口。 */ + static void taskEntry(void *pvParameters); -/** - * @brief 根据最新 WiFi RSSI 更新健康状态,并返回对应健康检查结果。 - * @param rssi 当前 WiFi 信号强度(dBm)。 - * @return 健康检查结果。 - */ -health_status_t health_update_from_wifi_rssi(int8_t rssi); + /** @brief OTA 传输前执行健康检查。 */ + health_status_t checkBeforeOta(); -/** - * @brief 获取最近一次采样到的芯片温度(摄氏度)。 - * @return 最近一次温度采样值(摄氏度)。 - */ -float health_get_last_temperature(void); + /** @brief OTA 中继转发阶段执行健康检查(跳过 WiFi 检查)。 */ + health_status_t checkBeforeOtaRelay(); -/** - * @brief 获取最近一次健康监控采样得到的 WiFi RSSI。 - * @param out_rssi 输出 RSSI。 - * @param out_age_ms 输出样本距今时长(毫秒)。 - * @return true 有有效样本,false 当前无有效样本。 - */ -bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms); + /** @brief 记录一条健康事件日志。 */ + void logEvent(const char *event, int value); + + /** @brief 获取当前系统健康状态。 */ + health_state_t getCurrent(); + + /** @brief 根据 WiFi RSSI 更新健康状态。 */ + health_status_t updateFromWifiRssi(int8_t rssi); + + /** @brief 获取最近一次芯片温度。 */ + float getLastTemperature(); + + /** @brief 获取最近一次 WiFi RSSI 及其年龄。 */ + bool getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms); + + /** @brief 全局健康状态(public,供外部读取)。 */ + static health_state_t healthState; + +private: + HealthMonitor() = default; + ~HealthMonitor() = default; + HealthMonitor(const HealthMonitor&) = delete; + HealthMonitor& operator=(const HealthMonitor&) = delete; + + SemaphoreHandle_t m_mutex = NULL; + temperature_sensor_handle_t m_tempHandle = NULL; + float m_lastTemperature = 0.0f; + int8_t m_lastWifiRssi = -127; + int64_t m_lastWifiRssiMs = 0; + + static void tempMonitorCallback(void *arg); +}; \ No newline at end of file diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 72a32bd..5e7d726 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -1,6 +1,8 @@ /* - * ota_relay.c - OTA 中继核心状态机实现 + * ota_relay.cpp - OTA 中继核心状态机实现 * T2.3, T2.4, T3.1 - T3.7 + * + * 所有功能封装为 OtaRelay 类方法。 */ #include #include @@ -19,156 +21,28 @@ #include "health_monitor.h" #include "monitor/debug_log.h" -/** @brief 当前 OTA 中继状态机状态。 */ -relay_state_t g_relay_state = RELAY_IDLE; -uint32_t g_relay_seq = 0; -uint32_t g_relay_total_packets = 0; -volatile bool g_gd32_app_alive = false; +// ============================================================ +// 静态成员定义 +// ============================================================ +relay_state_t OtaRelay::relayState = RELAY_IDLE; +uint32_t OtaRelay::relaySeq = 0; +uint32_t OtaRelay::relayTotalPackets = 0; +volatile bool OtaRelay::gd32AppAlive = false; +QueueHandle_t OtaRelay::dispatchQueue = NULL; +QueueHandle_t OtaRelay::resultQueue = NULL; -static const esp_partition_t *g_gd32_partition = NULL; -/** @brief OTA 认证会话上下文,包含会话密钥和 IV。 */ -static session_ctx_t g_session_ctx; -static uint8_t g_nonce_server[16] = {0}; -static uint8_t g_k_session[16] = {0}; -static uint8_t g_gd32_uid_runtime[12] = {0}; - -static constexpr size_t GD32_UID_LEN = 12; -static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36; - -/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */ -QueueHandle_t g_ota_dispatch_queue = NULL; -/** @brief OTA 结果回传队列,将执行结果返回给调用方。 */ -QueueHandle_t g_ota_result_queue = NULL; - -/** - * @brief OTA 握手阶段:向 GD32 发送握手请求并等待进入认证流程。 - * @return 操作结果码。 - */ -static ota_result_t send_handshake_req(void); - -/** - * @brief 认证阶段:生成随机数并完成与 GD32 的双向认证。 - * @return 操作结果码。 - */ -static ota_result_t send_nonce_and_authenticate(void); - -/** - * @brief 向 GD32 发送待升级固件的总大小。 - * @param size 固件总字节数。 - * @return 操作结果码。 - */ -static ota_result_t send_firmware_size(uint32_t size); - -/** - * @brief 从 Flash 逐包读取固件并中继转发给 GD32。 - * @return 操作结果码。 - */ -static ota_result_t relay_packets_from_flash(void); - -/** - * @brief OTA 收尾阶段:发送结束命令并等待 GD32 确认。 - * @return 操作结果码。 - */ -static ota_result_t finish_ota(void); -static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out); -static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len); -static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type, - uint8_t *resp_payload, size_t resp_max, - size_t *resp_len, uint32_t timeout_ms, - uint32_t max_wait_rounds); - -/** - * @brief 判断响应负载是否为带序号的结构化 ACK。 - * @param payload 响应负载指针。 - * @param len 负载长度。 - * @param expected_seq 期望的序号。 - * @return 匹配返回 true,否则返回 false。 - */ -static bool is_structured_ack_with_seq(const uint8_t *payload, size_t len, uint32_t expected_seq) +OtaRelay& OtaRelay::instance() { - if (!payload || len < 6) return false; - if (payload[0] != 0xBA || payload[1] != 0xC0) return false; - uint32_t seq = (uint32_t)payload[2] | ((uint32_t)payload[3] << 8) | - ((uint32_t)payload[4] << 16) | ((uint32_t)payload[5] << 24); - return seq == expected_seq; -} - -/** - * @brief 解析结构化错误响应中的序号和错误码。 - * @param payload 响应负载指针。 - * @param len 负载长度。 - * @param seq_out 输出错误对应序号,可为 NULL。 - * @param err_out 输出错误码,可为 NULL。 - * @return 解析成功返回 true,否则返回 false。 - */ -static bool parse_structured_error(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out) -{ - if (!payload || len < 7) return false; - if (payload[0] != 0xBA || payload[1] != 0xC0) return false; - if (seq_out) { - *seq_out = (uint32_t)payload[2] | ((uint32_t)payload[3] << 8) | - ((uint32_t)payload[4] << 16) | ((uint32_t)payload[5] << 24); - } - if (err_out) *err_out = payload[6]; - return true; -} - -/** - * @brief 判断缓冲区是否全部由指定字节值组成。 - * @param buf 输入缓冲区。 - * @param len 缓冲区长度。 - * @param value 目标字节值。 - * @return 全部匹配返回 true,否则返回 false。 - */ -static bool is_handshake_echo_ack(const uint8_t *payload, size_t len) -{ - return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU; -} - -static bool is_challenge_payload_ack(const uint8_t *payload, size_t len) -{ - return payload && len == GD32_CHALLENGE_RESPONSE_LEN; -} - -static bool is_confirm_plain_ack(const uint8_t *payload, size_t len) -{ - return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U; -} - -static bool is_size_seq0_ack(const uint8_t *payload, size_t len) -{ - return is_structured_ack_with_seq(payload, len, 0U); -} - -static bool is_finish_plain_ack(const uint8_t *payload, size_t len) -{ - return payload && len >= 2U && payload[0] == 0x0AU && payload[1] == 0xACU; -} - -/** - * @brief FINISH 阶段匹配器:接受 0x0A 0xAC 明文 ACK 或结构化错误帧。 - * - * BOOT 在 FINISH 成功时回 2B 明文 ACK {0x0A, 0xAC}; - * 失败时回 7B 结构化错误帧 {0xBA,0xC0, seq(4B), err_code}。 - * 两种都需要识别,否则 ESP32 会把错误帧当成残留 ACK 继续轮询。 - */ -static bool is_finish_ack_or_error(const uint8_t *payload, size_t len) -{ - if (!payload) return false; - /* 明文 ACK: 0x0A 0xAC */ - if (len >= 2U && payload[0] == 0x0AU && payload[1] == 0xACU) return true; - /* 结构化错误帧: 0xBA 0xC0 + seq(4B) + err_code */ - if (len >= 7U && payload[0] == 0xBAU && payload[1] == 0xC0U) return true; - return false; + static OtaRelay inst; + return inst; } -/** - * @brief 按小端序读取 4 字节无符号整数。 - * @param p 输入字节指针(至少 4 字节)。 - * @return 读取出的 uint32_t 数值。 - */ -static bool bytes_all_value(const uint8_t *buf, size_t len, uint8_t value) +// ============================================================ +// 内部工具函数 +// ============================================================ + +static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value) { if (!buf || len == 0) return false; for (size_t i = 0; i < len; ++i) { @@ -177,241 +51,28 @@ static bool bytes_all_value(const uint8_t *buf, size_t len, uint8_t value) return true; } -/** - * @brief 继续轮询,直到拿到符合当前阶段预期形状的 ACK,或遇到结构化错误/超时。 - */ -static esp_err_t poll_until_expected_ack_shape(const char *stage, - uint8_t *resp_type, - uint8_t *resp_payload, - size_t resp_max, - size_t *resp_len, - uint32_t poll_timeout_ms, - uint32_t wait_rounds, - uint32_t maxExtraPolls, - bool (*matcher)(const uint8_t *, size_t)) +int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out) { - if (!stage || !resp_type || !resp_payload || !resp_len || !matcher) { - return ESP_ERR_INVALID_ARG; - } - - esp_err_t err = ESP_OK; - if (*resp_type == SPI_OTA_RSP_WAIT) { - err = wait_until_not_wait(stage, resp_type, resp_payload, resp_max, resp_len, - poll_timeout_ms, wait_rounds); - if (err != ESP_OK) { - return err; - } - } - - for (uint32_t extra = 0; extra < maxExtraPolls; ++extra) { - if (*resp_type != SPI_OTA_RSP_ACK) { - return ESP_OK; - } - if (matcher(resp_payload, *resp_len)) { - return ESP_OK; - } - - uint32_t err_seq = 0; - uint8_t err_code = 0; - if (parse_structured_error(resp_payload, *resp_len, &err_seq, &err_code)) { - return ESP_OK; - } - - DEBUG_WARN("OTA", "%s 收到上一阶段残留ACK/非目标ACK,继续轮询: len=%u", stage, (unsigned)*resp_len); - *resp_len = 0; - err = spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, poll_timeout_ms); - if (err != ESP_OK) { - return err; - } - log_resp_payload(stage, *resp_type, resp_payload, *resp_len); - if (*resp_type == SPI_OTA_RSP_WAIT) { - err = wait_until_not_wait(stage, resp_type, resp_payload, resp_max, resp_len, - poll_timeout_ms, wait_rounds); - if (err != ESP_OK) { - return err; - } - } - } - - return ESP_OK; -} - -/** - * @brief 解析并校验 GD32 Challenge 响应,提取会话密钥和 UID。 - * @param resp_payload Challenge 响应数据。 - * @param resp_len 响应长度。 - * @param out_k_session 输出会话密钥(16 字节)。 - * @param out_uid 输出 UID(GD32_UID_LEN 字节)。 - * @return 校验通过返回 true,否则返回 false。 - */ -static bool parse_and_verify_challenge_payload(const uint8_t *resp_payload, size_t resp_len, - uint8_t *out_k_session, uint8_t *out_uid) -{ - if (!resp_payload || !out_k_session || !out_uid) return false; - - if (resp_len != GD32_CHALLENGE_RESPONSE_LEN) { - DEBUG_ERR("OTA", "CHALLENGE validate failed: invalid payload length=%u", (unsigned)resp_len); - return false; - } - - /* - * GD32 响应格式 (36 bytes): - * [0..11] = UID (12 bytes) - * [12..19] = nonce_server[8:16] (8 bytes, nonce 后半段) - * [20..35] = encrypted_Ksession (16 bytes) = AES(K_master, K_session) - * - * 其中 K_session = AES(K_master, nonce_server) - */ - - const uint8_t *uid = resp_payload; - const uint8_t *nonce_frag = resp_payload + 12; - const uint8_t *encrypted_ksession = resp_payload + 20; - - // 校验 nonce 片段: 应与 nonce_server[8:16] 一致 - if (memcmp(nonce_frag, g_nonce_server + 8, 8) != 0) { - DEBUG_ERR("OTA", "CHALLENGE校验失败: nonce片段不匹配"); - return false; - } - - // 派生 K_session = AES(K_master, nonce_server) - uint8_t K_session[16] = {0}; - if (aes128_ecb_encrypt_block_local(MASTER_KEY, g_nonce_server, K_session) != 0) { - DEBUG_ERR("OTA", "CHALLENGE校验失败: 派生K_session失败"); - return false; - } - - // 验证 encrypted_Ksession: AES(K_master, K_session) 应匹配 - uint8_t expected_encrypted[16] = {0}; - if (aes128_ecb_encrypt_block_local(MASTER_KEY, K_session, expected_encrypted) != 0) { - DEBUG_ERR("OTA", "CHALLENGE校验失败: 加密K_session失败"); - return false; - } - - if (memcmp(encrypted_ksession, expected_encrypted, 16) != 0) { - DEBUG_ERR("OTA", "CHALLENGE校验失败: encrypted_Ksession不匹配"); - return false; - } - - // 校验通过,输出 K_session 和 UID - memcpy(out_k_session, K_session, 16); - memcpy(out_uid, uid, GD32_UID_LEN); - - if (bytes_all_value(out_uid, GD32_UID_LEN, 0x00U) || - bytes_all_value(out_uid, GD32_UID_LEN, 0xFFU)) { - DEBUG_ERR("OTA", "CHALLENGE校验失败: UID无效(全00/全FF)"); - return false; - } - - char uid_hex[GD32_UID_LEN * 2 + 1] = {0}; - for (size_t i = 0; i < GD32_UID_LEN; ++i) { - snprintf(&uid_hex[i * 2], sizeof(uid_hex) - i * 2, "%02X", out_uid[i]); - } - DEBUG_INFO("OTA", - "GD32 UID(%uB @0x%08" PRIX32 "/0x%08" PRIX32 "/0x%08" PRIX32 "): %s", - (unsigned)GD32_UID_LEN, - DEVICE_SERIAL_ADDR_LSB, DEVICE_SERIAL_ADDR_MID, DEVICE_SERIAL_ADDR_MSB, - uid_hex); - - return true; -} - -/** - * @brief 使用 AES-128 ECB 加密单个 16 字节分组。 - * @param key 16 字节密钥。 - * @param in 输入明文分组(16 字节)。 - * @param out 输出密文分组(16 字节)。 - * @return 0 表示成功,非 0 表示失败。 - */ -static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out) -{ - esp_aes_context aes; - esp_aes_init(&aes); - int ret = esp_aes_setkey(&aes, key, 128); - if (ret == 0) { - ret = esp_aes_crypt_ecb(&aes, ESP_AES_ENCRYPT, in, out); - } - esp_aes_free(&aes); + esp_aes_context ctx; + esp_aes_init(&ctx); + if (esp_aes_setkey(&ctx, key, 128) != 0) { esp_aes_free(&ctx); return -1; } + int ret = esp_aes_crypt_ecb(&ctx, ESP_AES_ENCRYPT, in, out); + esp_aes_free(&ctx); return ret; } - -/** - * @brief 按 3 个分组对 48 字节数据执行 AES-128 ECB 加密。 - * @param key 16 字节密钥。 - * @param in 输入明文(48 字节)。 - * @param out 输出密文(48 字节)。 - * @return 全部分组加密成功返回 true,否则返回 false。 - */ -static bool aes128_ecb_encrypt_48bytes_local(const uint8_t *key, const uint8_t *in, uint8_t *out) -{ - for (int i = 0; i < 3; ++i) { - if (aes128_ecb_encrypt_block_local(key, in + i * 16, out + i * 16) != 0) { - return false; - } - } - return true; -} - -/** - * @brief 计算带 key 前缀的 CRC32(key + 48B 数据),输出小端 4 字节。 - * @param key 16 字节会话密钥。 - * @param data48 输入数据(48 字节)。 - * @param output 输出 CRC32(4 字节)。 - */ -static void calculate_crc32_with_key_local(const uint8_t *key, const uint8_t *data48, uint8_t *output) -{ - uint32_t crc = 0xFFFFFFFFU; - for (int i = 0; i < 16; ++i) { - crc ^= key[i]; - for (int j = 0; j < 8; ++j) { - crc = (crc >> 1) ^ (0xEDB88320U & (0U - (crc & 1U))); - } - } - for (size_t i = 0; i < GD32_OTA_PACKET_SIZE; ++i) { - crc ^= data48[i]; - for (int j = 0; j < 8; ++j) { - crc = (crc >> 1) ^ (0xEDB88320U & (0U - (crc & 1U))); - } - } - crc ^= 0xFFFFFFFFU; - output[0] = (uint8_t)(crc & 0xFFU); - output[1] = (uint8_t)((crc >> 8) & 0xFFU); - output[2] = (uint8_t)((crc >> 16) & 0xFFU); - output[3] = (uint8_t)((crc >> 24) & 0xFFU); -} - -/** - * @brief 记录响应类型和负载十六进制摘要,便于 OTA 阶段诊断。 - * @param stage 当前阶段名称。 - * @param resp_type 响应类型。 - * @param payload 响应负载。 - * @param len 响应负载长度。 - */ -static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len) +void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len) { char hex[193] = {0}; size_t n = (len > 32U) ? 32U : len; - for (size_t i = 0; i < n; ++i) { + for (size_t i = 0; i < n; ++i) snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]); - } - DEBUG_DBG("OTA", "%s 响应: type=0x%02X len=%u payload[%u]=%s%s", - stage, resp_type, (unsigned)len, (unsigned)n, - n > 0 ? hex : "", - len > n ? "..." : ""); + DEBUG_DBG("OTA", "%s resp: type=0x%02X len=%u payload[%u]=%s%s", + stage, resp_type, (unsigned)len, (unsigned)n, + n > 0 ? hex : "", len > n ? "..." : ""); } -/** - * @brief 当收到 WAIT 响应时进行只读轮询,直到响应变为非 WAIT 或达到轮询上限。 - * @param stage 当前阶段名称(用于日志)。 - * @param resp_type 输入/输出响应类型。 - * @param resp_payload 输出响应负载缓冲区。 - * @param resp_max 响应缓冲区大小。 - * @param resp_len 输出响应长度指针,可为 NULL。 - * @param timeout_ms 每次轮询的超时毫秒。 - * @param max_wait_rounds 最多轮询次数。 - * @return ESP_OK 或底层 SPI 轮询错误码。 - */ -static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type, +esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, uint32_t timeout_ms, uint32_t max_wait_rounds) { @@ -419,37 +80,144 @@ static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type, uint32_t round = 0; while (*resp_type == SPI_OTA_RSP_WAIT && round < max_wait_rounds) { ++round; - DEBUG_WARN("OTA", "%s 收到 WAIT,开始第 %u 次只读轮询...", stage, round); - /* GD32 擦除 Flash 期间 (__disable_irq) 无法响应 SPI, - * ESP32 的 poll 会持续拿到 0xFF 或 WAIT 帧残留字节, - * 导致 spi_ota_poll_response 超时返回 ESP_ERR_TIMEOUT。 - * - * 这是正常现象——只要还在 WAIT 轮询范围内,超时不代表协议失败, - * 只是 GD32 还在忙。继续重试即可。 - * - * 用 100ms 短超时做每次 poll,避免擦除期间长阻塞。 - * 300 轮 × 100ms = 30s,远超 2-3 秒的 Flash 擦除时间。 - * 实际上擦除完成后 ACK 会立即返回,不会真的等 30s。 */ - esp_err_t err = spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, 100); - if (err == ESP_ERR_TIMEOUT) { - /* 擦除期间 SPI 无响应 → 保持 WAIT 状态继续轮询 */ - *resp_type = SPI_OTA_RSP_WAIT; - continue; - } - if (err != ESP_OK) { - return err; - } - log_resp_payload(stage, *resp_type, resp_payload, resp_len ? *resp_len : 0); + DEBUG_WARN("OTA", "%s WAIT, poll #%u...", stage, round); + esp_err_t err = SpiMaster::instance().otaPollResponse(resp_type, resp_payload, resp_max, resp_len, 100); + if (err == ESP_ERR_TIMEOUT) { *resp_type = SPI_OTA_RSP_WAIT; continue; } + if (err != ESP_OK) return err; + logRespPayload(stage, *resp_type, resp_payload, resp_len ? *resp_len : 0); } return ESP_OK; } +bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq) +{ + if (!payload || len < 6) return false; + if (payload[0] != 0xBA || payload[1] != 0xC0) return false; + uint32_t seq = (uint32_t)payload[2] | ((uint32_t)payload[3] << 8) | + ((uint32_t)payload[4] << 16) | ((uint32_t)payload[5] << 24); + return seq == expected_seq; +} + +bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out) +{ + if (!payload || len < 7) return false; + if (payload[0] != 0xBA || payload[1] != 0xC0) return false; + if (seq_out) *seq_out = (uint32_t)payload[2] | ((uint32_t)payload[3] << 8) | + ((uint32_t)payload[4] << 16) | ((uint32_t)payload[5] << 24); + if (err_out) *err_out = payload[6]; + return true; +} + +bool OtaRelay::isHandshakeEchoAck(const uint8_t *payload, size_t len) +{ return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU; } + +bool OtaRelay::isChallengePayloadAck(const uint8_t *payload, size_t len) +{ return payload && len == GD32_CHALLENGE_RESPONSE_LEN; } + +bool OtaRelay::isConfirmPlainAck(const uint8_t *payload, size_t len) +{ return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U; } + +bool OtaRelay::isSizeSeq0Ack(const uint8_t *payload, size_t len) +{ return isStructuredAckWithSeq(payload, len, 0U); } + +bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len) +{ + if (!payload) return false; + if (len >= 2U && payload[0] == 0x0AU && payload[1] == 0xACU) return true; + if (len >= 7U && payload[0] == 0xBAU && payload[1] == 0xC0U) return true; + return false; +} + +bool OtaRelay::isStructuredAckWithPayload(const uint8_t *payload, size_t len) +{ return payload && len >= 6U && payload[0] == 0xBAU && payload[1] == 0xC0U; } + +esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage, + uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, + uint32_t poll_timeout_ms, uint32_t wait_rounds, + uint32_t maxExtraPolls, + bool (*matcher)(const uint8_t *, size_t)) +{ + if (!stage || !resp_type || !resp_payload || !resp_len || !matcher) + return ESP_ERR_INVALID_ARG; + esp_err_t err = ESP_OK; + if (*resp_type == SPI_OTA_RSP_WAIT) { + err = instance().waitUntilNotWait(stage, resp_type, resp_payload, resp_max, resp_len, poll_timeout_ms, wait_rounds); + if (err != ESP_OK) return err; + } + for (uint32_t extra = 0; extra < maxExtraPolls; ++extra) { + if (*resp_type != SPI_OTA_RSP_ACK) return ESP_OK; + if (matcher(resp_payload, *resp_len)) return ESP_OK; + uint32_t err_seq = 0; uint8_t err_code = 0; + if (parseStructuredError(resp_payload, *resp_len, &err_seq, &err_code)) return ESP_OK; + DEBUG_WARN("OTA", "%s stale ACK, poll more: len=%u", stage, (unsigned)*resp_len); + *resp_len = 0; + err = SpiMaster::instance().otaPollResponse(resp_type, resp_payload, resp_max, resp_len, poll_timeout_ms); + if (err != ESP_OK) return err; + logRespPayload(stage, *resp_type, resp_payload, *resp_len); + if (*resp_type == SPI_OTA_RSP_WAIT) { + err = instance().waitUntilNotWait(stage, resp_type, resp_payload, resp_max, resp_len, poll_timeout_ms, wait_rounds); + if (err != ESP_OK) return err; + } + } + return ESP_OK; +} + +bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len, + uint8_t *out_k_session, uint8_t *out_uid) +{ + if (!resp_payload || !out_k_session || !out_uid) return false; + if (resp_len != GD32_CHALLENGE_RESPONSE_LEN) { + DEBUG_ERR("OTA", "CHALLENGE: invalid len=%u", (unsigned)resp_len); return false; + } + const uint8_t *uid = resp_payload; + const uint8_t *nonce_frag = resp_payload + 12; + const uint8_t *encrypted_ksession = resp_payload + 20; + if (memcmp(nonce_frag, instance().m_nonceServer + 8, 8) != 0) { + DEBUG_ERR("OTA", "CHALLENGE: nonce mismatch"); return false; + } + uint8_t K_session[16] = {0}; + if (aes128EcbEncryptBlockLocal(MASTER_KEY, instance().m_nonceServer, K_session) != 0) { + DEBUG_ERR("OTA", "CHALLENGE: derive K_session failed"); return false; + } + uint8_t expected_encrypted[16] = {0}; + if (aes128EcbEncryptBlockLocal(MASTER_KEY, K_session, expected_encrypted) != 0) { + DEBUG_ERR("OTA", "CHALLENGE: encrypt K_session failed"); return false; + } + if (memcmp(encrypted_ksession, expected_encrypted, 16) != 0) { + DEBUG_ERR("OTA", "CHALLENGE: K_session mismatch"); return false; + } + memcpy(out_k_session, K_session, 16); + memcpy(out_uid, uid, GD32_UID_LEN); + if (bytesAllValue(out_uid, GD32_UID_LEN, 0x00U) || bytesAllValue(out_uid, GD32_UID_LEN, 0xFFU)) { + DEBUG_ERR("OTA", "CHALLENGE: UID all 0 or FF"); return false; + } + return true; +} + +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; + return true; +} + +void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output) +{ + uint32_t crc = 0xFFFFFFFFU; + for (int i = 0; i < 16; ++i) { crc ^= key[i]; for (int j = 0; j < 8; ++j) crc = (crc >> 1) ^ (0xEDB88320U & (0U - (crc & 1U))); } + for (size_t i = 0; i < 48U; ++i) { crc ^= data48[i]; for (int j = 0; j < 8; ++j) crc = (crc >> 1) ^ (0xEDB88320U & (0U - (crc & 1U))); } + crc ^= 0xFFFFFFFFU; + output[0] = (uint8_t)(crc & 0xFFU); output[1] = (uint8_t)((crc >> 8) & 0xFFU); + output[2] = (uint8_t)((crc >> 16) & 0xFFU); output[3] = (uint8_t)((crc >> 24) & 0xFFU); +} + /** * @brief 从 Flash 加载元数据 * @param meta 输出参数,加载后的元数据结构体。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t meta_load(ota_meta_t *meta) +esp_err_t OtaRelay::metaLoad(ota_meta_t *meta) { if (!meta) return ESP_ERR_INVALID_ARG; memset(meta, 0, sizeof(ota_meta_t)); @@ -476,7 +244,7 @@ esp_err_t meta_load(ota_meta_t *meta) * @param meta 待保存的元数据结构体。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t meta_save(const ota_meta_t *meta) +esp_err_t OtaRelay::metaSave(const ota_meta_t *meta) { if (!meta) return ESP_ERR_INVALID_ARG; @@ -506,27 +274,27 @@ esp_err_t meta_save(const ota_meta_t *meta) * @param last_seq 最后成功的分块序号。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq) +esp_err_t OtaRelay::metaUpdate(uint32_t stored_size, uint32_t last_seq) { ota_meta_t meta; - esp_err_t err = meta_load(&meta); + esp_err_t err = OtaRelay::metaLoad(&meta); if (err != ESP_OK) return err; meta.stored_size = stored_size; meta.last_seq = last_seq; - return meta_save(&meta); + return OtaRelay::metaSave(&meta); } /** * @brief OTA 完成后或中止时清除元数据 * @return ESP_OK 成功,其他失败。 */ -esp_err_t meta_clear(void) +esp_err_t OtaRelay::metaClear() { DEBUG_INFO("OTA", "清除 OTA 元数据"); ota_meta_t meta = {}; - return meta_save(&meta); + return OtaRelay::metaSave(&meta); } /** @@ -536,18 +304,18 @@ esp_err_t meta_clear(void) * @param len 数据长度(字节)。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len) +esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len) { - if (!g_gd32_partition) { - g_gd32_partition = esp_partition_find_first( + if (!instance().m_gd32Partition) { + instance().m_gd32Partition = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota"); - if (!g_gd32_partition) { + if (!instance().m_gd32Partition) { DEBUG_ERR("OTA", "找不到 gd32_ota 分区"); return ESP_ERR_NOT_FOUND; } } - return esp_partition_write(g_gd32_partition, offset, data, len); + return esp_partition_write(instance().m_gd32Partition, offset, data, len); } /** @@ -557,28 +325,28 @@ esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len) * @param len 读取长度(字节)。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len) +esp_err_t OtaRelay::flashReadChunk(uint32_t offset, uint8_t *data, size_t len) { - if (!g_gd32_partition) { - g_gd32_partition = esp_partition_find_first( + if (!instance().m_gd32Partition) { + instance().m_gd32Partition = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota"); - if (!g_gd32_partition) { + if (!instance().m_gd32Partition) { DEBUG_ERR("OTA", "找不到 gd32_ota 分区"); return ESP_ERR_NOT_FOUND; } } - return esp_partition_read(g_gd32_partition, offset, data, len); + return esp_partition_read(instance().m_gd32Partition, offset, data, len); } /** * @brief 阶段1 - 发起与 GD32 的握手 * @return 操作结果码。 */ -static void request_gd32_app_enter_bootloader(void) +void OtaRelay::requestGd32AppEnterBootloader() { DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口"); - esp_err_t err = gd32_app_request_bootloader(500); + esp_err_t err = SpiMaster::instance().appRequestBootloader(500); if (err == ESP_OK) { /* 收到确认:可能来自 APP(即将跳转 BOOT)或 BOOT 本身(已在 BOOT)。 * 如果来自 APP,GD32 需要复位并重新初始化 SPI 外设,需等 ~1-2s; @@ -591,7 +359,7 @@ static void request_gd32_app_enter_bootloader(void) } } -static ota_result_t send_handshake_req(void) +ota_result_t OtaRelay::sendHandshakeReq() { DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ==="); DEBUG_INFO("OTA", "发送 HANDSHAKE_REQ (0x00 0xAC)"); @@ -606,7 +374,7 @@ static ota_result_t send_handshake_req(void) constexpr uint32_t kHandshakeRetryGapMs = 100U; /** @brief 当前 OTA 中继状态机状态。 */ - g_relay_state = RELAY_HANDSHAKE_SENT; + relayState = RELAY_HANDSHAKE_SENT; DEBUG_INFO("OTA", "状态: RELAY_HANDSHAKE_SENT"); for (uint32_t attempt = 0U; attempt < kHandshakeAttempts; ++attempt) { @@ -614,7 +382,7 @@ static ota_result_t send_handshake_req(void) uint8_t resp_payload[64] = {0}; size_t resp_len = 0U; - esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, payload, sizeof(payload), + esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, payload, sizeof(payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, kHandshakeReqTimeoutMs); last_err = err; @@ -622,15 +390,15 @@ static ota_result_t send_handshake_req(void) last_resp_len = resp_len; if (err == ESP_OK) { - log_resp_payload("HANDSHAKE_REQ", resp_type, resp_payload, resp_len); - err = poll_until_expected_ack_shape("HANDSHAKE_REQ", &resp_type, resp_payload, + logRespPayload("HANDSHAKE_REQ", resp_type, resp_payload, resp_len); + err = pollUntilExpectedAckShape("HANDSHAKE_REQ", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, - kHandshakePollTimeoutMs, 2, 2, is_handshake_echo_ack); + kHandshakePollTimeoutMs, 2, 2, OtaRelay::isHandshakeEchoAck); last_err = err; last_resp_type = resp_type; last_resp_len = resp_len; - if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK && is_handshake_echo_ack(resp_payload, resp_len)) { + if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK && isHandshakeEchoAck(resp_payload, resp_len)) { DEBUG_INFO("OTA", "握手请求已确认 (attempt=%lu)", (unsigned long)(attempt + 1U)); return OTA_RESULT_OK; } @@ -647,7 +415,7 @@ static ota_result_t send_handshake_req(void) DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d, resp_type=0x%02X, resp_len=%zu", last_err, last_resp_type, last_resp_len); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } @@ -655,70 +423,70 @@ static ota_result_t send_handshake_req(void) * @brief 发送随机数并与 GD32 相互认证 * @return 操作结果码。 */ -static ota_result_t send_nonce_and_authenticate(void) +ota_result_t OtaRelay::sendNonceAndAuthenticate() { uint8_t resp_type = 0; uint8_t resp_payload[64] = {0}; size_t resp_len = 0; uint32_t timestamp = xTaskGetTickCount(); - uint32_t mem_addr = (uint32_t)(uintptr_t)g_nonce_server; + uint32_t mem_addr = (uint32_t)(uintptr_t)m_nonceServer; uint32_t rand_seed = 0x12345678; - scramble_generate(timestamp, mem_addr, rand_seed, g_nonce_server); + Protocol::scrambleGenerate(timestamp, mem_addr, rand_seed, m_nonceServer); DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---"); DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08X", timestamp); - g_relay_state = RELAY_CHALLENGE_SENT; + relayState = RELAY_CHALLENGE_SENT; DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_SENT"); - esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, g_nonce_server, 16, + esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, m_nonceServer, 16, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000); - log_resp_payload("CHALLENGE", resp_type, resp_payload, resp_len); - err = poll_until_expected_ack_shape("CHALLENGE", &resp_type, resp_payload, + logRespPayload("CHALLENGE", resp_type, resp_payload, resp_len); + err = pollUntilExpectedAckShape("CHALLENGE", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, - 1000, 12, 8, is_challenge_payload_ack); - if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !is_challenge_payload_ack(resp_payload, resp_len)) { + 1000, 12, 8, OtaRelay::isChallengePayloadAck); + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isChallengePayloadAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } - g_relay_state = RELAY_CHALLENGE_RESPOND; + relayState = RELAY_CHALLENGE_RESPOND; DEBUG_INFO("OTA", "已收到 GD32 握手数据包"); DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_RESPOND"); - if (!parse_and_verify_challenge_payload(resp_payload, resp_len, g_k_session, g_gd32_uid_runtime)) { + if (!parseAndVerifyChallengePayload(resp_payload, resp_len, m_kSession, m_gd32UidRuntime)) { DEBUG_ERR("OTA", "CHALLENGE 解包/校验失败"); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_AUTH; } uint8_t plaintext[16]; for (int i = 0; i < 16; ++i) { - plaintext[i] = g_nonce_server[i] ^ 0x55U; + plaintext[i] = m_nonceServer[i] ^ 0x55U; } uint8_t encrypted_confirm[16] = {0}; - if (aes128_ecb_encrypt_block_local(g_k_session, plaintext, encrypted_confirm) != 0) { + if (aes128EcbEncryptBlockLocal(m_kSession, plaintext, encrypted_confirm) != 0) { DEBUG_ERR("OTA", "加密确认块失败"); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_AUTH; } DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY/CONFIRM ---"); - g_relay_state = RELAY_SESSION_KEY_SENT; + relayState = RELAY_SESSION_KEY_SENT; DEBUG_INFO("OTA", "状态: RELAY_SESSION_KEY_SENT"); resp_len = 0; - err = spi_ota_transfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16, + err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); - log_resp_payload("CONFIRM", resp_type, resp_payload, resp_len); - err = poll_until_expected_ack_shape("CONFIRM", &resp_type, resp_payload, + logRespPayload("CONFIRM", resp_type, resp_payload, resp_len); + err = pollUntilExpectedAckShape("CONFIRM", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, - 1000, 8, 8, is_confirm_plain_ack); - if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !is_confirm_plain_ack(resp_payload, resp_len)) { + 1000, 8, 8, OtaRelay::isConfirmPlainAck); + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isConfirmPlainAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "SESSION_KEY/CONFIRM 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } @@ -732,11 +500,11 @@ static ota_result_t send_nonce_and_authenticate(void) DEBUG_INFO("OTA", "CONFIRM 验证通过, resp_len=%zu, payload=%s", resp_len, hex); } - g_relay_state = RELAY_AUTH_COMPLETE; + relayState = RELAY_AUTH_COMPLETE; DEBUG_INFO("OTA", "状态: RELAY_AUTH_COMPLETE"); DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ==="); - session_key_init(&g_session_ctx, g_k_session); + Protocol::sessionKeyInit(&m_sessionCtx, m_kSession); return OTA_RESULT_OK; } @@ -745,7 +513,7 @@ static ota_result_t send_nonce_and_authenticate(void) * @param size 固件总字节数。 * @return 操作结果码。 */ -static ota_result_t send_firmware_size(uint32_t size) +ota_result_t OtaRelay::sendFirmwareSize(uint32_t size) { uint8_t resp_type = 0; uint8_t resp_payload[32] = {0}; @@ -760,24 +528,24 @@ static ota_result_t send_firmware_size(uint32_t size) size_payload[2] = (uint8_t)((size >> 16) & 0xFF); size_payload[3] = (uint8_t)((size >> 24) & 0xFF); - g_relay_state = RELAY_SIZE_SENT; + relayState = RELAY_SIZE_SENT; DEBUG_INFO("OTA", "状态: RELAY_SIZE_SENT"); - esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload), + esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); - log_resp_payload("SEND_SIZE", resp_type, resp_payload, resp_len); + logRespPayload("SEND_SIZE", resp_type, resp_payload, resp_len); DEBUG_WARN("OTA", "SEND_SIZE poll config: poll_timeout_ms=2000 wait_rounds=300 maxExtraPolls=24"); - err = poll_until_expected_ack_shape("SEND_SIZE", &resp_type, resp_payload, + err = pollUntilExpectedAckShape("SEND_SIZE", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, - 2000, 300, 24, is_size_seq0_ack); - if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !is_size_seq0_ack(resp_payload, resp_len)) { + 2000, 300, 24, OtaRelay::isSizeSeq0Ack); + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isSizeSeq0Ack(resp_payload, resp_len)) { DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } DEBUG_INFO("OTA", "大小已发送: %u 字节,等待 GD32 确认...", size); - g_relay_state = RELAY_SIZE_ACK; + relayState = RELAY_SIZE_ACK; DEBUG_INFO("OTA", "状态: RELAY_SIZE_ACK"); return OTA_RESULT_OK; @@ -787,12 +555,12 @@ static ota_result_t send_firmware_size(uint32_t size) * @brief 从 Flash 向 GD32 转发存储的固件(48B/包) * @return 操作结果码。 */ -static ota_result_t relay_packets_from_flash(void) +ota_result_t OtaRelay::relayPacketsFromFlash() { DEBUG_INFO("OTA", "=== 阶段3: 从 Flash 转发固件 ==="); ota_meta_t meta; - if (meta_load(&meta) != ESP_OK) { + if (OtaRelay::metaLoad(&meta) != ESP_OK) { DEBUG_ERR("OTA", "元数据加载失败"); return OTA_RESULT_ERR_STORAGE; } @@ -807,14 +575,14 @@ static ota_result_t relay_packets_from_flash(void) DEBUG_INFO("OTA", "总包数: %u, 每包 %u 字节", total_packets, (unsigned)GD32_OTA_PACKET_SIZE); DEBUG_INFO("OTA", "状态: RELAY_RELAY_ACTIVE"); - g_relay_state = RELAY_RELAY_ACTIVE; - g_relay_total_packets = total_packets; - g_relay_seq = 0; + relayState = RELAY_RELAY_ACTIVE; + relayTotalPackets = total_packets; + relaySeq = 0; for (uint32_t seq = 0; seq < total_packets; seq++) { uint32_t retry_count = 0; if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == total_packets - 1) { - health_status_t health = health_check_before_ota_relay(); + health_status_t health = HealthMonitor::instance().checkBeforeOtaRelay(); if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) { DEBUG_ERR("OTA", "健康检查危险,中止 OTA"); return OTA_RESULT_ERR_HEALTH; @@ -828,7 +596,7 @@ static ota_result_t relay_packets_from_flash(void) chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE); } - esp_err_t err = flash_read_chunk(flash_offset, plain48, chunk_size); + esp_err_t err = OtaRelay::flashReadChunk(flash_offset, plain48, chunk_size); if (err != ESP_OK) { DEBUG_ERR("OTA", "Flash 读取失败, offset=%u, err=%d", flash_offset, err); return OTA_RESULT_ERR_STORAGE; @@ -840,7 +608,7 @@ static ota_result_t relay_packets_from_flash(void) tx_buf[6] = 0x00; tx_buf[7] = 0x00; - if (!aes128_ecb_encrypt_48bytes_local(g_k_session, plain48, &tx_buf[8])) { + if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) { DEBUG_ERR("OTA", "包 %u 加密失败", seq); return OTA_RESULT_ERR_AUTH; } @@ -850,22 +618,22 @@ static ota_result_t relay_packets_from_flash(void) tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU); tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU); tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU); - calculate_crc32_with_key_local(g_k_session, plain48, &tx_buf[60]); + calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]); DEBUG_DBG("OTA", "包 %u: chunk=%u if_end=0x%08X crc32=%02X%02X%02X%02X", seq, (unsigned)chunk_size, (unsigned)if_end, tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]); - g_relay_state = RELAY_PACKET_SENT; + relayState = RELAY_PACKET_SENT; resend_packet: while (1) { resp_len = 0; - err = spi_ota_transfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf), + err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); - log_resp_payload("DATA_PACKET", resp_type, resp_payload, resp_len); + logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len); if (err == ESP_OK) { - err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10); + err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10); } if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) { DEBUG_WARN("OTA", "数据包 %u 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len); @@ -880,13 +648,13 @@ static ota_result_t relay_packets_from_flash(void) * 典型场景:SIZE 阶段的 ACK(seq=0) 仍挂在 DMA 上, * 包 0 发出后第一次 poll 读到 seq=0 而非期望的 seq=1。 * 继续轮询直到拿到正确的 ACK 或超时。 */ - if (!is_structured_ack_with_seq(resp_payload, resp_len, expected_ack)) { + if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { uint32_t err_seq = 0; uint8_t err_code = 0; - if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code)) { + if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) { if (err_seq == seq) { DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code); - g_relay_state = RELAY_PACKET_ACK_RETRY; + relayState = RELAY_PACKET_ACK_RETRY; if (++retry_count >= 3U) { DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32; @@ -902,15 +670,15 @@ static ota_result_t relay_packets_from_flash(void) bool got_expected = false; for (uint32_t retry = 0; retry < 5; ++retry) { DEBUG_WARN("OTA", "包 %u 收到非目标ACK(seq不匹配),继续轮询(%u/5)...", seq, retry + 1); - err = spi_ota_poll_response(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000); + err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000); if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) { - err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5); + err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5); } if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) { /* poll 超时或收到非 ACK(WAIT/ERROR)-> GD32 Flash 缓冲可能已满, * 正在排空。重发当前包让 GD32 重新入队处理。 */ DEBUG_WARN("OTA", "包 %u poll 超时/非ACK,重发 (retry=%u/3)", seq, retry_count + 1); - g_relay_state = RELAY_PACKET_ACK_RETRY; + relayState = RELAY_PACKET_ACK_RETRY; if (++retry_count >= 3U) { DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); result = OTA_RESULT_ERR_GD32; @@ -918,11 +686,11 @@ static ota_result_t relay_packets_from_flash(void) } goto resend_packet; } - if (is_structured_ack_with_seq(resp_payload, resp_len, expected_ack)) { + if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { got_expected = true; break; } - if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { + if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { break; /* 下面统一处理 */ } } @@ -930,14 +698,14 @@ static ota_result_t relay_packets_from_flash(void) if (got_expected) { /* 匹配成功 */ flash_offset += (uint32_t)chunk_size; - g_relay_state = RELAY_PACKET_ACK_OK; + relayState = RELAY_PACKET_ACK_OK; break; } /* 最终检查是否为错误帧 */ - if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { + if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code); - g_relay_state = RELAY_PACKET_ACK_RETRY; + relayState = RELAY_PACKET_ACK_RETRY; if (++retry_count >= 3U) { DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32; @@ -948,7 +716,7 @@ static ota_result_t relay_packets_from_flash(void) /* 轮询耗尽仍未拿到期望 ACK -> 重发当前包 */ DEBUG_WARN("OTA", "包 %u 轮询耗尽未匹配,重发 (retry=%u/3)", seq, retry_count + 1); - g_relay_state = RELAY_PACKET_ACK_RETRY; + relayState = RELAY_PACKET_ACK_RETRY; if (++retry_count >= 3U) { DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); result = OTA_RESULT_ERR_GD32; @@ -959,7 +727,7 @@ static ota_result_t relay_packets_from_flash(void) /* 匹配成功 */ flash_offset += (uint32_t)chunk_size; - g_relay_state = RELAY_PACKET_ACK_OK; + relayState = RELAY_PACKET_ACK_OK; break; } @@ -967,13 +735,13 @@ static ota_result_t relay_packets_from_flash(void) break; } - g_relay_seq = seq + 1; + relaySeq = seq + 1; if (seq % (total_packets / 10 + 1) == 0) { - uint32_t progress = (g_relay_seq * 100U) / total_packets; - DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)", g_relay_seq, total_packets, progress); + uint32_t progress = (relaySeq * 100U) / total_packets; + DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)", relaySeq, total_packets, progress); } } - g_relay_state = RELAY_RECEIVE_DONE; + relayState = RELAY_RECEIVE_DONE; DEBUG_DBG("OTA", "状态: RELAY_RECEIVE_DONE"); return result; } @@ -982,7 +750,7 @@ static ota_result_t relay_packets_from_flash(void) * @brief 结束 OTA - 发送 0x0A AC 并清理 * @return 操作结果码。 */ -static ota_result_t finish_ota(void) +ota_result_t OtaRelay::finishOta() { DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ==="); DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP"); @@ -992,52 +760,52 @@ static ota_result_t finish_ota(void) size_t resp_len = 0; uint8_t finish_payload[2] = {0x0A, 0xAC}; - g_relay_state = RELAY_FINISH_SENT; - esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), + relayState = RELAY_FINISH_SENT; + esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); - log_resp_payload("FINISH_OTA", resp_type, resp_payload, resp_len); - err = poll_until_expected_ack_shape("FINISH_OTA", &resp_type, resp_payload, + logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len); + err = pollUntilExpectedAckShape("FINISH_OTA", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, - 1000, 10, 8, is_finish_ack_or_error); + 1000, 10, 8, OtaRelay::isFinishPlainAck); if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || - !is_finish_ack_or_error(resp_payload, resp_len)) { + !isFinishPlainAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "FINISH_OTA 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_GD32; } /* 检查是否为结构化错误帧(BOOT validateApplication 失败等) */ uint32_t err_seq = 0; uint8_t err_code = 0; - if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code)) { + if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) { DEBUG_ERR("OTA", "FINISH_OTA BOOT 返回错误: seq=0x%08lX err=0x%02X (%s)", (unsigned long)err_seq, err_code, err_code == 0x02U ? "FLASH_ERROR/validateApp失败" : err_code == 0x04U ? "INVALID_PACKET" : err_code == 0x03U ? "INVALID_KEY" : "未知错误"); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_GD32; } /* 正常明文 ACK: 0x0A 0xAC */ - if (!is_finish_plain_ack(resp_payload, resp_len)) { + if (!isFinishPlainAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "FINISH_OTA 响应格式异常: len=%zu payload[0]=0x%02X", resp_len, resp_payload[0]); - g_relay_state = RELAY_ERROR; + relayState = RELAY_ERROR; return OTA_RESULT_ERR_GD32; } - g_relay_state = RELAY_DONE; - session_key_clear(&g_session_ctx); - memset(&g_session_ctx, 0, sizeof(session_ctx_t)); - memset(g_k_session, 0, sizeof(g_k_session)); - memset(g_nonce_server, 0, sizeof(g_nonce_server)); - memset(g_gd32_uid_runtime, 0, sizeof(g_gd32_uid_runtime)); + relayState = RELAY_DONE; + 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)); DEBUG_INFO("OTA", "状态: RELAY_DONE"); DEBUG_INFO("OTA", "=== OTA 完成,等待 GD32 APP 启动 ==="); - meta_clear(); + OtaRelay::metaClear(); /* ============================================================ * 阶段5: APP 存活验证 @@ -1047,15 +815,15 @@ static ota_result_t finish_ota(void) * 我们以 500ms 间隔反复 ping APP,最多等 15 秒。 * 收到 APP_ALIVE 回复才认为 OTA 真正成功。 * ============================================================ */ - g_relay_state = RELAY_APP_VERIFY; - g_gd32_app_alive = false; + relayState = RELAY_APP_VERIFY; + gd32AppAlive = false; for (uint32_t attempt = 0; attempt < 30; ++attempt) { vTaskDelay(pdMS_TO_TICKS(500)); uint32_t uptime = 0; - esp_err_t ping_err = gd32_app_ping(1000, &uptime); + esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime); if (ping_err == ESP_OK) { - g_gd32_app_alive = true; + gd32AppAlive = true; DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec", (unsigned long)uptime); DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ==="); break; @@ -1063,7 +831,7 @@ static ota_result_t finish_ota(void) DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%u/30)", attempt + 1); } - if (!g_gd32_app_alive) { + if (!gd32AppAlive) { DEBUG_WARN("OTA", "GD32 APP 存活确认超时,OTA 传输已完成但 APP 未启动"); } @@ -1074,27 +842,27 @@ static ota_result_t finish_ota(void) * @brief 启动完整 OTA 流程的主入口 * @return 操作结果码。 */ -ota_result_t ota_relay_start_handshake(void) +ota_result_t OtaRelay::startHandshake() { ota_result_t res; - request_gd32_app_enter_bootloader(); + requestGd32AppEnterBootloader(); - res = send_handshake_req(); + res = sendHandshakeReq(); if (res != OTA_RESULT_OK) return res; - res = send_nonce_and_authenticate(); + res = sendNonceAndAuthenticate(); if (res != OTA_RESULT_OK) return res; ota_meta_t meta; - meta_load(&meta); - res = send_firmware_size(meta.total_size); + OtaRelay::metaLoad(&meta); + res = sendFirmwareSize(meta.total_size); if (res != OTA_RESULT_OK) return res; - res = relay_packets_from_flash(); + res = relayPacketsFromFlash(); if (res != OTA_RESULT_OK) return res; - res = finish_ota(); + res = finishOta(); return res; } @@ -1103,31 +871,31 @@ ota_result_t ota_relay_start_handshake(void) * @brief 从 Flash 向 GD32 转发固件 * @return 操作结果码。 */ -ota_result_t ota_relay_forward_firmware(void) +ota_result_t OtaRelay::forwardFirmware() { ota_meta_t meta; - if (meta_load(&meta) != ESP_OK) { + if (OtaRelay::metaLoad(&meta) != ESP_OK) { return OTA_RESULT_ERR_STORAGE; } DEBUG_INFO("OTA", "开始固件转发: %u 字节, %u 块", meta.total_size, meta.chunk_count); - request_gd32_app_enter_bootloader(); + requestGd32AppEnterBootloader(); - ota_result_t res = send_handshake_req(); + ota_result_t res = sendHandshakeReq(); if (res != OTA_RESULT_OK) return res; - res = send_nonce_and_authenticate(); + res = sendNonceAndAuthenticate(); if (res != OTA_RESULT_OK) return res; - res = send_firmware_size(meta.total_size); + res = sendFirmwareSize(meta.total_size); if (res != OTA_RESULT_OK) return res; - res = relay_packets_from_flash(); + res = relayPacketsFromFlash(); if (res != OTA_RESULT_OK) return res; - res = finish_ota(); + res = finishOta(); return res; } @@ -1136,42 +904,42 @@ ota_result_t ota_relay_forward_firmware(void) * @brief 获取当前状态 * @return 当前中继状态枚举值。 */ -relay_state_t ota_relay_get_state(void) +relay_state_t OtaRelay::getState() { /** @brief 当前 OTA 中继状态机状态。 */ - return g_relay_state; + return relayState; } /** * @brief 重置状态机 */ -void ota_relay_reset(void) +void OtaRelay::reset() { /** @brief 当前 OTA 中继状态机状态。 */ - g_relay_state = RELAY_IDLE; - g_relay_seq = 0; - g_relay_total_packets = 0; - session_key_clear(&g_session_ctx); - memset(&g_session_ctx, 0, sizeof(session_ctx_t)); - memset(g_k_session, 0, sizeof(g_k_session)); - memset(g_nonce_server, 0, sizeof(g_nonce_server)); - memset(g_gd32_uid_runtime, 0, sizeof(g_gd32_uid_runtime)); + relayState = RELAY_IDLE; + relaySeq = 0; + relayTotalPackets = 0; + 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)); } /** * @brief OTA 中继 FreeRTOS 任务 * @param pvParameters FreeRTOS 任务参数,未使用。 */ -void ota_relay_task(void *pvParameters) +void OtaRelay::taskEntry(void *pvParameters) { DEBUG_INFO("OTA", "OTA 中继任务启动"); /** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */ - g_ota_dispatch_queue = xQueueCreate(4, sizeof(uint32_t)); + dispatchQueue = xQueueCreate(4, sizeof(uint32_t)); /** @brief OTA 结果回传队列,将执行结果返回给调用方。 */ - g_ota_result_queue = xQueueCreate(4, sizeof(uint32_t)); + resultQueue = xQueueCreate(4, sizeof(uint32_t)); - if (!g_ota_dispatch_queue || !g_ota_result_queue) { + if (!dispatchQueue || !resultQueue) { DEBUG_ERR("OTA", "OTA 队列创建失败"); vTaskDelete(NULL); return; @@ -1179,23 +947,23 @@ void ota_relay_task(void *pvParameters) while (1) { uint32_t msg = 0; - BaseType_t ret = xQueueReceive(g_ota_dispatch_queue, &msg, portMAX_DELAY); + BaseType_t ret = xQueueReceive(dispatchQueue, &msg, portMAX_DELAY); if (ret != pdTRUE) { continue; } DEBUG_INFO("OTA", "收到 OTA 调度信号: %lu,开始转发...", (unsigned long)msg); - ota_result_t result = ota_relay_forward_firmware(); + ota_result_t result = instance().forwardFirmware(); uint32_t result_msg = (uint32_t)result; - xQueueSend(g_ota_result_queue, &result_msg, pdMS_TO_TICKS(100)); + xQueueSend(resultQueue, &result_msg, pdMS_TO_TICKS(100)); if (result == OTA_RESULT_OK) { DEBUG_INFO("OTA", "OTA 转发成功完成"); - ota_relay_reset(); + instance().reset(); } else { DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result); - ota_relay_reset(); + instance().reset(); } } } diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index a0347ec..f99e4b2 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -1,6 +1,8 @@ /* * ota_relay.h - OTA 中继核心状态机 * T2.3, T3.1 - T3.7 + * + * OtaRelay 类封装 OTA 中继状态机、Flash 存储和元数据管理。 */ #ifndef OTA_RELAY_H #define OTA_RELAY_H @@ -8,151 +10,133 @@ #include #include "freertos/FreeRTOS.h" #include "freertos/queue.h" +#include "transport/protocol.h" +#include "esp_partition.h" -// T4.3: OTA 元数据结构,用于 NVS 存储 #define OTA_META_NVS_NAMESPACE "ota_meta" #define OTA_META_NVS_KEY "meta" typedef struct { - uint32_t total_size; // 固件总大小(字节) - uint32_t stored_size; // 当前已存储大小(字节) - uint32_t last_seq; // 最后成功存储的序列号 - uint32_t chunk_count; // 预期总块数 - uint8_t checksum[32]; // 固件 SHA256 校验(可选) + uint32_t total_size; + uint32_t stored_size; + uint32_t last_seq; + uint32_t chunk_count; + uint8_t checksum[32]; } ota_meta_t; -// OTA 中继状态(对应 GD32 四阶段协议) typedef enum { - // 阶段0: 空闲 RELAY_IDLE = 0, - - // 阶段1: 安全握手 - RELAY_HANDSHAKE_IDLE, // 等待 HANDSHAKE_REQ - RELAY_HANDSHAKE_SENT, // 已发送 0x00 AC,等待 GD32 响应 - RELAY_CHALLENGE_SENT, // 已发送 nonce_server (16B),等待响应 - RELAY_CHALLENGE_RESPOND, // 已收到 GD32 握手数据包 - RELAY_SESSION_KEY_SENT, // 已发送变换后的随机数,等待 0xBA C0 - RELAY_AUTH_COMPLETE, // 已收到 0xBA C0,安全通道建立完成 - - // 阶段2: 传输准备 - RELAY_SIZE_SENT, // 已发送固件大小(4B LE) - RELAY_SIZE_ACK, // 已收到 0xBA C0 + seq - - // 阶段3: 数据传输 - RELAY_RECEIVING, // 接收并存储固件到 Flash - RELAY_RELAY_ACTIVE, // 存储完成,开始向 GD32 转发 - RELAY_PACKET_SENT, // 已发送 DATA_PACKET,等待 GD32 ACK - RELAY_PACKET_ACK_OK, // ACK 正常,继续下一包 - RELAY_PACKET_ACK_RETRY, // ACK 要求重发,重新发送当前包 - RELAY_RECEIVE_DONE, // 已发送 DATA_END,等待 TRANSFER_DONE - - // 阶段4: 结束跳转 - RELAY_TRANSFER_DONE, // 已收到 0xBA C0 + FF FF FF FF - RELAY_FINISH_SENT, // 已发送 0x0A AC,等待 GD32 确认 - RELAY_DONE, // GD32 回复 0x0A AC,OTA 完成 - RELAY_APP_VERIFY, // 等待 GD32 APP 启动并回复存活确认 - - // 错误状态 + RELAY_HANDSHAKE_IDLE, + RELAY_HANDSHAKE_SENT, + RELAY_CHALLENGE_SENT, + RELAY_CHALLENGE_RESPOND, + RELAY_SESSION_KEY_SENT, + RELAY_AUTH_COMPLETE, + RELAY_SIZE_SENT, + RELAY_SIZE_ACK, + RELAY_RECEIVING, + RELAY_RELAY_ACTIVE, + RELAY_PACKET_SENT, + RELAY_PACKET_ACK_OK, + RELAY_PACKET_ACK_RETRY, + RELAY_RECEIVE_DONE, + RELAY_TRANSFER_DONE, + RELAY_FINISH_SENT, + RELAY_DONE, + RELAY_APP_VERIFY, RELAY_ERROR = 0xFF } relay_state_t; -// OTA 结果码 typedef enum { OTA_RESULT_OK = 0, - OTA_RESULT_ERR_TIMEOUT = -1, // GD32 超时无响应 - OTA_RESULT_ERR_CRC = -2, // CRC 校验失败 - OTA_RESULT_ERR_AUTH = -3, // 认证失败 - OTA_RESULT_ERR_STORAGE = -4, // Flash 存储错误 - OTA_RESULT_ERR_GD32 = -5, // GD32 返回错误 - OTA_RESULT_ERR_HEALTH = -6, // 健康状态异常中止 - OTA_RESULT_ERR_USER_ABORT = -7 // 用户中止 + OTA_RESULT_ERR_TIMEOUT = -1, + OTA_RESULT_ERR_CRC = -2, + OTA_RESULT_ERR_AUTH = -3, + OTA_RESULT_ERR_STORAGE = -4, + OTA_RESULT_ERR_GD32 = -5, + OTA_RESULT_ERR_HEALTH = -6, + OTA_RESULT_ERR_USER_ABORT = -7 } ota_result_t; -// 外部队列句柄(在 app_main 中创建) -extern QueueHandle_t g_ota_dispatch_queue; // 从 WiFi/BLE 到 OTA 中继 -extern QueueHandle_t g_ota_result_queue; // OTA 结果到 WiFi/BLE +class OtaRelay { +public: + static OtaRelay& instance(); -/** - * @brief OTA 中继任务入口,负责执行握手、认证、分包转发和结束流程。 - * @param pvParameters FreeRTOS 任务参数,未使用。 - */ -void ota_relay_task(void *pvParameters); + static void taskEntry(void *pvParameters); + ota_result_t startHandshake(); + ota_result_t forwardFirmware(); + relay_state_t getState(); + void reset(); -/** - * @brief 发起与 GD32 的握手流程,建立 OTA 传输前的安全会话。 - * @return 操作结果码。 - */ -ota_result_t ota_relay_start_handshake(void); + static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len); + static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len); + static esp_err_t metaLoad(ota_meta_t *meta); + static esp_err_t metaSave(const ota_meta_t *meta); + static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq); + static esp_err_t metaClear(); -/** - * @brief 从 Flash 中读取已缓存固件并按协议转发给 GD32。 - * @return 操作结果码。 - */ -ota_result_t ota_relay_forward_firmware(void); + // 公开状态 + static relay_state_t relayState; + static uint32_t relaySeq; + static uint32_t relayTotalPackets; + static volatile bool gd32AppAlive; + static QueueHandle_t dispatchQueue; + static QueueHandle_t resultQueue; -/** - * @brief 获取当前 OTA 中继状态机状态。 - * @return 当前中继状态枚举值。 - */ -relay_state_t ota_relay_get_state(void); + // 实例状态(public,供状态机方法访问) + const esp_partition_t *m_gd32Partition = NULL; + session_ctx_t m_sessionCtx; + uint8_t m_nonceServer[16] = {0}; + uint8_t m_kSession[16] = {0}; + uint8_t m_gd32UidRuntime[12] = {0}; -/** - * @brief 重置 OTA 中继状态机,清理当前状态以便重新开始流程。 - */ -void ota_relay_reset(void); + static constexpr size_t GD32_UID_LEN = 12; + static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36; -/** - * @brief 向 OTA 缓存分区写入一段固件数据。 - * @param offset 分区内写入偏移。 - * @param data 待写入的数据指针。 - * @param len 数据长度(字节)。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len); + ota_result_t sendHandshakeReq(); + ota_result_t sendNonceAndAuthenticate(); + ota_result_t sendFirmwareSize(uint32_t size); + ota_result_t relayPacketsFromFlash(); + ota_result_t finishOta(); -/** - * @brief 从 OTA 缓存分区读取一段固件数据。 - * @param offset 分区内读取偏移。 - * @param data 读取缓冲区指针。 - * @param len 读取长度(字节)。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len); + 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); + 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, + uint32_t max_wait_rounds); -/** - * @brief 从持久化文件中加载 OTA 元数据。 - * @param meta 输出参数,加载后的元数据结构体。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t meta_load(ota_meta_t *meta); + static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq); + static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out); + static bool isHandshakeEchoAck(const uint8_t *payload, size_t len); + static bool isChallengePayloadAck(const uint8_t *payload, size_t len); + static bool isConfirmPlainAck(const uint8_t *payload, size_t len); + static bool isSizeSeq0Ack(const uint8_t *payload, size_t len); + static bool isFinishPlainAck(const uint8_t *payload, size_t len); + static bool isTransferDoneAck(const uint8_t *payload, size_t len); + static bool isStructuredAckWithPayload(const uint8_t *payload, size_t len); -/** - * @brief 将 OTA 元数据保存到持久化文件。 - * @param meta 待保存的元数据结构体。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t meta_save(const ota_meta_t *meta); + static esp_err_t pollUntilExpectedAckShape(const char *stage, + uint8_t *resp_type, + uint8_t *resp_payload, + size_t resp_max, + size_t *resp_len, + uint32_t poll_timeout_ms, + uint32_t wait_rounds, + uint32_t maxExtraPolls, + bool (*matcher)(const uint8_t *, size_t)); + static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len, + uint8_t *out_k_session, uint8_t *out_uid); + static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out); + static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output); -/** - * @brief 更新已存储大小与最后分块序号等 OTA 进度信息。 - * @param stored_size 已存储的固件大小(字节)。 - * @param last_seq 最后成功的分块序号。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq); + void requestGd32AppEnterBootloader(); -/** - * @brief 清空 OTA 元数据,通常在完成或中止后调用。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t meta_clear(void); - -// GD32 APP 存活标志(OTA 完成后由 APP PING 确认) -extern volatile bool g_gd32_app_alive; - -// 当前中继状态(供健康监控外部访问) -extern relay_state_t g_relay_state; -extern uint32_t g_relay_seq; -extern uint32_t g_relay_total_packets; +private: + OtaRelay() = default; + ~OtaRelay() = default; + OtaRelay(const OtaRelay&) = delete; + OtaRelay& operator=(const OtaRelay&) = delete; +}; #endif // OTA_RELAY_H \ No newline at end of file diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index 5be4bb5..8cc4962 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -1,5 +1,6 @@ -/* +/* * ble_ota.cpp - BLE OTA GATT 服务器实现 (ESP-IDF 6.0 NimBLE) + * 所有功能封装为 BleOta 类方法。 * T5.1 - T5.5 * * 通过 BLE GATT 接收上位机固件,支持两种目标: @@ -57,27 +58,25 @@ // 前向声明(GAP event handler 在广播函数之前定义,需要声明) // ============================================================ static void ble_ota_start_advertising(void); -static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes); -static void ble_ota_send_finish_event(uint8_t stage, uint32_t value); +static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes); +static void sendFinishEvent(uint8_t stage, uint32_t value); // ============================================================ // 静态状态 // ============================================================ -static bool s_ble_ready = false; // NimBLE 同步完成 -static bool s_ble_connected = false; // GATT 客户端已连接 -/** @brief 当前 BLE 连接句柄。 */ -static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE; -static uint16_t s_data_out_val_handle = 0; // DATA_OUT 值句柄 (供通知用) + +// ============================================================ +BleOta& BleOta::instance() +{ + static BleOta inst; + return inst; +} + +// File-scope val_handle for GATT service definition (struct literal needs compile-time address) +static uint16_t s_data_out_val_handle = 0; // ESP32 自身 OTA 状态 -static bool s_esp32_ota_active = false; -static esp_ota_handle_t s_esp32_ota_handle = 0; -static const esp_partition_t *s_esp32_update_partition = NULL; -static size_t s_esp32_received = 0; -static bool s_esp32_reboot_pending = false; -static uint8_t s_esp32_last_cmd = 0; -static uint16_t s_esp32_last_seq = 0xFFFFU; /** * @brief 异步完成 ESP32 OTA 收尾并触发重启。 @@ -88,80 +87,80 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters) (void)pvParameters; DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动"); - if (s_esp32_last_seq != 0xFFFFU) { - ble_ota_send_window_ack(s_esp32_last_cmd, s_esp32_last_seq, (uint32_t)s_esp32_received); - DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", s_esp32_last_seq); + if (BleOta::instance().m_esp32LastSeq != 0xFFFFU) { + sendWindowAck(BleOta::instance().m_esp32LastCmd, BleOta::instance().m_esp32LastSeq, (uint32_t)BleOta::instance().m_esp32Received); + DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", BleOta::instance().m_esp32LastSeq); } vTaskDelay(pdMS_TO_TICKS(150)); - if (!s_esp32_ota_active || s_esp32_update_partition == NULL) { + if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) { DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启"); - s_esp32_reboot_pending = false; + BleOta::instance().m_esp32RebootPending = false; vTaskDelete(NULL); return; } - DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)s_esp32_received); + DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)BleOta::instance().m_esp32Received); int64_t t0_us = esp_timer_get_time(); DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_end()"); - ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_OTA_END, 0); - esp_err_t err = esp_ota_end(s_esp32_ota_handle); + sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0); + esp_err_t err = esp_ota_end(BleOta::instance().m_esp32OtaHandle); int64_t t1_us = esp_timer_get_time(); if (err != ESP_OK) { DEBUG_ERR(TAG, "[FINISH] esp_ota_end 失败: %s (耗时=%lld ms)", esp_err_to_name(err), (long long)((t1_us - t0_us) / 1000)); - ble_ota_send_finish_event(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err); - esp_ota_abort(s_esp32_ota_handle); - s_esp32_ota_active = false; - s_esp32_ota_handle = 0; - s_esp32_update_partition = NULL; - s_esp32_received = 0; - s_esp32_reboot_pending = false; + sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err); + esp_ota_abort(BleOta::instance().m_esp32OtaHandle); + BleOta::instance().m_esp32OtaActive = false; + BleOta::instance().m_esp32OtaHandle = 0; + BleOta::instance().m_esp32UpdatePartition = NULL; + BleOta::instance().m_esp32Received = 0; + BleOta::instance().m_esp32RebootPending = false; vTaskDelete(NULL); return; } DEBUG_INFO(TAG, "[FINISH] esp_ota_end 完成 (耗时=%lld ms)", (long long)((t1_us - t0_us) / 1000)); - ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_OTA_END, (uint32_t)((t1_us - t0_us) / 1000)); + sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, (uint32_t)((t1_us - t0_us) / 1000)); int64_t t2_us = esp_timer_get_time(); DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_set_boot_partition()"); - ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_SET_BOOT, 0); - err = esp_ota_set_boot_partition(s_esp32_update_partition); + sendFinishEvent(BLE_OTA_FINISH_BEGIN_SET_BOOT, 0); + err = esp_ota_set_boot_partition(BleOta::instance().m_esp32UpdatePartition); int64_t t3_us = esp_timer_get_time(); if (err != ESP_OK) { DEBUG_ERR(TAG, "[FINISH] 设置启动分区失败: %s (耗时=%lld ms)", esp_err_to_name(err), (long long)((t3_us - t2_us) / 1000)); - ble_ota_send_finish_event(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err); - s_esp32_ota_active = false; - s_esp32_ota_handle = 0; - s_esp32_update_partition = NULL; - s_esp32_received = 0; - s_esp32_reboot_pending = false; + sendFinishEvent(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err); + BleOta::instance().m_esp32OtaActive = false; + BleOta::instance().m_esp32OtaHandle = 0; + BleOta::instance().m_esp32UpdatePartition = NULL; + BleOta::instance().m_esp32Received = 0; + BleOta::instance().m_esp32RebootPending = false; vTaskDelete(NULL); return; } DEBUG_INFO(TAG, "[FINISH] esp_ota_set_boot_partition 完成 (耗时=%lld ms)", (long long)((t3_us - t2_us) / 1000)); - ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000)); + sendFinishEvent(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000)); - s_esp32_ota_active = false; - s_esp32_ota_handle = 0; - s_esp32_update_partition = NULL; - s_esp32_received = 0; + BleOta::instance().m_esp32OtaActive = false; + BleOta::instance().m_esp32OtaHandle = 0; + BleOta::instance().m_esp32UpdatePartition = NULL; + BleOta::instance().m_esp32Received = 0; DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,800ms 后重启..."); int64_t t4_us = esp_timer_get_time(); DEBUG_INFO(TAG, "[FINISH] 即将进入 800ms 重启前等待"); - ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_DELAY, 800); + sendFinishEvent(BLE_OTA_FINISH_BEGIN_DELAY, 800); vTaskDelay(pdMS_TO_TICKS(800)); int64_t t5_us = esp_timer_get_time(); DEBUG_INFO(TAG, "[FINISH] 800ms 等待结束 (实际耗时=%lld ms)", (long long)((t5_us - t4_us) / 1000)); - ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_DELAY, (uint32_t)((t5_us - t4_us) / 1000)); + sendFinishEvent(BLE_OTA_FINISH_DONE_DELAY, (uint32_t)((t5_us - t4_us) / 1000)); DEBUG_INFO(TAG, "ESP32 即将重启"); - ble_ota_send_finish_event(BLE_OTA_FINISH_REBOOT_NOW, 0); + sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0); esp_restart(); } @@ -194,56 +193,56 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size) DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx", boot_partition->label, boot_partition->size, boot_partition->address); - if (s_esp32_ota_active) { + if (BleOta::instance().m_esp32OtaActive) { DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA,会先中止旧会话"); - esp_ota_abort(s_esp32_ota_handle); - s_esp32_ota_active = false; - s_esp32_ota_handle = 0; - s_esp32_update_partition = NULL; - s_esp32_received = 0; - s_esp32_reboot_pending = false; - s_esp32_last_seq = 0xFFFFU; + esp_ota_abort(BleOta::instance().m_esp32OtaHandle); + BleOta::instance().m_esp32OtaActive = false; + BleOta::instance().m_esp32OtaHandle = 0; + BleOta::instance().m_esp32UpdatePartition = NULL; + BleOta::instance().m_esp32Received = 0; + BleOta::instance().m_esp32RebootPending = false; + BleOta::instance().m_esp32LastSeq = 0xFFFFU; } - s_esp32_update_partition = esp_partition_find_first( + BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first( ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL); - if (s_esp32_update_partition == NULL || - s_esp32_update_partition->address == boot_partition->address) { - s_esp32_update_partition = esp_partition_find_first( + if (BleOta::instance().m_esp32UpdatePartition == NULL || + BleOta::instance().m_esp32UpdatePartition->address == boot_partition->address) { + BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first( ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); } - if (s_esp32_update_partition == NULL) { + if (BleOta::instance().m_esp32UpdatePartition == NULL) { DEBUG_ERR(TAG, "找不到可用的 OTA 分区"); return ESP_FAIL; } DEBUG_INFO(TAG, "ESP32 OTA 目标分区: %s, size=%u, address=0x%lx", - s_esp32_update_partition->label, - s_esp32_update_partition->size, - s_esp32_update_partition->address); + BleOta::instance().m_esp32UpdatePartition->label, + BleOta::instance().m_esp32UpdatePartition->size, + BleOta::instance().m_esp32UpdatePartition->address); - if (total_size > s_esp32_update_partition->size) { + if (total_size > BleOta::instance().m_esp32UpdatePartition->size) { DEBUG_ERR(TAG, "ESP32 固件太大: %u > 分区大小: %u", - total_size, s_esp32_update_partition->size); - s_esp32_update_partition = NULL; + total_size, BleOta::instance().m_esp32UpdatePartition->size); + BleOta::instance().m_esp32UpdatePartition = NULL; return ESP_FAIL; } - esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle); + esp_err_t err = esp_ota_begin(BleOta::instance().m_esp32UpdatePartition, total_size, &BleOta::instance().m_esp32OtaHandle); if (err != ESP_OK) { DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err)); - s_esp32_update_partition = NULL; - s_esp32_ota_handle = 0; + BleOta::instance().m_esp32UpdatePartition = NULL; + BleOta::instance().m_esp32OtaHandle = 0; return err; } - s_esp32_received = 0; - s_esp32_ota_active = true; - s_esp32_reboot_pending = false; - s_esp32_last_cmd = 0; - s_esp32_last_seq = 0xFFFFU; - DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label); + BleOta::instance().m_esp32Received = 0; + BleOta::instance().m_esp32OtaActive = true; + BleOta::instance().m_esp32RebootPending = false; + BleOta::instance().m_esp32LastCmd = 0; + BleOta::instance().m_esp32LastSeq = 0xFFFFU; + DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", BleOta::instance().m_esp32UpdatePartition->label); return ESP_OK; } @@ -255,12 +254,12 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size) */ static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len) { - if (!s_esp32_ota_active || s_esp32_update_partition == NULL) { + if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) { return ESP_ERR_INVALID_STATE; } - esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len); + esp_err_t err = esp_ota_write(BleOta::instance().m_esp32OtaHandle, data, len); if (err == ESP_OK) { - s_esp32_received += len; + BleOta::instance().m_esp32Received += len; } return err; } @@ -271,7 +270,7 @@ static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len) * @param seq 当前确认的分块序号。 * @param completed_bytes 已完成写入的累计字节数。 */ -static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes) +static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes) { uint8_t ack[8] = {0}; ack[0] = BLE_OTA_NOTIFY_ACK; @@ -283,7 +282,7 @@ static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t complete ack[6] = (uint8_t)((completed_bytes >> 16) & 0xFFU); ack[7] = (uint8_t)((completed_bytes >> 24) & 0xFFU); - esp_err_t err = ble_ota_notify_response(ack, sizeof(ack)); + esp_err_t err = BleOta::instance().notifyResponse(ack, sizeof(ack)); if (err != ESP_OK) { DEBUG_WARN(TAG, "发送 ACK 失败: cmd=0x%02X seq=%u err=%s", cmd, seq, esp_err_to_name(err)); @@ -298,7 +297,7 @@ static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t complete * @param stage 阶段标识码。 * @param value 阶段附带值(错误码或耗时等)。 */ -static void ble_ota_send_finish_event(uint8_t stage, uint32_t value) +static void sendFinishEvent(uint8_t stage, uint32_t value) { uint8_t pkt[6] = {0}; pkt[0] = BLE_OTA_NOTIFY_FINISH; @@ -307,7 +306,7 @@ static void ble_ota_send_finish_event(uint8_t stage, uint32_t value) pkt[3] = (uint8_t)((value >> 8) & 0xFFU); pkt[4] = (uint8_t)((value >> 16) & 0xFFU); pkt[5] = (uint8_t)((value >> 24) & 0xFFU); - esp_err_t err = ble_ota_notify_response(pkt, sizeof(pkt)); + esp_err_t err = BleOta::instance().notifyResponse(pkt, sizeof(pkt)); if (err != ESP_OK) { DEBUG_WARN(TAG, "发送 FINISH 事件失败: stage=0x%02X err=%s", stage, esp_err_to_name(err)); @@ -354,28 +353,28 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) { // 检查固件是否存储完毕 → 触发 GD32 OTA 中继 ota_meta_t meta; - esp_err_t err = meta_load(&meta); + esp_err_t err = OtaRelay::metaLoad(&meta); if (err == ESP_OK && meta.total_size > 0 && meta.stored_size >= meta.total_size) { DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继", meta.stored_size, meta.total_size); - ble_ota_signal_relay(); + BleOta::instance().signalRelay(); } else { DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")", meta.stored_size, meta.total_size); } } else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) { - if (!s_esp32_ota_active || s_esp32_update_partition == NULL) { + if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) { DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级"); return 0; } - if (s_esp32_reboot_pending) { + if (BleOta::instance().m_esp32RebootPending) { DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL"); return 0; } - s_esp32_reboot_pending = true; + BleOta::instance().m_esp32RebootPending = true; BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task, "ble_ota_finish", 4096, @@ -384,7 +383,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, NULL); if (ok != pdPASS) { DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败"); - s_esp32_reboot_pending = false; + BleOta::instance().m_esp32RebootPending = false; } else { DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(由设备端异步完成)"); } @@ -485,21 +484,21 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, } DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u", - seq, (unsigned int)s_esp32_received); + seq, (unsigned int)BleOta::instance().m_esp32Received); if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || - (total_size > 0 && s_esp32_received >= total_size)) { - ble_ota_send_window_ack(cmd, seq, (uint32_t)s_esp32_received); + (total_size > 0 && BleOta::instance().m_esp32Received >= total_size)) { + sendWindowAck(cmd, seq, (uint32_t)BleOta::instance().m_esp32Received); } - if (total_size > 0 && s_esp32_received >= total_size) { + if (total_size > 0 && BleOta::instance().m_esp32Received >= total_size) { DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)", - (unsigned int)s_esp32_received, total_size, seq + 1); - s_esp32_last_cmd = cmd; - s_esp32_last_seq = seq; + (unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1); + BleOta::instance().m_esp32LastCmd = cmd; + BleOta::instance().m_esp32LastSeq = seq; - if (!s_esp32_reboot_pending) { - s_esp32_reboot_pending = true; + if (!BleOta::instance().m_esp32RebootPending) { + BleOta::instance().m_esp32RebootPending = true; BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task, "ble_ota_finish", 4096, @@ -508,7 +507,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, NULL); if (ok != pdPASS) { DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败"); - s_esp32_reboot_pending = false; + BleOta::instance().m_esp32RebootPending = false; return BLE_ATT_ERR_UNLIKELY; } DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启"); @@ -534,7 +533,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, meta.last_seq = 0; meta.chunk_count = (total_size + (uint32_t)data_len - 1U) / (uint32_t)data_len; - esp_err_t err = meta_save(&meta); + esp_err_t err = OtaRelay::metaSave(&meta); if (err != ESP_OK) { DEBUG_ERR(TAG, "元数据保存失败"); return BLE_ATT_ERR_UNLIKELY; @@ -545,7 +544,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, } ota_meta_t meta = {}; - if (meta_load(&meta) != ESP_OK || meta.total_size == 0) { + if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) { DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq); return BLE_ATT_ERR_UNLIKELY; } @@ -565,7 +564,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, return BLE_ATT_ERR_UNLIKELY; } - esp_err_t err = flash_write_chunk(flash_offset, &buf[data_offset], data_len); + esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, &buf[data_offset], data_len); if (err != ESP_OK) { DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s", seq, flash_offset, esp_err_to_name(err)); @@ -573,21 +572,21 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, } uint32_t stored_size = flash_offset + (uint32_t)data_len; - err = meta_update(stored_size, seq); + err = OtaRelay::metaUpdate(stored_size, seq); if (err != ESP_OK) { DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq); } if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || (meta.total_size > 0 && stored_size >= meta.total_size)) { - ble_ota_send_window_ack(cmd, seq, stored_size); + sendWindowAck(cmd, seq, stored_size); } DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32, seq, flash_offset, stored_size); // --- 检查 GD32 固件是否接收完毕 --- - if (meta_load(&meta) == ESP_OK && + if (OtaRelay::metaLoad(&meta) == ESP_OK && meta.total_size > 0 && meta.stored_size >= meta.total_size) { DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)", @@ -627,7 +626,7 @@ static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle, /* * DATA_OUT (0x0003) - Notify only - * 访问回调不做读写处理,通知由 ble_ota_notify_response() 发送 + * 访问回调不做读写处理,通知由 BleOta::instance().notifyResponse() 发送 */ /** * @brief GATT DATA_OUT 特征访问回调(Notify-only,占位实现)。 @@ -727,10 +726,9 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) case BLE_GAP_EVENT_CONNECT: if (event->connect.status == 0) { // 连接成功 - s_ble_connected = true; - /** @brief 当前 BLE 连接句柄。 */ - s_conn_handle = event->connect.conn_handle; - DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", s_conn_handle); + BleOta::instance().m_bleConnected = true; + BleOta::instance().m_connHandle = event->connect.conn_handle; + DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", BleOta::instance().m_connHandle); // 向对端请求更快的连接参数,提升 OTA 吞吐 struct ble_gap_upd_params up = {}; @@ -740,7 +738,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) up.supervision_timeout = 2000; // 2 秒监控超时 up.min_ce_len = 0; // 控制器自行决定 up.max_ce_len = 0; - int rc = ble_gap_update_params(s_conn_handle, &up); + int rc = ble_gap_update_params(BleOta::instance().m_connHandle, &up); if (rc != 0) { DEBUG_WARN(TAG, "请求更新连接参数失败: rc=%d(对端不一定接受,继续运行)", rc); } else { @@ -750,7 +748,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) } else { DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status); /** @brief BLE GATT 客户端连接状态。 */ - s_ble_connected = false; + BleOta::instance().m_bleConnected = false; } return 0; @@ -758,9 +756,8 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d", event->disconnect.reason); /** @brief BLE GATT 客户端连接状态。 */ - s_ble_connected = false; - /** @brief 当前 BLE 连接句柄。 */ - s_conn_handle = BLE_HS_CONN_HANDLE_NONE; + BleOta::instance().m_bleConnected = false; + BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE; // 恢复广播以接受新连接 ble_ota_start_advertising(); return 0; @@ -794,7 +791,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) /** * @brief 启动 BLE 广播,使设备可被发现和连接。 */ -static void ble_ota_start_advertising(void) +static void ble_ota_start_advertising(void) // kept as free function for BLE callback compatibility { int rc; @@ -883,10 +880,10 @@ static void ble_ota_on_sync(void) { DEBUG_INFO(TAG, "NimBLE 协议栈同步完成"); - // GATT 服务已在 ble_ota_server_start() 中注册,不再重复注册 + // GATT 服务已在 serverStart() 中注册,不再重复注册 // 重复调用 ble_gatts_count_cfg / ble_gatts_add_svcs 会导致 BLE_HS_EBUSY - s_ble_ready = true; + BleOta::instance().m_bleReady = true; ble_ota_start_advertising(); } @@ -898,7 +895,7 @@ static void ble_ota_on_reset(int reason) { DEBUG_WARN(TAG, "NimBLE 协议栈重置: reason=%d", reason); /** @brief NimBLE 协议栈同步完成标志。 */ - s_ble_ready = false; + BleOta::instance().m_bleReady = false; } // ============================================================ @@ -924,7 +921,7 @@ static void ble_ota_nimble_host_task(void *param) * @brief 启动 BLE GATT 服务器并注册 OTA 服务。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t ble_ota_server_start(void) +esp_err_t BleOta::serverStart() { int rc; @@ -976,6 +973,8 @@ esp_err_t ble_ota_server_start(void) nimble_port_freertos_init(ble_ota_nimble_host_task); + // Sync val_handle to class member + BleOta::instance().mDataOutValHandle = s_data_out_val_handle; DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成"); return ESP_OK; } @@ -986,12 +985,12 @@ esp_err_t ble_ota_server_start(void) * @param len 数据长度(字节)。 * @return ESP_OK 成功,其他失败。 */ -esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len) +esp_err_t BleOta::notifyResponse(const uint8_t *data, size_t len) { - if (!s_ble_connected) { + if (!BleOta::instance().m_bleConnected) { return ESP_ERR_NOT_SUPPORTED; } - if (s_data_out_val_handle == 0) { + if (BleOta::instance().mDataOutValHandle == 0) { DEBUG_WARN(TAG, "DATA_OUT 值句柄未设置"); return ESP_ERR_INVALID_STATE; } @@ -1004,7 +1003,7 @@ esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len) return ESP_ERR_NO_MEM; } - int rc = ble_gattc_notify_custom(s_conn_handle, s_data_out_val_handle, om); + int rc = ble_gattc_notify_custom(BleOta::instance().m_connHandle, BleOta::instance().mDataOutValHandle, om); if (rc != 0) { DEBUG_WARN(TAG, "通知发送失败: rc=%d", rc); return ESP_FAIL; @@ -1018,24 +1017,24 @@ esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len) * @brief 检查当前是否有 BLE 客户端连接。 * @return true 已连接,false 未连接。 */ -bool ble_ota_is_connected(void) +bool BleOta::isConnected() { /** @brief BLE GATT 客户端连接状态。 */ - return s_ble_connected; + return BleOta::instance().m_bleConnected; } /** * @brief 通知中继模块开始向 GD32 转发固件。 */ -void ble_ota_signal_relay(void) +void BleOta::signalRelay() { - if (g_ota_dispatch_queue == NULL) { + if (OtaRelay::dispatchQueue == NULL) { DEBUG_ERR(TAG, "OTA 调度队列未初始化"); return; } uint32_t msg = 1; - BaseType_t ret = xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(200)); + BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &msg, pdMS_TO_TICKS(200)); if (ret != pdTRUE) { DEBUG_ERR(TAG, "OTA 调度信号发送失败: 队列满"); return; @@ -1049,18 +1048,18 @@ void ble_ota_signal_relay(void) // // 注意: 此任务的职责是初始化 BLE 服务器并保持存活。 // 实际的 BLE 事件循环在独立的 NimBLE Host 任务中运行。 -// 为避免与 ota_relay_task 竞争,此任务不从 g_ota_dispatch_queue 消费。 +// 为避免与 ota_relay_task 竞争,此任务不从 OtaRelay::dispatchQueue 消费。 // ============================================================ /** * @brief BLE OTA FreeRTOS 任务,初始化 NimBLE 并运行 GATT 服务。 * @param pvParameters FreeRTOS 任务参数,未使用。 */ -void ble_ota_task(void *pvParameters) +void BleOta::taskEntry(void *pvParameters) { (void)pvParameters; DEBUG_INFO(TAG, "=== BLE OTA 任务启动 ==="); - esp_err_t err = ble_ota_server_start(); + esp_err_t err = BleOta::instance().serverStart(); if (err != ESP_OK) { DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用", esp_err_to_name(err)); @@ -1075,8 +1074,8 @@ void ble_ota_task(void *pvParameters) if (heartbeat % 6 == 0) { DEBUG_DBG(TAG, "心跳 #%lu | BLE %s | 连接: %s", (unsigned long)(heartbeat / 6), - s_ble_ready ? "就绪" : "未就绪", - s_ble_connected ? "已连接" : "未连接"); + BleOta::instance().m_bleReady ? "就绪" : "未就绪", + BleOta::instance().m_bleConnected ? "已连接" : "未连接"); } vTaskDelay(pdMS_TO_TICKS(10000)); diff --git a/main/server/ble_ota.h b/main/server/ble_ota.h index 74ad3d6..9f0dfd7 100644 --- a/main/server/ble_ota.h +++ b/main/server/ble_ota.h @@ -1,41 +1,37 @@ -/* +/* * ble_ota.h - BLE OTA GATT 服务器 * T5.1 - T5.5 */ #ifndef BLE_OTA_H #define BLE_OTA_H -#include "ota_relay.h" +#include +#include "esp_err.h" +#include "esp_ota_ops.h" -/** - * @brief BLE OTA 任务入口,负责初始化 NimBLE 并运行 GATT OTA 服务。 - * @param pvParameters FreeRTOS 任务参数,未使用。 - */ -void ble_ota_task(void *pvParameters); +class BleOta { +public: + static BleOta& instance(); -/** - * @brief 启动 BLE GATT 服务器并注册 OTA 相关服务与特征。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t ble_ota_server_start(void); + static void taskEntry(void *pvParameters); + esp_err_t serverStart(); + esp_err_t notifyResponse(const uint8_t *data, size_t len); + bool isConnected(); + void signalRelay(); -/** - * @brief 通过 BLE Notify 将 GD32 的响应数据发送给上位机 APP。 - * @param data 待推送的数据指针。 - * @param len 数据长度(字节)。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len); + // 以下状态为 public,供 C 回调函数访问 + bool m_bleReady = false; + bool m_bleConnected = false; + uint16_t m_connHandle = 0xFFFF; + uint16_t mDataOutValHandle = 0; -/** - * @brief 检查当前是否存在已连接的 BLE 客户端。 - * @return true 已连接,false 未连接。 - */ -bool ble_ota_is_connected(void); - -/** - * @brief 在固件存储完成后通知中继模块开始向 GD32 转发。 - */ -void ble_ota_signal_relay(void); + bool m_esp32OtaActive = false; + esp_ota_handle_t m_esp32OtaHandle = 0; + const esp_partition_t *m_esp32UpdatePartition = NULL; + size_t m_esp32Received = 0; + bool m_esp32RebootPending = false; + uint8_t m_esp32LastCmd = 0; + uint16_t m_esp32LastSeq = 0xFFFFU; +}; #endif // BLE_OTA_H \ No newline at end of file diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 0b27f72..d2913ee 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -1,5 +1,6 @@ /* - * wifi_ota.c - WiFi OTA 服务器实现 + * wifi_ota.cpp - WiFi OTA 服务器实现 + * WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。 * T4.1 - T4.5 */ #include @@ -28,36 +29,13 @@ #include "spi_master.h" #include "cJSON.h" -/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */ -static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP; -/** @brief 冷启动标志,上电复位或掉电复位时为 true。 */ -static bool s_cold_boot = false; -/** @brief OTA 中继是否已触发标志。 */ -static bool s_relay_triggered = false; -/** @brief STA 模式连接状态。 */ -static bool s_sta_connected = false; -/** @brief STA 连接重试次数。 */ -static int s_sta_retry_count = 0; -/** @brief STA 重试已耗尽标志,用于加速 wifi_ota_task 中的 AP 切换。 */ -static bool s_sta_retry_exhausted = false; -/** @brief STA 连接成功信号量。 */ -static SemaphoreHandle_t s_sta_connected_sem = NULL; -/** @brief STA 获取 IP 信号量。 */ -static SemaphoreHandle_t s_sta_got_ip_sem = NULL; -// ESP32 OTA 状态 -static bool s_esp32_ota_active = false; -/** @brief ESP32 OTA 写入句柄。 */ -static esp_ota_handle_t s_esp32_ota_handle = 0; -static const esp_partition_t *s_esp32_update_partition = NULL; -/** @brief ESP32 OTA 已接收字节数。 */ -static size_t s_esp32_received = 0; - -// GD32 OTA 存储状态 -static bool s_gd32_storage_done = false; - -/** @brief HTTP 服务器句柄。 */ -static httpd_handle_t s_http_server = NULL; +// ============================================================ +WifiOta& WifiOta::instance() +{ + static WifiOta inst; + return inst; +} static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U; @@ -69,7 +47,6 @@ static uint32_t align_up_u32(uint32_t value, uint32_t alignment) /** * @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。 */ -static void wifi_set_static_ip(void); /** * @brief 为 HTTP 响应添加跨域访问控制头。 @@ -106,7 +83,7 @@ static esp_err_t http_options_handler(httpd_req_t *req) * @param event_id 具体事件 ID。 * @param event_data 事件附带数据。 */ -static void wifi_event_handler(void* arg, esp_event_base_t event_base, +void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == WIFI_EVENT) { @@ -118,39 +95,36 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, case WIFI_EVENT_STA_CONNECTED: DEBUG_INFO("WIFI", "STA 已关联到 AP"); - /** @brief STA 模式连接状态。 */ - s_sta_connected = true; - /** @brief STA 连接重试次数。 */ - s_sta_retry_count = 0; - s_sta_retry_exhausted = false; // ★ 连接成功,重置耗尽标志 - if (s_sta_connected_sem) { - xSemaphoreGive(s_sta_connected_sem); + WifiOta::instance().m_staConnected = true; + WifiOta::instance().m_staRetryCount = 0; + WifiOta::instance().m_staRetryExhausted = false; // ★ 连接成功,重置耗尽标志 + if (WifiOta::instance().m_staConnectedSem) { + xSemaphoreGive(WifiOta::instance().m_staConnectedSem); } break; case WIFI_EVENT_STA_DISCONNECTED: { wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data; - DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count); - /** @brief STA 模式连接状态。 */ - s_sta_connected = false; + DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, WifiOta::instance().m_staRetryCount); + WifiOta::instance().m_staConnected = false; - if (s_sta_retry_count < WIFI_STA_MAX_RETRY) { - s_sta_retry_count++; + if (WifiOta::instance().m_staRetryCount < WIFI_STA_MAX_RETRY) { + WifiOta::instance().m_staRetryCount++; // ★ 冷启动首次连接失败时增加退避延迟 // 长时间断电后路由器可能还在初始化(ARP/DHCP/信道选择) // reason 201=NO_AP_FOUND, 15=AUTH_FAIL, 2=AUTH_EXPIRE, 204=HANDSHAKE_TIMEOUT - if (s_cold_boot && s_sta_retry_count <= 3) { - int delay_ms = 2000 * s_sta_retry_count; // 2s, 4s, 6s 递增退避 + if (WifiOta::instance().m_coldBoot && WifiOta::instance().m_staRetryCount <= 3) { + int delay_ms = 2000 * WifiOta::instance().m_staRetryCount; // 2s, 4s, 6s 递增退避 DEBUG_INFO("WIFI", "冷启动退避重连第 %d 次 (reason=%d, 等 %dms)...", - s_sta_retry_count, disconn->reason, delay_ms); + WifiOta::instance().m_staRetryCount, disconn->reason, delay_ms); vTaskDelay(pdMS_TO_TICKS(delay_ms)); } else { - DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count); + DEBUG_INFO("WIFI", "重连第 %d 次...", WifiOta::instance().m_staRetryCount); } esp_wifi_connect(); } else { - DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count); - s_sta_retry_exhausted = true; + DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", WifiOta::instance().m_staRetryCount); + WifiOta::instance().m_staRetryExhausted = true; } break; } @@ -176,8 +150,8 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, case IP_EVENT_STA_GOT_IP: { ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip)); - if (s_sta_got_ip_sem) { - xSemaphoreGive(s_sta_got_ip_sem); + if (WifiOta::instance().m_staGotIpSem) { + xSemaphoreGive(WifiOta::instance().m_staGotIpSem); } break; } @@ -194,12 +168,11 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, * @brief 初始化 SoftAP 模式,供局域网 OTA 使用。 * @return ESP_OK 成功,其他失败。 */ -static esp_err_t wifi_init_ap(void) +void WifiOta::initAp() { DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ==="); - /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */ - s_wifi_mode = OTA_WIFI_MODE_AP; + WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_AP; esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); if (ap_netif == NULL) { @@ -238,8 +211,6 @@ static esp_err_t wifi_init_ap(void) DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1"); DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1"); DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT); - - return ESP_OK; } // ---------------------------------------- @@ -248,16 +219,13 @@ static esp_err_t wifi_init_ap(void) * @brief 初始化 STA 模式并连接预配置的路由器。 * @return ESP_OK 成功,其他失败。 */ -static esp_err_t wifi_init_sta(void) +esp_err_t WifiOta::initSta() { DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ==="); - /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */ - s_wifi_mode = OTA_WIFI_MODE_STA; - /** @brief STA 连接重试次数。 */ - s_sta_retry_count = 0; - /** @brief STA 重试已耗尽标志。 */ - s_sta_retry_exhausted = false; + WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_STA; + WifiOta::instance().m_staRetryCount = 0; + WifiOta::instance().m_staRetryExhausted = false; // ★ 设置国家代码:中国,允许信道 1-13 // ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13 @@ -283,13 +251,13 @@ static esp_err_t wifi_init_sta(void) ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); - wifi_set_static_ip(); + setStaticIp(); // ★ 冷启动时在 wifi_start 前增加延迟 // 长时间断电后首次上电,WiFi PHY 校准需要额外时间 // esp_wifi_start() 内部会触发 PHY 初始化和校准 // 如果校准未完成就启动射频,可能导致连接失败 - if (s_cold_boot) { + if (WifiOta::instance().m_coldBoot) { DEBUG_INFO("WIFI", "冷启动: 等待 PHY 就绪..."); vTaskDelay(pdMS_TO_TICKS(500)); } @@ -308,9 +276,9 @@ static esp_err_t wifi_init_sta(void) * @param len 缓冲区长度。 * @return ESP_OK 连接成功,其他失败。 */ -static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len) +esp_err_t WifiOta::waitStaConnected(char *ip_str, size_t ip_len) { - SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? s_sta_connected_sem : s_sta_got_ip_sem; + SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? WifiOta::instance().m_staConnectedSem : WifiOta::instance().m_staGotIpSem; const char *wait_name = WIFI_STA_USE_STATIC_IP ? "STA_CONNECTED" : "STA_GOT_IP"; if (wait_sem == NULL) { @@ -318,7 +286,7 @@ static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len) return ESP_ERR_INVALID_STATE; } - // ★ 使用轮询等待,检查 s_sta_retry_exhausted 标志加速退出 + // ★ 使用轮询等待,检查 WifiOta::instance().m_staRetryExhausted 标志加速退出 // 不再一次性等完整超时,而是每秒检查一次重试耗尽状态 TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS); TickType_t start_tick = xTaskGetTickCount(); @@ -334,30 +302,29 @@ static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len) esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) { if (ip_str) { - snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip)); + snprintf(ip_str, ip_len, IPSTR, IP2STR(&ip_info.ip)); } DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip)); } - /** @brief STA 模式连接状态。 */ - s_sta_connected = true; + WifiOta::instance().m_staConnected = true; return ESP_OK; } // ★ 重试已耗尽,不再等待 - if (s_sta_retry_exhausted) { - DEBUG_ERR("WIFI", "STA 重试已耗尽 (%d 次),立即切换 AP", s_sta_retry_count); + if (WifiOta::instance().m_staRetryExhausted) { + DEBUG_ERR("WIFI", "STA 重试已耗尽 (%d 次),立即切换 AP", WifiOta::instance().m_staRetryCount); return ESP_ERR_TIMEOUT; } } - DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count); + DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, WifiOta::instance().m_staRetryCount); return ESP_ERR_TIMEOUT; } /** * @brief 设置 STA 静态 IP */ -static void wifi_set_static_ip(void) +void WifiOta::setStaticIp() { if (WIFI_STA_USE_STATIC_IP) { esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); @@ -398,43 +365,39 @@ static esp_err_t esp32_ota_begin(size_t total_size) DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx", boot_partition->label, boot_partition->size, boot_partition->address); - s_esp32_update_partition = esp_partition_find_first( + WifiOta::instance().m_esp32UpdatePartition = esp_partition_find_first( ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL); - if (s_esp32_update_partition == NULL || - s_esp32_update_partition->address == boot_partition->address) { - s_esp32_update_partition = esp_partition_find_first( + if (WifiOta::instance().m_esp32UpdatePartition == NULL || + WifiOta::instance().m_esp32UpdatePartition->address == boot_partition->address) { + WifiOta::instance().m_esp32UpdatePartition = esp_partition_find_first( ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); } - if (s_esp32_update_partition == NULL) { + if (WifiOta::instance().m_esp32UpdatePartition == NULL) { DEBUG_ERR("OTA", "找不到可用的 OTA 分区"); return ESP_FAIL; } DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx", - s_esp32_update_partition->label, - s_esp32_update_partition->size, - s_esp32_update_partition->address); + WifiOta::instance().m_esp32UpdatePartition->label, + WifiOta::instance().m_esp32UpdatePartition->size, + WifiOta::instance().m_esp32UpdatePartition->address); - if (total_size > s_esp32_update_partition->size) { + if (total_size > WifiOta::instance().m_esp32UpdatePartition->size) { DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u", - total_size, s_esp32_update_partition->size); + total_size, WifiOta::instance().m_esp32UpdatePartition->size); return ESP_FAIL; } - esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle); + esp_err_t err = esp_ota_begin(WifiOta::instance().m_esp32UpdatePartition, total_size, &WifiOta::instance().m_esp32OtaHandle); if (err != ESP_OK) { DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err)); - /** @brief ESP32 OTA 目标分区。 */ - s_esp32_update_partition = NULL; + WifiOta::instance().m_esp32UpdatePartition = NULL; return err; } - - /** @brief ESP32 OTA 已接收字节数。 */ - s_esp32_received = 0; - /** @brief ESP32 自身 OTA 写入进行中标志。 */ - s_esp32_ota_active = true; - DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label); + WifiOta::instance().m_esp32Received = 0; + WifiOta::instance().m_esp32OtaActive = true; + DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", WifiOta::instance().m_esp32UpdatePartition->label); return ESP_OK; } @@ -446,12 +409,12 @@ static esp_err_t esp32_ota_begin(size_t total_size) */ static esp_err_t esp32_ota_write(const uint8_t *data, size_t len) { - if (!s_esp32_ota_active || s_esp32_update_partition == NULL) { + if (!WifiOta::instance().m_esp32OtaActive || WifiOta::instance().m_esp32UpdatePartition == NULL) { return ESP_ERR_INVALID_STATE; } - esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len); + esp_err_t err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, data, len); if (err == ESP_OK) { - s_esp32_received += len; + WifiOta::instance().m_esp32Received += len; } return err; } @@ -465,33 +428,29 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req) { (void)req; http_set_cors_headers(req); - if (!s_esp32_ota_active) { + if (!WifiOta::instance().m_esp32OtaActive) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started"); return ESP_FAIL; } - DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", s_esp32_received); + DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", WifiOta::instance().m_esp32Received); - esp_err_t err = esp_ota_end(s_esp32_ota_handle); + esp_err_t err = esp_ota_end(WifiOta::instance().m_esp32OtaHandle); if (err != ESP_OK) { DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err)); - /** @brief ESP32 自身 OTA 写入进行中标志。 */ - s_esp32_ota_active = false; + WifiOta::instance().m_esp32OtaActive = false; httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err)); return ESP_FAIL; } - err = esp_ota_set_boot_partition(s_esp32_update_partition); + err = esp_ota_set_boot_partition(WifiOta::instance().m_esp32UpdatePartition); if (err != ESP_OK) { DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err)); - /** @brief ESP32 自身 OTA 写入进行中标志。 */ - s_esp32_ota_active = false; + WifiOta::instance().m_esp32OtaActive = false; httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err)); return ESP_FAIL; } - - /** @brief ESP32 自身 OTA 写入进行中标志。 */ - s_esp32_ota_active = false; + WifiOta::instance().m_esp32OtaActive = false; DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中..."); const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}"; @@ -533,11 +492,11 @@ static esp_err_t esp32_ota_status_handler(httpd_req_t *req) { http_set_cors_headers(req); char resp[128]; - if (s_esp32_ota_active) { + if (WifiOta::instance().m_esp32OtaActive) { snprintf(resp, sizeof(resp), "{\"active\": true, \"received\": %u, \"partition\": \"%s\"}", - (unsigned int)s_esp32_received, - s_esp32_update_partition ? s_esp32_update_partition->label : "null"); + (unsigned int)WifiOta::instance().m_esp32Received, + WifiOta::instance().m_esp32UpdatePartition ? WifiOta::instance().m_esp32UpdatePartition->label : "null"); } else { snprintf(resp, sizeof(resp), "{\"active\": false}"); } @@ -679,7 +638,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) } free(fw_buf); - DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)s_esp32_received, json_header_len); + DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)WifiOta::instance().m_esp32Received, json_header_len); const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}"; httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); @@ -696,12 +655,12 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) return ESP_FAIL; } - if (g_relay_state != RELAY_IDLE) { - DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", g_relay_state); - ota_relay_reset(); + if (OtaRelay::relayState != RELAY_IDLE) { + DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::relayState); + OtaRelay::instance().reset(); } - s_gd32_storage_done = false; - s_relay_triggered = false; + WifiOta::instance().m_gd32StorageDone = false; + WifiOta::instance().m_relayTriggered = false; const esp_partition_t *gd32_partition = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota"); @@ -728,7 +687,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg); return ESP_FAIL; } - meta_clear(); + OtaRelay::metaClear(); uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE); if (!buf) { @@ -747,13 +706,13 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) .chunk_count = (expected_fw_size_raw + OTA_BLOCK_SIZE - 1U) / OTA_BLOCK_SIZE, .checksum = {0} }; - meta_save(&meta_raw); + OtaRelay::metaSave(&meta_raw); DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw); int first_chunk_len = pre_read_len; if (first_chunk_len > req->content_len) first_chunk_len = req->content_len; if (first_chunk_len > 0) { - esp_err_t err = flash_write_chunk(0, pre_read_buf, first_chunk_len); + esp_err_t err = OtaRelay::flashWriteChunk(0, pre_read_buf, first_chunk_len); if (err != ESP_OK) { DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s", first_chunk_len, esp_err_to_name(err)); @@ -762,7 +721,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) return ESP_FAIL; } received = first_chunk_len; - meta_update((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); + OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); } while (received < req->content_len) { @@ -779,7 +738,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) } uint32_t flash_offset = (uint32_t)received; - esp_err_t err = flash_write_chunk(flash_offset, buf, ret); + esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, buf, ret); if (err != ESP_OK) { DEBUG_ERR("WIFI", "GD32 flash write failed: offset=%u len=%d err=%s", flash_offset, ret, esp_err_to_name(err)); @@ -789,21 +748,21 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) } received += ret; - meta_update((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); + OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); } free(buf); DEBUG_INFO("WIFI", "GD32 raw upload done: %d / %u bytes", received, expected_fw_size_raw); ota_meta_t final_meta; - meta_load(&final_meta); + OtaRelay::metaLoad(&final_meta); if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) { - s_gd32_storage_done = true; + WifiOta::instance().m_gd32StorageDone = true; DEBUG_INFO("WIFI", "GD32 firmware fully stored, auto-triggering relay"); // 自动触发中继 - if (g_ota_dispatch_queue) { + if (OtaRelay::dispatchQueue) { uint32_t cmd = 1; // 1 = start relay - xQueueSend(g_ota_dispatch_queue, &cmd, 0); + xQueueSend(OtaRelay::dispatchQueue, &cmd, 0); } } @@ -826,27 +785,27 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req) ota_meta_t meta; memset(&meta, 0, sizeof(meta)); - if (meta_load(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) { + if (OtaRelay::metaLoad(&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; } - if (s_relay_triggered && g_relay_state != RELAY_IDLE && - g_relay_state != RELAY_DONE && g_relay_state != RELAY_ERROR) { + if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE && + OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_ERROR) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered"); return ESP_FAIL; } /* 上一轮中继已结束/失败/空闲,重置标志允许重新触发 */ - s_relay_triggered = false; - s_gd32_storage_done = false; + WifiOta::instance().m_relayTriggered = false; + WifiOta::instance().m_gd32StorageDone = false; - if (!g_ota_dispatch_queue) { + if (!OtaRelay::dispatchQueue) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay queue not ready"); return ESP_FAIL; } - wifi_ota_signal_relay(); - if (!s_relay_triggered) { + WifiOta::instance().signalRelay(); + if (!WifiOta::instance().m_relayTriggered) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay trigger failed"); return ESP_FAIL; } @@ -869,7 +828,7 @@ static esp_err_t ota_status_handler(httpd_req_t *req) DEBUG_INFO("WIFI", "=== HTTP: GET /ota/status ==="); ota_meta_t meta; - meta_load(&meta); + OtaRelay::metaLoad(&meta); char resp[256]; int len = snprintf(resp, sizeof(resp), @@ -877,14 +836,14 @@ static esp_err_t ota_status_handler(httpd_req_t *req) "\"storage_done\": %s, \"relay_state\": %d, \"gd32_app_alive\": %s}", (unsigned long)meta.total_size, (unsigned long)meta.stored_size, (unsigned long)meta.last_seq, - s_gd32_storage_done ? "true" : "false", - g_relay_state, - g_gd32_app_alive ? "true" : "false"); + WifiOta::instance().m_gd32StorageDone ? "true" : "false", + OtaRelay::relayState, + OtaRelay::gd32AppAlive ? "true" : "false"); DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s", (unsigned long)meta.total_size, (unsigned long)meta.stored_size, - s_gd32_storage_done ? "true" : "false", - g_gd32_app_alive ? "true" : "false"); + WifiOta::instance().m_gd32StorageDone ? "true" : "false", + OtaRelay::gd32AppAlive ? "true" : "false"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); @@ -902,7 +861,7 @@ static esp_err_t health_handler(httpd_req_t *req) http_set_cors_headers(req); DEBUG_INFO("WIFI", "=== HTTP: GET /health ==="); - health_state_t state = health_get_current(); + health_state_t state = HealthMonitor::instance().getCurrent(); uint32_t free_heap = esp_get_free_heap_size(); char resp[128]; @@ -928,7 +887,7 @@ static esp_err_t logs_handler(httpd_req_t *req) http_set_cors_headers(req); int json_len = 0; - char *json = debug_log_ring_drain_json(&json_len); + char *json = DebugLog::instance().ringDrainJson(&json_len); if (json && json_len > 0) { httpd_resp_set_type(req, "application/json"); @@ -1002,7 +961,7 @@ static esp_err_t can_send_handler(httpd_req_t *req) // 发送 CAN 帧到 GD32 uint8_t resp_data[CAN_MAX_DLC] = {0}; - int resp_len = gd32_can_send(ch, ext_id, tx_data, tx_dlc, + int resp_len = SpiMaster::instance().canSend(ch, ext_id, tx_data, tx_dlc, resp_data, sizeof(resp_data), 1000); char resp[512]; @@ -1091,7 +1050,7 @@ static esp_err_t spi_send_handler(httpd_req_t *req) uint8_t resp_type = 0; uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0}; size_t resp_len = 0; - esp_err_t spi_err = spi_ota_transfer(frame_type, payload_data, payload_len, + esp_err_t spi_err = SpiMaster::instance().otaTransfer(frame_type, payload_data, payload_len, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, timeout_ms); @@ -1129,7 +1088,7 @@ static esp_err_t can_status_handler(httpd_req_t *req) snprintf(resp, sizeof(resp), "{\"channels\": 3, \"max_dlc\": %d, \"spi_init\": %s}", CAN_MAX_DLC, - g_spi_device != NULL ? "true" : "false"); + SpiMaster::instance().deviceHandle() != NULL ? "true" : "false"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; @@ -1147,7 +1106,7 @@ static esp_err_t wifi_status_handler(httpd_req_t *req) char ip_str[16] = "0.0.0.0"; int8_t rssi = -127; - if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) { + if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().m_staConnected) { esp_netif_ip_info_t ip_info; esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) { @@ -1161,9 +1120,9 @@ static esp_err_t wifi_status_handler(httpd_req_t *req) int len = snprintf(resp, sizeof(resp), "{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}", - s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA", + WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_AP ? "AP" : "STA", ip_str, rssi, - (s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A"); + (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA) ? (WifiOta::instance().m_staConnected ? "true" : "false") : "N/A"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); @@ -1184,7 +1143,7 @@ static esp_err_t system_status_handler(httpd_req_t *req) int8_t rssi = -127; bool connected = false; - if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) { + if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().m_staConnected) { esp_netif_ip_info_t ip_info; esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) { @@ -1199,26 +1158,26 @@ static esp_err_t system_status_handler(httpd_req_t *req) ota_meta_t meta; memset(&meta, 0, sizeof(meta)); - meta_load(&meta); + OtaRelay::metaLoad(&meta); char resp[800]; int len = snprintf(resp, sizeof(resp), "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu}}", - FW_VERSION,health_get_current(), + FW_VERSION,HealthMonitor::instance().getCurrent(), (unsigned long)esp_get_free_heap_size(), (unsigned long)esp_get_minimum_free_heap_size(), - (double)health_get_last_temperature(), + (double)HealthMonitor::instance().getLastTemperature(), (unsigned long)(esp_timer_get_time() / 1000000), - s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA", + WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_AP ? "AP" : "STA", ip_str, rssi, connected ? "true" : "false", (unsigned long)meta.total_size, (unsigned long)meta.stored_size, - s_gd32_storage_done ? "true" : "false", - g_relay_state, - (unsigned long)g_relay_seq, - (unsigned long)g_relay_total_packets); + WifiOta::instance().m_gd32StorageDone ? "true" : "false", + OtaRelay::relayState, + (unsigned long)OtaRelay::relaySeq, + (unsigned long)OtaRelay::relayTotalPackets); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); @@ -1231,7 +1190,7 @@ static esp_err_t system_status_handler(httpd_req_t *req) * @brief 启动 HTTP OTA 服务器,注册各接口路由。 * @return ESP_OK 成功,其他失败。 */ -static esp_err_t http_ota_server_start(void) +esp_err_t WifiOta::httpServerStart() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = HTTP_SERVER_PORT; @@ -1243,7 +1202,7 @@ static esp_err_t http_ota_server_start(void) DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ==="); DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port); - esp_err_t err = httpd_start(&s_http_server, &config); + esp_err_t err = httpd_start(&WifiOta::instance().m_httpServer, &config); if (err != ESP_OK) { DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err)); return err; @@ -1252,9 +1211,9 @@ static esp_err_t http_ota_server_start(void) // 从 NVS 恢复固件存储状态(重启后保留) { ota_meta_t boot_meta; - if (meta_load(&boot_meta) == ESP_OK && + if (OtaRelay::metaLoad(&boot_meta) == ESP_OK && boot_meta.total_size > 0 && boot_meta.stored_size >= boot_meta.total_size) { - s_gd32_storage_done = true; + WifiOta::instance().m_gd32StorageDone = true; DEBUG_INFO("WIFI", "从 NVS 恢复: 固件已就绪 (%u bytes)", boot_meta.total_size); } } @@ -1265,14 +1224,14 @@ static esp_err_t http_ota_server_start(void) .handler = ota_upload_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &upload_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &upload_uri); httpd_uri_t upload_options_uri = { .uri = "/ota/upload", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &upload_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &upload_options_uri); httpd_uri_t relay_uri = { .uri = "/ota/relay", @@ -1280,14 +1239,14 @@ static esp_err_t http_ota_server_start(void) .handler = ota_relay_trigger_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &relay_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &relay_uri); httpd_uri_t relay_options_uri = { .uri = "/ota/relay", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &relay_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &relay_options_uri); httpd_uri_t status_uri = { .uri = "/ota/status", @@ -1295,14 +1254,14 @@ static esp_err_t http_ota_server_start(void) .handler = ota_status_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &status_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_uri); httpd_uri_t status_options_uri = { .uri = "/ota/status", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &status_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_options_uri); httpd_uri_t esp32_update_uri = { .uri = "/ota/update", @@ -1310,14 +1269,14 @@ static esp_err_t http_ota_server_start(void) .handler = esp32_ota_update_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &esp32_update_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_update_uri); httpd_uri_t esp32_update_options_uri = { .uri = "/ota/update", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &esp32_update_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_update_options_uri); httpd_uri_t restart_uri = { .uri = "/restart", @@ -1325,14 +1284,14 @@ static esp_err_t http_ota_server_start(void) .handler = restart_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &restart_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &restart_uri); httpd_uri_t restart_options_uri = { .uri = "/restart", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &restart_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &restart_options_uri); httpd_uri_t esp32_status_uri = { .uri = "/ota/esp32_status", @@ -1340,14 +1299,14 @@ static esp_err_t http_ota_server_start(void) .handler = esp32_ota_status_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &esp32_status_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_status_uri); httpd_uri_t esp32_status_options_uri = { .uri = "/ota/esp32_status", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &esp32_status_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_status_options_uri); httpd_uri_t health_uri = { .uri = "/health", @@ -1355,14 +1314,14 @@ static esp_err_t http_ota_server_start(void) .handler = health_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &health_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &health_uri); httpd_uri_t health_options_uri = { .uri = "/health", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &health_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &health_options_uri); httpd_uri_t logs_uri = { .uri = "/logs", @@ -1370,14 +1329,14 @@ static esp_err_t http_ota_server_start(void) .handler = logs_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &logs_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_uri); httpd_uri_t logs_options_uri = { .uri = "/logs", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &logs_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_options_uri); httpd_uri_t wifi_status_uri = { .uri = "/wifi/status", @@ -1385,14 +1344,14 @@ static esp_err_t http_ota_server_start(void) .handler = wifi_status_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &wifi_status_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &wifi_status_uri); httpd_uri_t wifi_status_options_uri = { .uri = "/wifi/status", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &wifi_status_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &wifi_status_options_uri); httpd_uri_t system_status_uri = { .uri = "/system/status", @@ -1400,14 +1359,14 @@ static esp_err_t http_ota_server_start(void) .handler = system_status_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &system_status_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_status_uri); httpd_uri_t system_status_options_uri = { .uri = "/system/status", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &system_status_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_status_options_uri); // CAN 帧转发 httpd_uri_t can_send_uri = { @@ -1416,14 +1375,14 @@ static esp_err_t http_ota_server_start(void) .handler = can_send_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &can_send_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_send_uri); httpd_uri_t can_send_options_uri = { .uri = "/can/send", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &can_send_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_send_options_uri); httpd_uri_t can_status_uri = { .uri = "/can/status", @@ -1431,14 +1390,14 @@ static esp_err_t http_ota_server_start(void) .handler = can_status_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &can_status_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_status_uri); httpd_uri_t can_status_options_uri = { .uri = "/can/status", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &can_status_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_status_options_uri); DEBUG_INFO("WIFI", "HTTP 服务器已启动"); // SPI 直接发送 @@ -1448,14 +1407,14 @@ static esp_err_t http_ota_server_start(void) .handler = spi_send_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &spi_send_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_uri); httpd_uri_t spi_send_options_uri = { .uri = "/spi/send", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(s_http_server, &spi_send_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri); DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status, /spi/send"); @@ -1467,40 +1426,38 @@ static esp_err_t http_ota_server_start(void) /** * @brief 通知 OTA 中继开始向 GD32 转发固件。 */ -void wifi_ota_signal_relay(void) +void WifiOta::signalRelay() { ota_meta_t meta; memset(&meta, 0, sizeof(meta)); - if (meta_load(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) { + if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) { DEBUG_WARN("WIFI", "GD32 固件未完整存储,拒绝触发转发: stored=%lu total=%lu", (unsigned long)meta.stored_size, (unsigned long)meta.total_size); return; } - if (s_relay_triggered && g_relay_state != RELAY_IDLE && - g_relay_state != RELAY_DONE && g_relay_state != RELAY_ERROR) { - DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", g_relay_state); + if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE && + OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_ERROR) { + DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", OtaRelay::relayState); return; } /* 上一轮中继已结束/失败/空闲,重置标志 */ - s_relay_triggered = false; - s_gd32_storage_done = false; + WifiOta::instance().m_relayTriggered = false; + WifiOta::instance().m_gd32StorageDone = false; + WifiOta::instance().m_relayTriggered = true; - /** @brief OTA 中继是否已触发标志。 */ - s_relay_triggered = true; - - if (g_ota_dispatch_queue) { + if (OtaRelay::dispatchQueue) { uint32_t msg = 1; - BaseType_t ret = xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100)); + BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &msg, pdMS_TO_TICKS(100)); if (ret == pdTRUE) { DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务"); } else { DEBUG_ERR("WIFI", "OTA 调度信号发送失败"); - s_relay_triggered = false; + WifiOta::instance().m_relayTriggered = false; } } else { DEBUG_ERR("WIFI", "OTA 中继队列尚未就绪"); - s_relay_triggered = false; + WifiOta::instance().m_relayTriggered = false; } } @@ -1508,19 +1465,18 @@ void wifi_ota_signal_relay(void) * @brief 获取当前 WiFi 工作模式(AP 或 STA)。 * @return 当前 WiFi 模式枚举值。 */ -ota_wifi_mode_t wifi_get_mode(void) +ota_wifi_mode_t WifiOta::getMode() { - /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */ - return s_wifi_mode; + return WifiOta::instance().m_wifiMode; } /** * @brief 获取当前 WiFi 信号强度。 * @return 信号强度(dBm),未连接时返回无效值。 */ -int8_t wifi_get_rssi(void) +int8_t WifiOta::getRssi() { - if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) { + if (WifiOta::instance().m_wifiMode != OTA_WIFI_MODE_STA || !WifiOta::instance().m_staConnected) { return -127; } wifi_ap_record_t ap_info; @@ -1534,10 +1490,9 @@ int8_t wifi_get_rssi(void) * @brief 判断 STA 是否已连接到上级 WiFi。 * @return true 已连接,false 未连接。 */ -bool wifi_is_connected(void) +bool WifiOta::isConnected() { - /** @brief STA 模式连接状态。 */ - return s_sta_connected; + return WifiOta::instance().m_staConnected; } // ---------------------------------------- @@ -1548,14 +1503,14 @@ bool wifi_is_connected(void) * @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。 * @return true STA 初始化中,false 需回退 AP。 */ -bool wifi_early_init(void) +bool WifiOta::earlyInit() { DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ==="); // ★ 冷启动检测:判断是否为上电复位 esp_reset_reason_t reset_reason = esp_reset_reason(); - bool is_cold_boot = (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_BROWNOUT); - s_cold_boot = is_cold_boot; // 保存到模块级标志,供事件回调使用 + WifiOta::instance().m_coldBoot = (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_BROWNOUT); + DEBUG_INFO("WIFI", "复位原因: %d (%s), 冷启动: %s", reset_reason, (reset_reason == ESP_RST_POWERON) ? "POWER_ON" : @@ -1566,7 +1521,7 @@ bool wifi_early_init(void) (reset_reason == ESP_RST_TASK_WDT) ? "TASK_WDT" : (reset_reason == ESP_RST_DEEPSLEEP) ? "DEEP_SLEEP" : "OTHER", - is_cold_boot ? "是" : "否"); + WifiOta::instance().m_coldBoot ? "是" : "否"); esp_err_t err = nvs_flash_init(); bool nvs_erased = false; @@ -1588,7 +1543,7 @@ bool wifi_early_init(void) // ★ 冷启动时强制清除 WiFi NVS 缓存,避免残留脏数据 // 长时间断电后 NVS 中的 WiFi 配置缓存可能不一致 - if (is_cold_boot) { + if (WifiOta::instance().m_coldBoot) { nvs_handle_t nvs_handle; err = nvs_open("nvs.net80211", NVS_READWRITE, &nvs_handle); if (err == ESP_OK) { @@ -1610,17 +1565,15 @@ bool wifi_early_init(void) } } - /** @brief STA 连接成功信号量。 */ - s_sta_connected_sem = xSemaphoreCreateBinary(); - /** @brief STA 获取 IP 信号量。 */ - s_sta_got_ip_sem = xSemaphoreCreateBinary(); - if (!s_sta_connected_sem || !s_sta_got_ip_sem) { + WifiOta::instance().m_staConnectedSem = xSemaphoreCreateBinary(); + WifiOta::instance().m_staGotIpSem = xSemaphoreCreateBinary(); + if (!WifiOta::instance().m_staConnectedSem || !WifiOta::instance().m_staGotIpSem) { DEBUG_ERR("WIFI", "创建信号量失败"); } // 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前) - ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL)); - ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL)); DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册"); esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); @@ -1642,39 +1595,39 @@ bool wifi_early_init(void) // STA 优先,失败时返回 false 让 task 切 AP if (CONFIG_WIFI_MODE == 1) { DEBUG_INFO("WIFI", "工作模式: STA (优先)"); - err = wifi_init_sta(); + err = initSta(); if (err == ESP_OK) { // 连接已触发(STA_START 事件驱动),返回 true - // 实际连接状态由 wifi_ota_task 等待确认 + // 实际连接状态由 WifiOta::taskEntry 等待确认 return true; } DEBUG_WARN("WIFI", "STA 初始化失败,将切换 AP"); return false; } else { DEBUG_INFO("WIFI", "工作模式: AP"); - wifi_init_ap(); + initAp(); return false; // AP 模式,不需要等待连接 } } // ---------------------------------------- // WiFi OTA FreeRTOS 任务 -// WiFi 初始化已在 app_main 中通过 wifi_early_init() 完成 +// WiFi 初始化已在 app_main 中通过 WifiOta::instance().earlyInit() 完成 // 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控 /** * @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。 * @param pvParameters FreeRTOS 任务参数,未使用。 */ -void wifi_ota_task(void *pvParameters) +void WifiOta::taskEntry(void *pvParameters) { (void)pvParameters; DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ==="); // 等待 STA 连接结果(wifi_early_init 已触发连接) - if (wifi_get_mode() == OTA_WIFI_MODE_STA) { + if (WifiOta::getMode() == OTA_WIFI_MODE_STA) { char ip_str[16] = {0}; - esp_err_t err = wifi_wait_sta_connected(ip_str, sizeof(ip_str)); + esp_err_t err = instance().waitStaConnected(ip_str, sizeof(ip_str)); if (err == ESP_OK) { DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str); } else { @@ -1697,19 +1650,18 @@ void wifi_ota_task(void *pvParameters) DEBUG_ERR("WIFI", "WiFi 驱动重新初始化失败: %s", esp_err_to_name(deinit_err)); } else { // ★ deinit 后事件回调会丢失,必须重新注册 - esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL); - esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL); + esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL); + esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL); DEBUG_INFO("WIFI", "WiFi/IP 事件回调已重新注册"); } } - /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */ - s_wifi_mode = OTA_WIFI_MODE_AP; - wifi_init_ap(); + WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_AP; + instance().initAp(); } } // AP 模式:wifi_early_init 已完成,无需额外操作 - esp_err_t err = http_ota_server_start(); + esp_err_t err = instance().httpServerStart(); if (err != ESP_OK) { DEBUG_ERR("WIFI", "HTTP 服务器启动失败"); } @@ -1726,7 +1678,7 @@ void wifi_ota_task(void *pvParameters) DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION); DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision); DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size()); - DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", wifi_get_mode() == OTA_WIFI_MODE_STA ? "STA" : "AP"); + DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", WifiOta::getMode() == OTA_WIFI_MODE_STA ? "STA" : "AP"); DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成"); DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT); DEBUG_INFO("WIFI", "========================================"); @@ -1742,9 +1694,9 @@ void wifi_ota_task(void *pvParameters) // 每 10 秒检查一次 RSSI if (tick % 10 == 0) { - int8_t rssi = wifi_get_rssi(); + int8_t rssi = WifiOta::getRssi(); if (rssi != -127) { - health_update_from_wifi_rssi(rssi); + HealthMonitor::instance().updateFromWifiRssi(rssi); if (rssi < WIFI_RSSI_CRITICAL) { DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL); diff --git a/main/server/wifi_ota.h b/main/server/wifi_ota.h index 2bf58f5..3b80b9d 100644 --- a/main/server/wifi_ota.h +++ b/main/server/wifi_ota.h @@ -1,52 +1,58 @@ -/* +/* * wifi_ota.h - WiFi OTA 服务器 * T4.1 - T4.5 + * + * WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。 */ #ifndef WIFI_OTA_H #define WIFI_OTA_H #include "ota_relay.h" +#include "esp_ota_ops.h" +#include "esp_http_server.h" +#include "esp_event.h" -// WiFi 模式: AP 或 STA typedef enum { - OTA_WIFI_MODE_AP, // AP 模式(ESP32 作为热点) - OTA_WIFI_MODE_STA // STA 模式(连接路由器) + OTA_WIFI_MODE_AP, + OTA_WIFI_MODE_STA } ota_wifi_mode_t; -/** - * @brief WiFi OTA 任务入口,负责 HTTP 服务、上传处理和 OTA 触发流程。 - * @param pvParameters FreeRTOS 任务参数,未使用。 - */ -void wifi_ota_task(void *pvParameters); +class WifiOta { +public: + static WifiOta& instance(); -// 在 app_main 早期执行 WiFi 初始化并尝试以 STA 模式联网。 -/** - * @brief 返回 true 表示 STA 初始化已开始,false 表示后续应回退到 AP 模式。 - * @return true STA 初始化中,false 需回退 AP。 - */ -bool wifi_early_init(void); + bool earlyInit(); + static void taskEntry(void *pvParameters); + void signalRelay(); -/** - * @brief 在固件存储完成后通知 OTA 中继开始向 GD32 转发。 - */ -void wifi_ota_signal_relay(void); + static ota_wifi_mode_t getMode(); + static int8_t getRssi(); + static bool isConnected(); -/** - * @brief 获取当前 WiFi 工作模式(AP 或 STA)。 - * @return 当前 WiFi 模式枚举值。 - */ -ota_wifi_mode_t wifi_get_mode(void); + // 以下状态为 public,供 C 回调函数访问 + ota_wifi_mode_t m_wifiMode = OTA_WIFI_MODE_AP; + bool m_coldBoot = false; + bool m_relayTriggered = false; + bool m_staConnected = false; + int m_staRetryCount = 0; + bool m_staRetryExhausted = false; + SemaphoreHandle_t m_staConnectedSem = NULL; + SemaphoreHandle_t m_staGotIpSem = NULL; -/** - * @brief 获取当前 WiFi RSSI,未连接时通常返回无效/默认值。 - * @return 信号强度(dBm),未连接时返回无效值。 - */ -int8_t wifi_get_rssi(void); + bool m_esp32OtaActive = false; + esp_ota_handle_t m_esp32OtaHandle = 0; + const esp_partition_t *m_esp32UpdatePartition = NULL; + size_t m_esp32Received = 0; + bool m_gd32StorageDone = false; + httpd_handle_t m_httpServer = NULL; -/** - * @brief 判断当前 STA 模式下是否已经连接到上级 WiFi。 - * @return true 已连接,false 未连接。 - */ -bool wifi_is_connected(void); + void initAp(); + esp_err_t initSta(); + void setStaticIp(); + static void wifiEventHandler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data); + esp_err_t httpServerStart(); + esp_err_t waitStaConnected(char *ip_str, size_t ip_len); +}; -#endif // WIFI_OTA_H +#endif // WIFI_OTA_H \ No newline at end of file diff --git a/main/transport/protocol.cpp b/main/transport/protocol.cpp index e8601a1..ab48570 100644 --- a/main/transport/protocol.cpp +++ b/main/transport/protocol.cpp @@ -1,9 +1,11 @@ -/* - * protocol.c - 协议层实现 +/* + * protocol.cpp - 协议层实现 * T2.1, T2.2, T1.3 * ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG * * 加密实现使用 ESP32-C3 硬件 AES 加速器(通过 mbedtls port) + * + * 所有功能封装为 Protocol 类的静态方法。 */ #include #include @@ -13,31 +15,30 @@ #include "protocol.h" #include "monitor/debug_log.h" -/** @brief 协议层初始化标志。 */ -static bool s_protocol_initialized = false; - -/** - * @brief 初始化协议层,准备 CRC 和 AES 模块。 - */ -void protocol_init(void) +// ============================================================ +// Protocol 单例 +// ============================================================ +Protocol& Protocol::instance() { - if (s_protocol_initialized) return; + static Protocol inst; + return inst; +} + +void Protocol::init() +{ + if (m_initialized) return; DEBUG_INFO("PROT", "=== 初始化协议层 ==="); DEBUG_INFO("PROT", "协议层功能: CRC16 Modbus + 硬件 AES-128-CBC"); - /** @brief 协议层初始化标志。 */ - s_protocol_initialized = true; + m_initialized = true; DEBUG_INFO("PROT", "协议层就绪"); } /** * @brief CRC16 Modbus(LE,低字节在前) - * @param data 待校验的数据指针。 - * @param len 数据长度(字节)。 - * @return CRC16 校验值。 */ -uint16_t crc16_modbus(const uint8_t *data, size_t len) +uint16_t Protocol::crc16Modbus(const uint8_t *data, size_t len) { uint16_t crc = 0xFFFF; for (size_t i = 0; i < len; i++) { @@ -55,13 +56,8 @@ uint16_t crc16_modbus(const uint8_t *data, size_t len) /** * @brief 构建 SPI 帧: SOF + CMD + LEN(2LE) + PAYLOAD + CRC16(2LE) - * @param cmd 命令码。 - * @param payload 负载数据指针,可为 NULL。 - * @param len 负载长度(字节)。 - * @param out_frame 输出帧缓冲区。 - * @return 帧总长度,失败返回 -1。 */ -int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame) +int Protocol::buildFrame(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame) { if (len > FRAME_MAX_PAYLOAD) { DEBUG_ERR("PROT", "Payload 太大: %zu > %d", len, FRAME_MAX_PAYLOAD); @@ -79,7 +75,7 @@ int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_fr idx += len; } - uint16_t crc = crc16_modbus(out_frame, idx); + uint16_t crc = crc16Modbus(out_frame, idx); out_frame[idx++] = (uint8_t)(crc & 0xFF); out_frame[idx++] = (uint8_t)((crc >> 8) & 0xFF); @@ -90,15 +86,9 @@ int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_fr /** * @brief 解析并校验一帧 SPI 协议数据。 - * @param frame 输入帧缓冲区。 - * @param frame_len 输入帧长度。 - * @param out_cmd 输出命令字节,可为 NULL。 - * @param out_payload 输出负载缓冲区,可为 NULL。 - * @param out_len 输出负载长度指针,可为 NULL。 - * @return 0 成功,-1 失败。 */ -int frame_parse(const uint8_t *frame, size_t frame_len, - uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len) +int Protocol::parseFrame(const uint8_t *frame, size_t frame_len, + uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len) { if (frame_len < 6) { DEBUG_WARN("PROT", "帧太短: %zu < 6", frame_len); @@ -129,7 +119,7 @@ int frame_parse(const uint8_t *frame, size_t frame_len, } uint16_t received_crc = frame[idx + payload_len] | (frame[idx + payload_len + 1] << 8); - uint16_t calc_crc = crc16_modbus(frame, idx + payload_len); + uint16_t calc_crc = crc16Modbus(frame, idx + payload_len); if (received_crc != calc_crc) { DEBUG_WARN("PROT", "CRC 不匹配: 收到=0x%04X, 计算=0x%04X", received_crc, calc_crc); @@ -151,14 +141,8 @@ int frame_parse(const uint8_t *frame, size_t frame_len, /** * @brief 使用 AES-128 CBC 对明文进行加密(PKCS#7 填充)。 - * @param plaintext 明文数据指针。 - * @param len 明文长度。 - * @param key 16 字节密钥。 - * @param iv 16 字节初始向量。 - * @param ciphertext_out 输出密文缓冲区。 - * @return >=0 为密文长度,-1 表示失败。 */ -int protocol_aes_encrypt(const uint8_t *plaintext, size_t len, +int Protocol::aesEncrypt(const uint8_t *plaintext, size_t len, const uint8_t *key, const uint8_t *iv, uint8_t *ciphertext_out) { @@ -168,15 +152,18 @@ int protocol_aes_encrypt(const uint8_t *plaintext, size_t len, } // PKCS#7 填充 - size_t padded_len = ((len + 15) / 16) * 16; - uint8_t padded[256]; - memset(padded, 0, sizeof(padded)); - memcpy(padded, plaintext, len); - uint8_t pad = (uint8_t)(16 - (len % 16)); - if (pad == 0) pad = 16; - memset(&padded[len], pad, pad); + size_t padded_len = ((len / 16) + 1) * 16; + uint8_t padded[FRAME_MAX_PAYLOAD + 16]; + if (padded_len > sizeof(padded)) { + DEBUG_ERR("PROT", "填充后长度超出缓冲区: %zu", padded_len); + return -1; + } - DEBUG_DBG("PROT", "AES 加密: 明文=%zu 字节, 填充后=%zu 字节, 填充值=0x%02X", len, padded_len, pad); + memcpy(padded, plaintext, len); + uint8_t pad_val = (uint8_t)(padded_len - len); + memset(&padded[len], pad_val, pad_val); + + DEBUG_DBG("PROT", "AES 加密: 明文=%zu 字节, 填充后=%zu 字节", len, padded_len); // 硬件 AES-CBC 加密(使用 ESP32-C3 硬件 AES 加速器) esp_aes_context aes_ctx; @@ -196,25 +183,20 @@ int protocol_aes_encrypt(const uint8_t *plaintext, size_t len, esp_aes_free(&aes_ctx); - if (ret == 0) { - DEBUG_DBG("PROT", "AES 加密成功: %zu -> %zu 字节", len, padded_len); - return padded_len; - } else { + if (ret != 0) { DEBUG_ERR("PROT", "esp_aes_crypt_cbc 加密失败: %d", ret); return -1; } + + DEBUG_DBG("PROT", "AES 加密成功: %zu -> %zu 字节", len, padded_len); + + return padded_len; } /** - * @brief 使用 AES-128 CBC 解密密文并移除 PKCS#7 填充。 - * @param ciphertext 密文数据指针。 - * @param len 密文长度(必须是 16 的倍数)。 - * @param key 16 字节密钥。 - * @param iv 16 字节初始向量。 - * @param plaintext_out 输出明文缓冲区。 - * @return >=0 为明文长度,-1 表示失败。 + * @brief 使用 AES-128 CBC 对密文进行解密(去除 PKCS#7 填充)。 */ -int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len, +int Protocol::aesDecrypt(const uint8_t *ciphertext, size_t len, const uint8_t *key, const uint8_t *iv, uint8_t *plaintext_out) { @@ -268,13 +250,9 @@ int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len, /** * @brief 根据时间戳、地址和随机种子生成 16 字节 nonce。 - * @param timestamp 时间戳输入。 - * @param mem_addr 地址输入。 - * @param rand_seed 随机种子输入。 - * @param nonce_out 输出 nonce 缓冲区(16 字节)。 */ -void scramble_generate(uint32_t timestamp, uint32_t mem_addr, - uint32_t rand_seed, uint8_t *nonce_out) +void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr, + uint32_t rand_seed, uint8_t *nonce_out) { uint8_t state[16]; @@ -302,11 +280,8 @@ void scramble_generate(uint32_t timestamp, uint32_t mem_addr, /** * @brief 会话密钥初始化 - * @param sess 待初始化的会话上下文。 - * @param k_session 128 位会话密钥。 - * @return 0 成功,-1 失败。 */ -int session_key_init(session_ctx_t *sess, const uint8_t *k_session) +int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session) { if (!sess || !k_session) return -1; @@ -319,10 +294,9 @@ int session_key_init(session_ctx_t *sess, const uint8_t *k_session) } /** - * @brief 清除会话上下文中的密钥和状态。 - * @param sess 待清除的会话上下文。 + * @brief 清除会话上下文中的密钥和状态。 */ -void session_key_clear(session_ctx_t *sess) +void Protocol::sessionKeyClear(session_ctx_t *sess) { if (sess) { memset(sess->key, 0, 16); @@ -332,21 +306,16 @@ void session_key_clear(session_ctx_t *sess) } /** - * @brief 使用会话上下文加密数据。 - * @param sess 会话上下文。 - * @param in 输入明文。 - * @param len 数据长度。 - * @param out 输出密文缓冲区。 - * @return 密文长度,失败返回 -1。 + * @brief 使用会话上下文加密数据。 */ -int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out) +int Protocol::sessionEncrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out) { if (!sess || !in || !out) return -1; uint8_t iv_copy[16]; memcpy(iv_copy, sess->iv, 16); - int ret = protocol_aes_encrypt(in, len, sess->key, iv_copy, out); + int ret = aesEncrypt(in, len, sess->key, iv_copy, out); if (ret > 0) { memcpy(sess->iv, &out[ret - 16], 16); @@ -356,21 +325,16 @@ int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t } /** - * @brief 使用会话上下文解密数据。 - * @param sess 会话上下文。 - * @param in 输入密文。 - * @param len 数据长度。 - * @param out 输出明文缓冲区。 - * @return 明文长度,失败返回 -1。 + * @brief 使用会话上下文解密数据。 */ -int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out) +int Protocol::sessionDecrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out) { if (!sess || !in || !out) return -1; uint8_t iv_copy[16]; memcpy(iv_copy, sess->iv, 16); - int ret = protocol_aes_decrypt(in, len, sess->key, iv_copy, out); + int ret = aesDecrypt(in, len, sess->key, iv_copy, out); if (ret > 0) { memcpy(sess->iv, &in[len - 16], 16); @@ -381,10 +345,8 @@ int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t /** * @brief 使用 ESP-IDF 硬件 RNG 生成随机数 - * @param nonce 输出缓冲区。 - * @param len 随机数字节数。 */ -void generate_random_nonce(uint8_t *nonce, size_t len) +void Protocol::generateRandomNonce(uint8_t *nonce, size_t len) { esp_fill_random(nonce, len); -} +} \ No newline at end of file diff --git a/main/transport/protocol.h b/main/transport/protocol.h index ad9b9c0..90e6b64 100644 --- a/main/transport/protocol.h +++ b/main/transport/protocol.h @@ -1,7 +1,9 @@ -/* +/* * protocol.h - 帧格式、CRC16、AES 加密解密 * T2.2, T1.3 * ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG + * + * Protocol 类封装协议层功能。 */ #ifndef PROTOCOL_H #define PROTOCOL_H @@ -50,115 +52,42 @@ typedef struct { } session_ctx_t; /** - * @brief 初始化协议层资源,建立 CRC/AES 等基础能力的可用状态。 + * @brief 协议层类(单例) + * + * 封装 CRC16、AES-128-CBC、帧构建/解析、会话管理等功能。 + * 大部分方法为静态工具方法,单例仅持有初始化标志。 */ -void protocol_init(void); +class Protocol { +public: + static Protocol& instance(); -/** - * @brief 计算一段数据的 CRC16-Modbus 校验值。 - * @param data 待校验的数据指针。 - * @param len 数据长度(字节)。 - * @return CRC16 校验值。 - */ -uint16_t crc16_modbus(const uint8_t *data, size_t len); + void init(); -/** - * @brief 按协议格式构建一帧数据,返回生成后的总帧长度。 - * @param cmd 命令码。 - * @param payload 负载数据指针,可为 NULL。 - * @param len 负载长度(字节)。 - * @param out_frame 输出帧缓冲区。 - * @return 帧总长度,失败返回 -1。 - */ -int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame); + static uint16_t crc16Modbus(const uint8_t *data, size_t len); + static int buildFrame(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame); + static int parseFrame(const uint8_t *frame, size_t frame_len, + uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len); + static int aesEncrypt(const uint8_t *plaintext, size_t len, + const uint8_t *key, const uint8_t *iv, + uint8_t *ciphertext_out); + static int aesDecrypt(const uint8_t *ciphertext, size_t len, + const uint8_t *key, const uint8_t *iv, + uint8_t *plaintext_out); + static void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr, + uint32_t rand_seed, uint8_t *nonce_out); + static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session); + static void sessionKeyClear(session_ctx_t *sess); + static int sessionEncrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out); + static int sessionDecrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out); + static void generateRandomNonce(uint8_t *nonce, size_t len); -/** - * @brief 解析一帧协议数据,提取命令字、负载和长度,失败返回 -1。 - * @param frame 待解析的帧数据。 - * @param frame_len 帧数据总长度。 - * @param out_cmd 输出参数,解析出的命令码。 - * @param out_payload 输出缓冲区,存放解析出的负载。 - * @param out_len 输出参数,负载实际长度。 - * @return 0 成功,-1 失败。 - */ -int frame_parse(const uint8_t *frame, size_t frame_len, - uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len); +private: + Protocol() = default; + ~Protocol() = default; + Protocol(const Protocol&) = delete; + Protocol& operator=(const Protocol&) = delete; -/** - * @brief 使用 AES-128-CBC 对明文进行加密,返回密文长度,失败返回负值。 - * @param plaintext 明文数据。 - * @param len 明文长度(字节)。 - * @param key 128 位 AES 密钥。 - * @param iv 128 位 CBC 初始向量。 - * @param ciphertext_out 输出缓冲区,存放密文。 - * @return 密文长度,失败返回 -1。 - */ -int protocol_aes_encrypt(const uint8_t *plaintext, size_t len, - const uint8_t *key, const uint8_t *iv, - uint8_t *ciphertext_out); - -/** - * @brief 使用 AES-128-CBC 对密文进行解密,返回去除填充后的明文长度。 - * @param ciphertext 密文数据。 - * @param len 密文长度(字节,须为 16 的倍数)。 - * @param key 128 位 AES 密钥。 - * @param iv 128 位 CBC 初始向量。 - * @param plaintext_out 输出缓冲区,存放解密后的明文。 - * @return 明文长度,失败返回 -1。 - */ -int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len, - const uint8_t *key, const uint8_t *iv, - uint8_t *plaintext_out); - -/** - * @brief 根据时间戳、内存地址和随机种子生成 16 字节随机数。 - * @param timestamp 时间戳种子。 - * @param mem_addr 内存地址种子。 - * @param rand_seed 随机数种子。 - * @param nonce_out 输出缓冲区,存放生成的 16 字节随机数。 - */ -void scramble_generate(uint32_t timestamp, uint32_t mem_addr, - uint32_t rand_seed, uint8_t *nonce_out); - -/** - * @brief 用给定会话密钥初始化会话上下文。 - * @param sess 待初始化的会话上下文。 - * @param k_session 128 位会话密钥。 - * @return 0 成功,-1 失败。 - */ -int session_key_init(session_ctx_t *sess, const uint8_t *k_session); - -/** - * @brief 清除会话上下文中的密钥与状态信息。 - * @param sess 待清除的会话上下文。 - */ -void session_key_clear(session_ctx_t *sess); - -/** - * @brief 使用当前会话上下文对输入数据执行加密。 - * @param sess 会话上下文。 - * @param in 输入明文。 - * @param len 数据长度。 - * @param out 输出密文缓冲区。 - * @return 密文长度,失败返回 -1。 - */ -int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out); - -/** - * @brief 使用当前会话上下文对输入数据执行解密。 - * @param sess 会话上下文。 - * @param in 输入密文。 - * @param len 数据长度。 - * @param out 输出明文缓冲区。 - * @return 明文长度,失败返回 -1。 - */ -int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out); - -/** - * @brief 使用 ESP-IDF 硬件 RNG 生成指定长度的随机数。 - * @param nonce 输出缓冲区。 - * @param len 随机数字节数。 - */ -void generate_random_nonce(uint8_t *nonce, size_t len); + bool m_initialized = false; +}; #endif // PROTOCOL_H \ No newline at end of file diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index c5ae550..8380eeb 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -1,8 +1,10 @@ -/* - * spi_master.c - SPI Master 驱动实现 +/* + * spi_master.cpp - SPI Master 驱动实现 * T1.1 - T1.4 * ESP-IDF 6.0 适配版本 * CANFD 3通道发送接口 + * + * 所有功能封装为 SpiMaster 类方法。 */ #include #include @@ -15,13 +17,24 @@ #include "spi_master.h" #include "monitor/debug_log.h" -namespace { -constexpr size_t kSpiTransferMaxBytes = 256U; +static constexpr size_t kSpiTransferMaxBytes = 256U; -static esp_err_t spi_transfer_clocked(const uint8_t *tx_buf, size_t tx_len, - uint8_t *rx_buf, size_t rx_len) +// ============================================================ +// 单例 +// ============================================================ +SpiMaster& SpiMaster::instance() { - if (g_spi_device == NULL) { + static SpiMaster inst; + return inst; +} + +// ============================================================ +// 内部 SPI 传输 +// ============================================================ +esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, + uint8_t *rx_buf, size_t rx_len) +{ + if (m_spiDevice == NULL) { return ESP_ERR_INVALID_STATE; } @@ -60,16 +73,17 @@ static esp_err_t spi_transfer_clocked(const uint8_t *tx_buf, size_t tx_len, t.tx_buffer = tx_ptr; t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL; - esp_err_t ret = spi_device_transmit(g_spi_device, &t); + esp_err_t ret = spi_device_transmit(m_spiDevice, &t); + if (ret == ESP_OK && rx_len > 0U && rx_ptr != rx_buf) { memcpy(rx_buf, rx_ptr, rx_len); } return ret; } -static esp_err_t spi_transfer_request_response(uint8_t cmd, - const uint8_t *payload, size_t payload_len, - uint8_t *rx_buf, size_t rx_len) +esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd, + const uint8_t *payload, size_t payload_len, + uint8_t *rx_buf, size_t rx_len) { if ((payload_len > 0U && payload == NULL) || (rx_len > 0U && rx_buf == NULL)) { return ESP_ERR_INVALID_ARG; @@ -99,23 +113,19 @@ static esp_err_t spi_transfer_request_response(uint8_t cmd, memset(&tx_staging[tx_pos], 0xFF, total_len - tx_pos); } - esp_err_t ret = spi_transfer_clocked(tx_staging, total_len, - (rx_len > 0U) ? rx_staging : NULL, - (rx_len > 0U) ? total_len : 0U); + esp_err_t ret = transferClocked(tx_staging, total_len, + (rx_len > 0U) ? rx_staging : NULL, + (rx_len > 0U) ? total_len : 0U); if (ret == ESP_OK && rx_len > 0U) { memcpy(rx_buf, &rx_staging[cmd_len + payload_len], rx_len); } return ret; } -} // namespace -/** - * @brief 计算 SPI OTA 帧头和负载的异或 CRC8。 - * @param data 输入数据指针。 - * @param len 输入长度。 - * @return 计算得到的 CRC8 值。 - */ -static uint8_t spi_ota_crc8(const uint8_t *data, size_t len) +// ============================================================ +// OTA 帧工具 +// ============================================================ +uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len) { uint8_t crc = 0x00; for (size_t i = 0; i < len; ++i) { @@ -124,17 +134,8 @@ static uint8_t spi_ota_crc8(const uint8_t *data, size_t len) return crc; } -/** - * @brief 构建 SPI OTA 协议帧(SOF + TYPE + LEN + PAYLOAD + CRC)。 - * @param type 帧类型。 - * @param payload 负载数据指针,可为 NULL。 - * @param payload_len 负载长度。 - * @param out 输出帧缓冲区。 - * @param out_len 输出帧长度。 - * @return 构建成功返回 true,否则返回 false。 - */ -static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t payload_len, - uint8_t *out, size_t *out_len) +bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t *out_len) { if (!out || !out_len) return false; size_t total = 5 + payload_len; @@ -146,91 +147,67 @@ static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t pay if (payload && payload_len > 0) { memcpy(&out[4], payload, payload_len); } - out[4 + payload_len] = spi_ota_crc8(out, 4 + payload_len); + out[4 + payload_len] = otaCrc8(out, 4 + payload_len); *out_len = total; return true; } -/** - * @brief 解析并校验一帧 SPI OTA 数据。 - * @param frame 输入帧指针。 - * @param frame_len 输入帧缓冲区长度。 - * @param type 输出帧类型,可为 NULL。 - * @param payload 输出负载指针,可为 NULL。 - * @param payload_len 输出负载长度,可为 NULL。 - * @return 解析并校验成功返回 true,否则返回 false。 - */ -static bool spi_ota_parse_frame(const uint8_t *frame, size_t frame_len, - uint8_t *type, const uint8_t **payload, size_t *payload_len) +bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len, + uint8_t *type, const uint8_t **payload, size_t *payload_len) { - if (!frame || frame_len < 5 || frame[0] != SPI_OTA_SOF) return false; - size_t len = (size_t)frame[2] | ((size_t)frame[3] << 8); - size_t expected = 5 + len; - if (expected > frame_len) return false; - uint8_t crc = spi_ota_crc8(frame, 4 + len); - if (crc != frame[4 + len]) return false; + if (!frame || frame_len < 5) return false; + if (frame[0] != SPI_OTA_SOF) return false; + + size_t plen = (size_t)frame[2] | ((size_t)frame[3] << 8); + size_t total = 5 + plen; + if (frame_len < total) return false; + + uint8_t expected_crc = otaCrc8(frame, 4 + plen); + if (frame[4 + plen] != expected_crc) return false; + if (type) *type = frame[1]; - if (payload) *payload = &frame[4]; - if (payload_len) *payload_len = len; + if (payload) *payload = (plen > 0) ? &frame[4] : nullptr; + if (payload_len) *payload_len = plen; return true; } -/** - * @brief 在接收缓冲区中扫描并定位第一帧有效 SPI OTA 数据。 - * @param buf 输入缓冲区。 - * @param buf_len 缓冲区长度。 - * @param type 输出帧类型,可为 NULL。 - * @param payload 输出负载指针,可为 NULL。 - * @param payload_len 输出负载长度,可为 NULL。 - * @param frame_offset 输出帧起始偏移,可为 NULL。 - * @return 找到有效帧返回 true,否则返回 false。 - */ -static bool spi_ota_find_frame(const uint8_t *buf, size_t buf_len, - uint8_t *type, const uint8_t **payload, size_t *payload_len, - size_t *frame_offset) +bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, + uint8_t *type, const uint8_t **payload, size_t *payload_len, + size_t *frame_offset) { - if (!buf || buf_len < 5) return false; for (size_t i = 0; i + 5 <= buf_len; ++i) { if (buf[i] != SPI_OTA_SOF) continue; - size_t len_field = (size_t)buf[i+2] | ((size_t)buf[i+3] << 8); - size_t needed = 5 + len_field; - if (i + needed > buf_len) continue; - uint8_t crc = spi_ota_crc8(&buf[i], 4 + len_field); - if (crc != buf[i + 4 + len_field]) { - DEBUG_DBG("SPI", "帧@%u CRC错误: calc=0x%02X recv=0x%02X type=0x%02X len=%u", - (unsigned)i, crc, buf[i + 4 + len_field], buf[i+1], (unsigned)len_field); - continue; - } - if (spi_ota_parse_frame(&buf[i], buf_len - i, type, payload, payload_len)) { - if (frame_offset) *frame_offset = i; - return true; - } + + size_t plen = (size_t)buf[i + 2] | ((size_t)buf[i + 3] << 8); + size_t total = 5 + plen; + if (i + total > buf_len) continue; + + uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen); + if (buf[i + 4 + plen] != expected_crc) continue; + + if (type) *type = buf[i + 1]; + if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr; + if (payload_len) *payload_len = plen; + if (frame_offset) *frame_offset = i; + return true; } return false; } -/** @brief SPI2 从设备句柄。 */ -spi_device_handle_t g_spi_device = NULL; - -/** - * @brief SPI2 总线初始化 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_master_init(void) +// ============================================================ +// 初始化 +// ============================================================ +esp_err_t SpiMaster::init() { - DEBUG_INFO("SPI", "=== 初始化 SPI2 (VSPI) ==="); + DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ==="); spi_bus_config_t bus_cfg = {}; - bus_cfg.mosi_io_num = 5; - bus_cfg.miso_io_num = 7; + bus_cfg.miso_io_num = 2; + bus_cfg.mosi_io_num = 7; bus_cfg.sclk_io_num = 6; bus_cfg.quadwp_io_num = -1; bus_cfg.quadhd_io_num = -1; - bus_cfg.max_transfer_sz = 256; - - DEBUG_INFO("SPI", "SPI2 引脚: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6"); - DEBUG_INFO("SPI", "SPI2 配置: CLK=%d MHz, CS=GPIO%d, 最大传输=%d 字节", - SPI_CLOCK_HZ / 1000000, SPI_MASTER_CS_GPIO, bus_cfg.max_transfer_sz); + bus_cfg.max_transfer_sz = kSpiTransferMaxBytes; esp_err_t ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO); if (ret != ESP_OK) { @@ -243,408 +220,150 @@ esp_err_t spi_master_init(void) dev_cfg.mode = SPI_MODE; dev_cfg.spics_io_num = SPI_MASTER_CS_GPIO; dev_cfg.queue_size = SPI_QUEUE_SIZE; - dev_cfg.command_bits = 0; - dev_cfg.address_bits = 0; - dev_cfg.dummy_bits = 0; - /* CS↑ 后保持 CS 高的时间(SPI 时钟周期数)。 - * - * 0 = 最小 CS 高时间(仅满足 SPI 时序)。 - * 设大一点让 GD32 EXTI ISR 有足够时间完成 onCsRelease() 处理: - * 切 internal NSS HIGH + 清 RBNE FIFO + 帧解析 + 准备响应帧。 - * - * 10 × 250ns = 2.5µs,加上 SPI 驱动本身的 CS 间隮(~1µs), - * GD32 实际有 ~3.5µs 的 CS 高窗口。足够完成关键操作。 - */ - dev_cfg.cs_ena_posttrans = 10; - /* CS↓ 后等待 20 个 SPI 时钟周期再开始发 SCK。 - * - * 4MHz SPI → 20×250ns = 5µs 延时。 - * - * GD32 SPI 从机在 CS↓ 时需要 EXTI ISR 执行 onCsAssert(): - * 设置 s_csActive=true → 清空接收缓冲 → 切 internal NSS LOW → 配置 DMA。 - * - * 但 SPI0 RBNE 中断优先级 (1,0) 高于 EXTI (1,1), - * 首字节 RBNE 会打断正在执行的 EXTI ISR → - * s_csActive 还没设为 true → s_rxStreamLen 还没清零 → - * 首字节存到旧位置 → EXTI 返回后清零 → 首字节丢失。 - * - * cs_ena_pretrans=20 让 ESP32 在 CS↓ 后多等 5µs, - * GD32 EXTI ISR 只需 ~1µs 就能完成关键初始化, - * 5µs 足够让 onCsAssert() 执行完 s_csActive=true + s_rxStreamLen=0U。 - * - * 代价:每次 SPI 事务多 5µs 延时。 - * 一次 OTA 事务 = 1次请求 + ~10次轮询 = ~55µs 额外开销,可接受。 - */ - dev_cfg.cs_ena_pretrans = 20; + dev_cfg.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; - ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &g_spi_device); + ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &m_spiDevice); if (ret != ESP_OK) { - DEBUG_ERR("SPI", "添加 SPI 设备失败: %s", esp_err_to_name(ret)); + DEBUG_ERR("SPI", "SPI 设备添加失败: %s", esp_err_to_name(ret)); return ret; } - DEBUG_INFO("SPI", "SPI2 已初始化: CLK=%dMHz, CS=GPIO%d, 模式=%d", - SPI_CLOCK_HZ / 1000000, SPI_MASTER_CS_GPIO, SPI_MODE); - DEBUG_INFO("SPI", "SPI 设备句柄: %p", g_spi_device); + DEBUG_INFO("SPI", "SPI2 总线初始化完成: CS=GPIO%d, CLK=%d Hz", + SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ); + return ESP_OK; } -/** - * @brief 执行一次同步 SPI 事务,先发送请求,再补时钟读取响应尾部。 - * @param cmd SPI 命令码;为 0 时不额外插入命令字节,直接发送 payload。 - * @param payload 发送数据指针。 - * @param payload_len 发送数据长度。 - * @param recv_buf 接收缓冲区指针。 - * @param recv_len 期望接收的响应字节数。 - * @param timeout_ms 预留超时参数(当前实现未直接使用)。 - * @return ESP_OK 成功,其他为失败错误码。 - */ -esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, uint32_t timeout_ms) +// ============================================================ +// 公开 SPI 传输接口 +// ============================================================ +esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload, + size_t payload_len, uint8_t *recv_buf, + size_t recv_len, uint32_t timeout_ms) { (void)timeout_ms; - if (g_spi_device == NULL) { - DEBUG_ERR("SPI", "SPI 设备未初始化"); - return ESP_ERR_INVALID_STATE; + return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len); +} + +esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len) +{ + return transferRequestResponse(cmd, payload, len, NULL, 0); +} + +esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms) +{ + (void)timeout_ms; + return transferRequestResponse(cmd, NULL, 0, recv_buf, len); +} + +// ============================================================ +// SPI OTA 协议 +// ============================================================ +esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms) +{ + uint8_t tx_frame[SPI_OTA_FRAME_MAX]; + size_t tx_len = 0; + if (!otaBuildFrame(type, payload, payload_len, tx_frame, &tx_len)) { + return ESP_ERR_INVALID_ARG; } - DEBUG_DBG("SPI", "SPI 传输: cmd=0x%02X, 发送=%zu 字节, 接收=%zu 字节", - cmd, payload_len, recv_len); - - esp_err_t ret = spi_transfer_request_response(cmd, payload, payload_len, recv_buf, recv_len); + esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0); if (ret != ESP_OK) { - DEBUG_WARN("SPI", "SPI 发送 cmd=0x%02X 失败: %s", cmd, esp_err_to_name(ret)); - } else { - DEBUG_DBG("SPI", "SPI 传输完成: cmd=0x%02X", cmd); + DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret)); + return ret; } - return ret; + return otaPollResponse(resp_type, resp_payload, resp_max, resp_len, timeout_ms); } -// 仅发送 -/** - * @brief 调用同步传输接口发送数据,不要求接收返回内容。 - * @param cmd SPI 命令码。 - * @param payload 发送数据指针。 - * @param len 发送数据长度。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len) +esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms) { - return spi_sync_transfer(cmd, payload, len, NULL, 0, 3000); -} - -// 仅接收 -/** - * @brief 发送哑数据产生时钟,从从设备同步读取指定长度数据。 - * @param cmd SPI 命令码。 - * @param recv_buf 接收缓冲区。 - * @param len 接收长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms) -{ - return spi_sync_transfer(cmd, NULL, 0, recv_buf, len, timeout_ms); -} - -/** - * @brief 轮询读取 SPI OTA 响应帧,不重复发送请求帧。 - * @param resp_type 输出响应类型,可为 NULL。 - * @param resp_payload 输出响应负载缓冲区,可为 NULL。 - * @param resp_max 响应缓冲区最大长度。 - * @param resp_len 输出响应长度,可为 NULL。 - * @param timeout_ms 轮询超时时间(毫秒)。 - * @return ESP_OK 成功,ESP_ERR_TIMEOUT 超时,其他为底层错误码。 - */ -esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload, - size_t resp_max, size_t *resp_len, uint32_t timeout_ms) -{ - if (g_spi_device == NULL) { - return ESP_ERR_INVALID_STATE; - } - if (resp_len) *resp_len = 0; - if (resp_type) *resp_type = 0; - - uint8_t rx_raw[SPI_OTA_FRAME_MAX] = {0}; - uint8_t dummy[SPI_OTA_FRAME_MAX]; - memset(dummy, 0xFF, sizeof(dummy)); - + constexpr size_t kPollBytes = 96U; TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); - uint32_t attempt = 0; + while (xTaskGetTickCount() <= deadline) { - ++attempt; - memset(rx_raw, 0xFF, sizeof(rx_raw)); - esp_err_t ret = spi_transfer_clocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw)); + uint8_t rx_raw[kPollBytes]; + uint8_t dummy[kPollBytes]; + memset(dummy, 0xFF, sizeof(dummy)); + + esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw)); if (ret != ESP_OK) { - DEBUG_WARN("SPI", "SPI OTA 响应读取失败[%u]: %s", attempt, esp_err_to_name(ret)); - vTaskDelay(pdMS_TO_TICKS(1)); + DEBUG_WARN("SPI", "OTA 响应读取失败: %s", esp_err_to_name(ret)); + vTaskDelay(pdMS_TO_TICKS(10)); continue; } - uint8_t parsed_type = 0; - const uint8_t *parsed_payload = nullptr; - size_t parsed_len = 0; + uint8_t found_type = 0; + const uint8_t *found_payload = nullptr; + size_t found_len = 0; size_t frame_offset = 0; - if (spi_ota_find_frame(rx_raw, sizeof(rx_raw), &parsed_type, &parsed_payload, &parsed_len, &frame_offset)) { - DEBUG_DBG("SPI", "OTA响应: type=0x%02X len=%u offset=%u attempt=%u", - parsed_type, (unsigned)parsed_len, (unsigned)frame_offset, (unsigned)attempt); - if (resp_type) *resp_type = parsed_type; + + if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) { + if (resp_type) *resp_type = found_type; if (resp_payload && resp_max > 0) { - size_t copy_len = (parsed_len < resp_max) ? parsed_len : resp_max; - memcpy(resp_payload, parsed_payload, copy_len); - if (resp_len) *resp_len = copy_len; - } else if (resp_len) { - *resp_len = parsed_len; + size_t copy_len = (found_len < resp_max) ? found_len : resp_max; + if (found_payload && copy_len > 0) { + memcpy(resp_payload, found_payload, copy_len); + } + } + if (resp_len) *resp_len = found_len; + + DEBUG_DBG("SPI", "OTA 响应: type=0x%02X len=%u offset=%u", + found_type, (unsigned)found_len, (unsigned)frame_offset); + + if (found_type == SPI_OTA_RSP_WAIT) { + vTaskDelay(pdMS_TO_TICKS(10)); + continue; } return ESP_OK; } - bool all_ff = true; - for (size_t i = 0; i < sizeof(rx_raw); ++i) { - if (rx_raw[i] != 0xFF) { - all_ff = false; - break; - } - } - DEBUG_DBG("SPI", "OTA响应未就绪[%u]: %s first10=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", - attempt, all_ff ? "all-FF" : "no-valid-SOF", - rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3], - rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7], - rx_raw[8], rx_raw[9]); - vTaskDelay(pdMS_TO_TICKS(1)); + vTaskDelay(pdMS_TO_TICKS(10)); } - DEBUG_DBG("SPI", "OTA响应超时: timeout=%ums", (unsigned)timeout_ms); + DEBUG_WARN("SPI", "OTA 响应超时: %lu ms", (unsigned long)timeout_ms); return ESP_ERR_TIMEOUT; } -/** - * @brief 执行一次 SPI OTA 请求事务并等待响应帧。 - * @param type 请求帧类型。 - * @param payload 请求负载。 - * @param payload_len 负载长度。 - * @param resp_type 输出响应类型,可为 NULL。 - * @param resp_payload 输出响应负载缓冲区,可为 NULL。 - * @param resp_max 响应负载缓冲区大小。 - * @param resp_len 输出响应长度,可为 NULL。 - * @param timeout_ms 响应等待超时时间(毫秒)。 - * @return ESP_OK 成功,其他为失败错误码。 - */ -esp_err_t spi_ota_transfer(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) +esp_err_t SpiMaster::appRequestBootloader(uint32_t timeout_ms) { - if (g_spi_device == NULL) { - return ESP_ERR_INVALID_STATE; - } - if (resp_len) *resp_len = 0; - if (resp_type) *resp_type = 0; + DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader..."); - uint8_t tx_frame[SPI_OTA_FRAME_MAX] = {0}; - size_t tx_len = 0; - if (!spi_ota_build_frame(type, payload, payload_len, tx_frame, &tx_len)) { - return ESP_ERR_INVALID_ARG; - } - - DEBUG_DBG("SPI", "OTA事务: req_type=0x%02X req_len=%u", type, (unsigned)payload_len); - - /* ============================================================ - * 第1次 CS 周期:发送请求帧(只发不收) - * - * ESP32 发完请求帧后 CS↑,GD32 在 CS↑ 的 onCsRelease 中 - * 或 RBNE 中断的 tryProcessCompleteFrame 中处理帧, - * 将 ACK 写入 s_txBuf 并启动 DMA。 - * - * 注意:不能在同一个 CS 周期内发请求+读响应,因为: - * 1. SPI 全双工:ESP32 发请求时 MISO 同时在输出旧数据 - * 2. GD32 收到请求帧后才能准备 ACK,需重启 DMA - * 3. DMA 重启期间 MISO 会输出 0x00/0xFF,导致帧断裂 - * ============================================================ */ - esp_err_t ret = spi_transfer_clocked(tx_frame, tx_len, NULL, 0); - if (ret != ESP_OK) { - DEBUG_ERR("SPI", "SPI OTA 请求发送失败: %s", esp_err_to_name(ret)); - return ret; - } - - /* 等 GD32 处理帧 + 准备响应。 - * - * GD32 在 CS↑ 的 onCsRelease() 中处理帧: - * isrProcessFrame() → OTA handler 回调 → queueTxFrame() → DMA 启动 - * 总耗时约 10-20µs(ISR 内执行,无调度延迟)。 - * - * 但 EXTI ISR 可能被更高优先级的 RBNE 中断短暂抢占, - * 实际完成时间可能到 30-50µs。 - * - * 500µs 提供了 10x 以上的安全余量, - * 同时对 OTA 事务延迟影响可忽略(一次 OTA 事务 ~1ms 级别)。 */ - esp_rom_delay_us(500); - - /* ============================================================ - * 第2次 CS 周期:发 0xFF dummy 产生时钟,读 MISO 上的响应帧 - * - * GD32 的 onCsAssert 会重启 DMA,将 s_txBuf(ACK帧) - * 推到 MISO,ESP32 在 96 字节中扫描定位响应帧。 - * ============================================================ */ - return spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms); -} - -// CANFD 帧发送 -// 帧格式: [通道(1B)] [扩展帧ID(4B BE)] [数据(1~64B)] -/** - * @brief 发送一帧 CANFD 数据到 GD32,并可选等待响应。 - * @param channel CAN 通道号。 - * @param ext_id 扩展帧 ID(29-bit)。 - * @param data 发送负载指针。 - * @param dlc 发送数据长度。 - * @param recv_buf 接收缓冲区,可为 NULL。 - * @param recv_max 接收缓冲区最大长度。 - * @param timeout_ms 等待响应超时(毫秒)。 - * @return >=0 为接收字节数,<0 为错误码。 - */ -static int can_frame_send(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, - uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms) -{ - if (g_spi_device == NULL) { - DEBUG_ERR("SPI", "CAN 发送: SPI 设备未初始化"); - return -1; - } - - if (dlc == 0 || dlc > CAN_MAX_DLC) { - DEBUG_ERR("SPI", "CAN[ch%d] DLC 无效: %d (范围 1~%d)", channel, dlc, CAN_MAX_DLC); - return -2; - } - - if (channel >= CAN_CHANNEL_MAX) { - DEBUG_ERR("SPI", "CAN 通道号无效: %d (范围 0~%d)", channel, CAN_CHANNEL_MAX - 1); - return -3; - } - - DEBUG_DBG("SPI", "CAN[ch%d] ID=0x%08X 发送: DLC=%d", channel, ext_id, dlc); - - // 构建 CANFD 帧: [通道(1B)] [EXT_ID(4B)] [DATA(dlc B)] - uint8_t tx_buf[1 + 4 + CAN_MAX_DLC] = {0}; - tx_buf[0] = (uint8_t)channel; - tx_buf[1] = (uint8_t)((ext_id >> 24) & 0xFF); - tx_buf[2] = (uint8_t)((ext_id >> 16) & 0xFF); - tx_buf[3] = (uint8_t)((ext_id >> 8) & 0xFF); - tx_buf[4] = (uint8_t)(ext_id & 0xFF); - memcpy(&tx_buf[5], data, dlc); - - size_t tx_len = 5 + dlc; - uint8_t rx_buf[5 + CAN_MAX_DLC] = {0}; - - const size_t rx_len = (recv_buf != NULL && recv_max > 0U) ? (size_t)(5U + recv_max) : 0U; - esp_err_t ret = spi_transfer_clocked(tx_buf, tx_len, rx_buf, rx_len); - if (ret != ESP_OK) { - DEBUG_ERR("SPI", "CAN[ch%d] ID=0x%08X 发送失败: %s", - channel, ext_id, esp_err_to_name(ret)); - return -4; - } - - if (recv_buf && recv_max > 0) { - // 解析 GD32 响应帧 - uint8_t resp_channel = rx_buf[0]; - uint32_t resp_id = (rx_buf[1] << 24) | (rx_buf[2] << 16) - | (rx_buf[3] << 8) | rx_buf[4]; - uint8_t resp_dlc = resp_id >> 24; // 复用了 ID 字段低字节作为 DLC - resp_id &= 0x00FFFFFF; - - uint8_t copy_len = (resp_dlc < recv_max) ? resp_dlc : recv_max; - memcpy(recv_buf, &rx_buf[5], copy_len); - - DEBUG_INFO("SPI", "CAN[ch%d] ID=0x%08X 响应: DLC=%d, 接收=%d 字节", - resp_channel, resp_id, copy_len, copy_len); - return copy_len; - } - - DEBUG_DBG("SPI", "CAN[ch%d] ID=0x%08X 发送完成 (无响应)", channel, ext_id); - return 0; -} - -/** - * @brief 对外暴露的 GD32 CAN 发送接口,必要时修正默认 DLC。 - * @param channel CAN 通道。 - * @param ext_id 扩展帧 ID。 - * @param data 发送数据指针。 - * @param dlc 数据长度。 - * @param recv_buf 接收缓冲区,可为 NULL。 - * @param recv_max 接收缓冲区大小。 - * @param timeout_ms 等待响应超时(毫秒)。 - * @return >=0 为接收字节数,<0 为错误码。 - */ -int gd32_can_send(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, - uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms) -{ - return can_frame_send(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms); -} - -/** - * @brief 发送无需响应的 GD32 CAN 帧。 - * @param channel CAN 通道。 - * @param ext_id 扩展帧 ID。 - * @param data 发送数据指针。 - * @param dlc 数据长度。 - * @return 0 表示已发送完成,<0 为错误码。 - */ -int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc) -{ - return can_frame_send(channel, ext_id, data, dlc, NULL, 0, 0); -} - -/** - * @brief 向正在运行的 GD32 DCU_APP 发送进入 Bootloader 的短 SPI 命令。 - * @param timeout_ms 等待 APP 返回确认的最长时间。 - * @return ESP_OK 表示 APP 已接收命令;超时则通常表示 GD32 已不在 APP 或 SPI 未就绪。 - */ -esp_err_t gd32_app_request_bootloader(uint32_t timeout_ms) -{ uint8_t cmd = SPI_APP_ENTER_BOOT_CMD; - esp_err_t ret = spi_transfer_clocked(&cmd, 1U, NULL, 0U); + esp_err_t ret = transferClocked(&cmd, 1, NULL, 0); if (ret != ESP_OK) { - DEBUG_WARN("SPI", "APP进入BOOT命令发送失败: %s", esp_err_to_name(ret)); + DEBUG_WARN("SPI", "发送 Bootloader 请求失败: %s", esp_err_to_name(ret)); return ret; } - uint8_t rx[8] = {0}; - uint8_t dummy[8]; - memset(dummy, 0xFF, sizeof(dummy)); TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); + while (xTaskGetTickCount() <= deadline) { - memset(rx, 0xFF, sizeof(rx)); - ret = spi_transfer_clocked(dummy, sizeof(dummy), rx, sizeof(rx)); - if (ret == ESP_OK) { - for (size_t i = 0; i + 3U < sizeof(rx); ++i) { - if (rx[i] == SPI_APP_ENTER_BOOT_CMD && rx[i + 1U] == SPI_APP_ENTER_BOOT_ACK_OK) { - DEBUG_INFO("SPI", "GD32 APP确认进入Bootloader OTA窗口"); - return ESP_OK; - } - } + uint8_t rx = 0; + uint8_t dummy = 0xFF; + ret = transferClocked(&dummy, 1, &rx, 1); + if (ret == ESP_OK && rx == SPI_APP_ENTER_BOOT_ACK_OK) { + DEBUG_INFO("SPI", "GD32 APP 已确认进入 Bootloader"); + return ESP_OK; } vTaskDelay(pdMS_TO_TICKS(10)); } - DEBUG_WARN("SPI", "GD32 APP进入Bootloader确认超时"); + DEBUG_WARN("SPI", "GD32 APP 进入 Bootloader 确认超时"); return ESP_ERR_TIMEOUT; } -esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec) +esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec) { - /* APP 侧 SPI 从机当前是中断逐字节收发,不适合像 BOOT 那样一次读 96B 长轮询。 - * 这里改为:每次都完整重发一帧 PING 请求,然后只读一个短窗口寻找 ACK。 - * 这样即便某次响应窗口错过,也会在下一次重发时重新生成 APP_ALIVE。 */ constexpr uint8_t kAppPingType = 0xA1U; constexpr size_t kAppPingPollBytes = 32U; uint8_t tx_frame[8] = {0}; size_t tx_len = 0U; - if (!spi_ota_build_frame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) { + if (!otaBuildFrame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) { return ESP_ERR_INVALID_ARG; } @@ -654,7 +373,7 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec) while (xTaskGetTickCount() <= deadline) { ++attempt; - esp_err_t err = spi_transfer_clocked(tx_frame, tx_len, NULL, 0U); + esp_err_t err = transferClocked(tx_frame, tx_len, NULL, 0U); if (err != ESP_OK) { DEBUG_DBG("SPI", "APP PING 请求发送失败[%lu]: %s", (unsigned long)attempt, esp_err_to_name(err)); @@ -669,7 +388,7 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec) memset(dummy, 0xFF, sizeof(dummy)); memset(rx_raw, 0xFF, sizeof(rx_raw)); - err = spi_transfer_clocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw)); + err = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw)); if (err != ESP_OK) { DEBUG_DBG("SPI", "APP PING 响应读取失败[%lu]: %s", (unsigned long)attempt, esp_err_to_name(err)); @@ -681,8 +400,8 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec) const uint8_t *resp_payload = nullptr; size_t resp_len = 0U; size_t frame_offset = 0U; - if (spi_ota_find_frame(rx_raw, sizeof(rx_raw), - &resp_type, &resp_payload, &resp_len, &frame_offset)) { + if (otaFindFrame(rx_raw, sizeof(rx_raw), + &resp_type, &resp_payload, &resp_len, &frame_offset)) { if (resp_type == SPI_OTA_RSP_ACK && resp_len >= 3U && resp_payload[0] == kAppPingType) { if (uptime_sec) { *uptime_sec = (uint32_t)resp_payload[1] | ((uint32_t)resp_payload[2] << 8); @@ -714,20 +433,9 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec) return ESP_ERR_TIMEOUT; } -// 带重试的 SPI 传输 -/** - * @brief 执行带重试的 SPI 同步传输。 - * @param cmd SPI 命令码。 - * @param payload 发送数据指针。 - * @param payload_len 发送长度。 - * @param recv_buf 接收缓冲区。 - * @param recv_len 接收长度。 - * @param max_retries 最大重试次数。 - * @return ESP_OK 成功,重试耗尽返回 ESP_ERR_TIMEOUT。 - */ -esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, int max_retries) +esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload, + size_t payload_len, uint8_t *recv_buf, + size_t recv_len, int max_retries) { esp_err_t ret; int retry_count = 0; @@ -735,8 +443,8 @@ esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload, DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries); while (retry_count < max_retries) { - ret = spi_sync_transfer(cmd, payload, payload_len, - recv_buf, recv_len, 3000); + ret = syncTransfer(cmd, payload, payload_len, + recv_buf, recv_len, 3000); if (ret == ESP_OK) { if (retry_count > 0) { DEBUG_INFO("SPI", "SPI 传输成功 (重试 %d 次后成功)", retry_count); @@ -745,7 +453,7 @@ esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload, } retry_count++; - DEBUG_WARN("SPI", "SPI 重试 %d/%d, cmd=0x%02X, err=%s", + DEBUG_WARN("SPI", "SPI 重试 %d/%d, cmd=0x%02X, err=%s", retry_count, max_retries, cmd, esp_err_to_name(ret)); vTaskDelay(pdMS_TO_TICKS(100)); } @@ -753,3 +461,60 @@ esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload, DEBUG_ERR("SPI", "SPI 传输 %d 次重试后失败, cmd=0x%02X", max_retries, cmd); return ESP_ERR_TIMEOUT; } + +// ============================================================ +// CANFD 接口 +// ============================================================ +int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc, + uint8_t *recv_buf, uint8_t recv_max, + uint32_t timeout_ms) +{ + if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { + return -1; + } + + uint8_t tx_buf[72]; + size_t tx_len = 0; + + tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF); + tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF); + memcpy(&tx_buf[tx_len], data, dlc); + tx_len += dlc; + + esp_err_t ret = syncTransfer(0, tx_buf, tx_len, recv_buf, recv_max, timeout_ms); + if (ret != ESP_OK) { + return -1; + } + + return (int)recv_max; +} + +int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc) +{ + if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { + return -1; + } + + uint8_t tx_buf[72]; + size_t tx_len = 0; + + tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF); + tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF); + memcpy(&tx_buf[tx_len], data, dlc); + tx_len += dlc; + + esp_err_t ret = send(0, tx_buf, tx_len); + if (ret != ESP_OK) { + return -1; + } + + return 0; +} \ No newline at end of file diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index ce3d54f..c45b295 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -1,6 +1,8 @@ -/* +/* * spi_master.h - SPI Master 驱动头文件 * CANFD 3通道发送接口 + 底层 SPI 总线 + * + * SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。 */ #ifndef SPI_MASTER_H #define SPI_MASTER_H @@ -26,9 +28,6 @@ #define SPI_OTA_RSP_WAIT (0x81) #define SPI_OTA_RSP_ERROR (0x82) -// SPI 设备句柄(外部引用) -extern spi_device_handle_t g_spi_device; - // ============================================================ // CANFD 帧格式(3通道) // [通道编号: 1B] [扩展帧ID: 4B BE] [数据: 1~64B] @@ -55,104 +54,96 @@ typedef struct { } can_frame_t; /** - * @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。 - * @param channel CAN 通道(0~2)。 - * @param ext_id 29 位扩展帧 ID。 - * @param data 数据负载指针。 - * @param dlc 数据长度,范围 1~64。 - * @param recv_buf 接收缓冲区,可为 NULL。 - * @param recv_max 期望最大接收字节数。 - * @param timeout_ms 超时时间(毫秒)。 - * @return >= 0 接收字节数,< 0 失败。 + * @brief SPI Master 驱动类(单例) + * + * 封装 SPI 总线初始化、同步传输、CANFD 通信和 SPI OTA 协议。 */ -int gd32_can_send(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, - uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms); +class SpiMaster { +public: + static SpiMaster& instance(); -/** - * @brief 发送一帧无需响应的 CANFD 数据,适合广播或通知场景。 - * @param channel CAN 通道(0~2)。 - * @param ext_id 29 位扩展帧 ID。 - * @param data 数据负载指针。 - * @param dlc 数据长度,范围 1~64。 - * @return >= 0 成功,< 0 失败。 - */ -int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc); + /** @brief 初始化 SPI2 总线与目标从设备。 */ + esp_err_t init(); -/** - * @brief 初始化 SPI2 总线与目标从设备。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_master_init(void); + // ============================================================ + // 底层 SPI 传输 + // ============================================================ -/** - * @brief 执行一次同步 SPI 事务,先发送请求,再读取请求之后的响应字节。 - * @param cmd SPI 命令码;为 0 时不额外插入命令字节。 - * @param payload 发送数据指针。 - * @param payload_len 发送数据长度。 - * @param recv_buf 接收缓冲区,可为 NULL。 - * @param recv_len 期望接收的响应长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, uint32_t timeout_ms); + /** @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。 */ + esp_err_t syncTransfer(uint8_t cmd, const uint8_t *payload, + size_t payload_len, uint8_t *recv_buf, + size_t recv_len, uint32_t timeout_ms); -/** - * @brief 仅发送 SPI 请求数据,不关心返回内容。 - * @param cmd SPI 命令码;为 0 时仅发送 payload。 - * @param payload 发送数据指针。 - * @param len 发送数据长度。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len); + /** @brief 仅发送 SPI 请求数据,不关心返回内容。 */ + esp_err_t send(uint8_t cmd, const uint8_t *payload, size_t len); -/** - * @brief 仅接收 SPI 数据,必要时先发送命令字节,再发送哑数据产生时钟。 - * @param cmd SPI 命令码;为 0 时只发送哑数据。 - * @param recv_buf 接收缓冲区。 - * @param len 接收长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return ESP_OK 成功,其他失败。 - */ -esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms); + /** @brief 仅接收 SPI 数据,必要时先发送命令字节。 */ + esp_err_t recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms); -/** - * @brief 按 GD32 SPI OTA 最小协议执行一次请求事务 + 一次响应读取事务。 - * @param type 请求帧类型(REQ/DATA/CANCEL)。 - * @param payload 请求负载。 - * @param payload_len 负载长度。 - * @param resp_type 输出响应类型(ACK/WAIT/ERROR)。 - * @param resp_payload 输出响应负载缓冲区。 - * @param resp_max 输出响应负载最大长度。 - * @param resp_len 输出实际响应负载长度。 - * @param timeout_ms 超时。 - */ -esp_err_t spi_ota_transfer(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); + // ============================================================ + // SPI OTA 协议 + // ============================================================ -/** - * @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 - */ -esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload, - size_t resp_max, size_t *resp_len, uint32_t timeout_ms); + /** @brief 按 GD32 SPI OTA 最小协议执行一次请求 + 响应读取。 */ + esp_err_t otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms); -/** - * @brief 通知正在运行的 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 - * @return ESP_OK 表示 APP 已确认;ESP_ERR_TIMEOUT 常表示 GD32 已在 Bootloader 或通信未就绪。 - */ -esp_err_t gd32_app_request_bootloader(uint32_t timeout_ms); + /** @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 */ + esp_err_t otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms); -/** - * @brief 向 GD32 APP 发送存活探测命令 (TYPE=0xA1),等待 APP 回复 ALIVE ACK。 - * @param timeout_ms 超时时间(毫秒)。 - * @param uptime_sec 输出 APP 运行秒数,可为 NULL。 - * @return ESP_OK APP 存活并回复;ESP_ERR_TIMEOUT 超时无响应。 - */ -esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec); + /** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */ + esp_err_t appRequestBootloader(uint32_t timeout_ms); -#endif // SPI_MASTER_H + /** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */ + esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec); + + /** @brief 执行带重试的 SPI 同步传输。 */ + esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload, + size_t payload_len, uint8_t *recv_buf, + size_t recv_len, int max_retries); + + // ============================================================ + // CANFD 接口 + // ============================================================ + + /** @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。 */ + int canSend(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc, + uint8_t *recv_buf, uint8_t recv_max, + uint32_t timeout_ms); + + /** @brief 发送一帧无需响应的 CANFD 数据。 */ + int canSendNoReply(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc); + + /** @brief 获取 SPI 设备句柄(供外部状态查询)。 */ + spi_device_handle_t deviceHandle() const { return m_spiDevice; } + +private: + SpiMaster() = default; + ~SpiMaster() = default; + SpiMaster(const SpiMaster&) = delete; + SpiMaster& operator=(const SpiMaster&) = delete; + + spi_device_handle_t m_spiDevice = NULL; + + // 内部 SPI 传输 + esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len, + uint8_t *rx_buf, size_t rx_len); + esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len, + uint8_t *rx_buf, size_t rx_len); + + // OTA 帧工具(静态,无状态) + static uint8_t otaCrc8(const uint8_t *data, size_t len); + static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t *out_len); + static bool otaParseFrame(const uint8_t *frame, size_t frame_len, + uint8_t *type, const uint8_t **payload, size_t *payload_len); + static bool otaFindFrame(const uint8_t *buf, size_t buf_len, + uint8_t *type, const uint8_t **payload, size_t *payload_len, + size_t *frame_offset); +}; + +#endif // SPI_MASTER_H \ No newline at end of file