From 2875ef21457a8742064b129197905471dc236c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Mon, 15 Jun 2026 01:14:29 +0800 Subject: [PATCH] =?UTF-8?q?docs(ota=5Fmutex):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=B3=A8=E9=87=8A=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/include/ota_mutex.h | 2 + main/monitor/debug_log.cpp | 49 +++++++ main/monitor/debug_log.h | 4 + main/monitor/health_monitor.cpp | 55 +++++++ main/monitor/health_monitor.h | 4 + main/ota/esp32_ota_core.cpp | 37 +++++ main/ota/esp32_ota_core.h | 41 ++++++ main/relay/ota_relay.cpp | 162 +++++++++++++++++++++ main/relay/ota_relay.h | 246 ++++++++++++++++++++++++++++++++ main/server/ble_ota.cpp | 8 ++ main/server/ble_ota.h | 25 ++++ main/server/wifi_ota.cpp | 10 ++ main/server/wifi_ota.h | 71 +++++++++ main/transport/protocol.cpp | 7 + main/transport/protocol.h | 24 ++++ main/transport/spi_master.cpp | 173 ++++++++++++++++++++++ main/transport/spi_master.h | 108 ++++++++++++++ 17 files changed, 1026 insertions(+) diff --git a/main/include/ota_mutex.h b/main/include/ota_mutex.h index 92d8ad2..a443119 100644 --- a/main/include/ota_mutex.h +++ b/main/include/ota_mutex.h @@ -14,6 +14,7 @@ extern volatile bool g_esp32OtaOccupied; // 获取全局 OTA 占用标志(带超时),成功返回 true // 内部: Take Mutex → 检查标志 → 若空闲则置 true → Give Mutex +/** @brief 获取全局 OTA 占用标志(带超时),成功返回 true。 */ static inline bool esp32_ota_mutex_acquire(uint32_t timeout_ms) { if (!g_esp32OtaMutex) return false; if (xSemaphoreTake(g_esp32OtaMutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) return false; @@ -28,6 +29,7 @@ static inline bool esp32_ota_mutex_acquire(uint32_t timeout_ms) { // 释放全局 OTA 占用标志 // 内部: Take Mutex → 清除标志 → Give Mutex +/** @brief 释放全局 OTA 占用标志。 */ static inline void esp32_ota_mutex_release(void) { if (!g_esp32OtaMutex) return; xSemaphoreTake(g_esp32OtaMutex, portMAX_DELAY); diff --git a/main/monitor/debug_log.cpp b/main/monitor/debug_log.cpp index 9b80905..e7f226f 100644 --- a/main/monitor/debug_log.cpp +++ b/main/monitor/debug_log.cpp @@ -33,6 +33,10 @@ static const char *TAG = "DEBUG_LOG"; // ============================================================ // 单例 // ============================================================ +/** + * @brief 获取 DebugLog 单例实例。 + * @return DebugLog 引用。 + */ DebugLog& DebugLog::instance() { static DebugLog inst; @@ -42,6 +46,10 @@ DebugLog& DebugLog::instance() // ============================================================ // 日志环形缓冲区(供 HTTP /logs 拉取) // ============================================================ +/** + * @brief 获取当前环形日志缓冲区中的日志条数。 + * @return 日志条数。 + */ int DebugLog::ringCount() { if (!m_logRingMutex) return 0; @@ -51,6 +59,11 @@ int DebugLog::ringCount() return count; } +/** + * @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串。 + * @param out_len 输出参数,返回 JSON 字符串长度。 + * @return 指向 JSON 字符串的指针,需调用者 free。 + */ char *DebugLog::ringDrainJson(int *out_len) { if (!m_logRingMutex) { @@ -117,6 +130,11 @@ char *DebugLog::ringDrainJson(int *out_len) return json; } +/** + * @brief 向环形日志缓冲区中推送一条日志数据。 + * @param data 日志数据指针。 + * @param len 数据长度。 + */ void DebugLog::ringPush(const char *data, int len) { if (!m_logRingMutex || len <= 0) return; @@ -139,6 +157,10 @@ void DebugLog::ringPush(const char *data, int len) // ============================================================ // UART 调试日志 // ============================================================ +/** + * @brief 初始化 UART0 调试串口。 + * @return esp_err_t 错误码。 + */ esp_err_t DebugLog::uartInit() { if (m_uartInitialized) { @@ -180,6 +202,12 @@ esp_err_t DebugLog::uartInit() return ESP_OK; } +/** + * @brief 通过 UART0 直接发送原始字节数据。 + * @param data 待发送数据指针。 + * @param len 数据长度。 + * @return esp_err_t 错误码。 + */ esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len) { if (!m_uartInitialized) { @@ -197,6 +225,10 @@ esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len) // ============================================================ // 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建) // ============================================================ +/** + * @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 + * @return esp_err_t 错误码。 + */ esp_err_t DebugLog::init() { if (m_initialized) { @@ -219,6 +251,10 @@ esp_err_t DebugLog::init() return ESP_OK; } +/** + * @brief 确保 UDP socket 已创建(延迟创建)。 + * @return esp_err_t 错误码。 + */ esp_err_t DebugLog::ensureUdpSocket() { if (m_udpSock >= 0) { @@ -235,6 +271,12 @@ esp_err_t DebugLog::ensureUdpSocket() return ESP_OK; } +/** + * @brief 发送原始日志数据到底层输出通道。 + * @param data 待发送数据指针。 + * @param len 数据长度。 + * @return esp_err_t 错误码。 + */ esp_err_t DebugLog::send(const uint8_t *data, size_t len) { esp_err_t udp_ret = ESP_FAIL; @@ -270,6 +312,13 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len) return ESP_FAIL; } +/** + * @brief 按日志级别与标签格式化输出一条日志。 + * @param level 日志级别。 + * @param tag 日志标签。 + * @param fmt 格式化字符串。 + * @param ... 可变参数。 + */ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...) { if (!m_initialized) { diff --git a/main/monitor/debug_log.h b/main/monitor/debug_log.h index 527712a..f1cb284 100644 --- a/main/monitor/debug_log.h +++ b/main/monitor/debug_log.h @@ -48,6 +48,10 @@ typedef enum { */ class DebugLog { public: + /** + * @brief 获取 DebugLog 单例实例。 + * @return DebugLog 引用。 + */ static DebugLog& instance(); /** @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 */ diff --git a/main/monitor/health_monitor.cpp b/main/monitor/health_monitor.cpp index 8a5aa63..e943669 100644 --- a/main/monitor/health_monitor.cpp +++ b/main/monitor/health_monitor.cpp @@ -23,6 +23,10 @@ // ============================================================ // 单例 // ============================================================ +/** + * @brief 获取 HealthMonitor 单例实例。 + * @return HealthMonitor 引用。 + */ HealthMonitor& HealthMonitor::instance() { static HealthMonitor inst; @@ -32,6 +36,9 @@ HealthMonitor& HealthMonitor::instance() // ============================================================ // 初始化 // ============================================================ +/** + * @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 + */ void HealthMonitor::init() { m_mutex = xSemaphoreCreateMutex(); @@ -43,6 +50,10 @@ void HealthMonitor::init() // ============================================================ // 线程安全的健康状态读写 // ============================================================ +/** + * @brief 加锁读取当前健康状态。 + * @return 当前健康状态。 + */ health_state_t HealthMonitor::getState() { if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY); @@ -51,6 +62,10 @@ health_state_t HealthMonitor::getState() return s; } +/** + * @brief 加锁设置健康状态。 + * @param newState 新的健康状态。 + */ void HealthMonitor::setState(health_state_t newState) { if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY); @@ -61,11 +76,19 @@ void HealthMonitor::setState(health_state_t newState) // ============================================================ // 获取当前健康状态(public 接口) // ============================================================ +/** + * @brief 获取当前系统健康状态(线程安全)。 + * @return 当前健康状态。 + */ health_state_t HealthMonitor::getCurrent() { return getState(); } +/** + * @brief 获取最近一次芯片温度。 + * @return 温度值(摄氏度)。 + */ float HealthMonitor::getLastTemperature() { if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY); @@ -74,6 +97,12 @@ float HealthMonitor::getLastTemperature() return t; } +/** + * @brief 获取最近一次 WiFi RSSI 及其年龄。 + * @param out_rssi 输出参数,返回 RSSI 值。 + * @param out_age_ms 输出参数,返回样本年龄(毫秒)。 + * @return 是否有有效数据。 + */ bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms) { if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY); @@ -96,6 +125,10 @@ bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms) // ============================================================ // 温度监控(定时器回调) // ============================================================ +/** + * @brief 温度监控定时器回调函数。 + * @param arg 用户参数(未使用)。 + */ void HealthMonitor::tempMonitorCallback(void *arg) { auto& self = instance(); @@ -127,6 +160,10 @@ void HealthMonitor::tempMonitorCallback(void *arg) // ============================================================ // FreeRTOS 任务入口 // ============================================================ +/** + * @brief 健康监控 FreeRTOS 任务入口。 + * @param pvParameters 任务参数。 + */ void HealthMonitor::taskEntry(void *pvParameters) { auto& self = instance(); @@ -173,6 +210,11 @@ void HealthMonitor::taskEntry(void *pvParameters) // ============================================================ // 健康事件日志 // ============================================================ +/** + * @brief 记录一条健康事件日志。 + * @param event 事件名称。 + * @param value 事件值。 + */ void HealthMonitor::logEvent(const char *event, int value) { DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value); @@ -181,6 +223,10 @@ void HealthMonitor::logEvent(const char *event, int value) // ============================================================ // OTA 健康检查 // ============================================================ +/** + * @brief OTA 传输前执行健康检查。 + * @return 健康检查状态。 + */ health_status_t HealthMonitor::checkBeforeOta() { health_status_t status = HEALTH_MON_OK; @@ -227,6 +273,10 @@ health_status_t HealthMonitor::checkBeforeOta() return status; } +/** + * @brief OTA 中继转发阶段执行健康检查(跳过 WiFi 检查)。 + * @return 健康检查状态。 + */ health_status_t HealthMonitor::checkBeforeOtaRelay() { health_status_t status = HEALTH_MON_OK; @@ -261,6 +311,11 @@ health_status_t HealthMonitor::checkBeforeOtaRelay() // ============================================================ // WiFi RSSI 更新 // ============================================================ +/** + * @brief 根据 WiFi RSSI 更新健康状态。 + * @param rssi WiFi RSSI 值。 + * @return 健康检查状态。 + */ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi) { if (rssi < WIFI_RSSI_CRITICAL) { diff --git a/main/monitor/health_monitor.h b/main/monitor/health_monitor.h index fe625de..c4665ab 100644 --- a/main/monitor/health_monitor.h +++ b/main/monitor/health_monitor.h @@ -37,6 +37,10 @@ typedef enum { */ class HealthMonitor { public: + /** + * @brief 获取 HealthMonitor 单例实例。 + * @return HealthMonitor 引用。 + */ static HealthMonitor& instance(); /** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */ diff --git a/main/ota/esp32_ota_core.cpp b/main/ota/esp32_ota_core.cpp index 3e4b8f3..6ba112d 100644 --- a/main/ota/esp32_ota_core.cpp +++ b/main/ota/esp32_ota_core.cpp @@ -23,12 +23,19 @@ #define TAG "OTA_CORE" +/** + * @brief 获取 Esp32OtaCore 单例实例。 + * @return Esp32OtaCore 引用。 + */ Esp32OtaCore& Esp32OtaCore::instance() { static Esp32OtaCore inst; return inst; } +/** + * @brief 复位所有状态变量。 + */ void Esp32OtaCore::resetState() { m_active = false; @@ -40,6 +47,11 @@ void Esp32OtaCore::resetState() m_ended = false; } +/** + * @brief 开启 OTA 会话。 + * @param total_size 固件总大小(字节)。 + * @return ESP_OK 成功,ESP_ERR_INVALID_STATE 被占用,ESP_FAIL 其他错误。 + */ esp_err_t Esp32OtaCore::begin(size_t total_size) { // 若本核心已有未完成会话,先中止并释放全局互斥 @@ -109,6 +121,12 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) return ESP_OK; } +/** + * @brief 写入固件数据块。 + * @param data 数据缓冲区。 + * @param len 数据长度。 + * @return ESP_OK 成功,否则错误码。 + */ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len) { if (!m_active || m_partition == nullptr) { @@ -124,6 +142,10 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len) return err; } +/** + * @brief 从 OTA 分区读回数据并计算 SHA-256。 + * @return ESP_OK 成功,否则错误码。 + */ esp_err_t Esp32OtaCore::computeShaFromPartition() { if (m_partition == NULL || m_received == 0) { @@ -172,6 +194,11 @@ esp_err_t Esp32OtaCore::computeShaFromPartition() return ESP_OK; } +/** + * @brief 计算设备端 SHA-256 并输出 hex 字符串。 + * @param out_device_sha_hex 输出缓冲区,至少 65 字节。 + * @return ESP_OK 成功,否则错误码。 + */ esp_err_t Esp32OtaCore::computeDeviceSha(char *out_device_sha_hex) { if (!m_active || m_partition == NULL) { @@ -191,6 +218,13 @@ esp_err_t Esp32OtaCore::computeDeviceSha(char *out_device_sha_hex) return ESP_OK; } +/** + * @brief 收尾 OTA 会话:end、校验 SHA、set_boot、可选重启。 + * @param frontend_sha_hex 前端提供的 64 字符 hex SHA-256。 + * @param reboot 成功后是否重启。 + * @param out_device_sha_hex 输出设备端 SHA hex 缓冲区。 + * @return ESP_OK 成功,ESP_ERR_INVALID_VERSION SHA 不匹配,否则错误码。 + */ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, char *out_device_sha_hex) { @@ -287,6 +321,9 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, return ESP_OK; } +/** + * @brief 中止 OTA 会话并清理资源。 + */ void Esp32OtaCore::abort() { if (!m_active) { diff --git a/main/ota/esp32_ota_core.h b/main/ota/esp32_ota_core.h index 7892015..2439ebd 100644 --- a/main/ota/esp32_ota_core.h +++ b/main/ota/esp32_ota_core.h @@ -22,15 +22,30 @@ class Esp32OtaCore { public: + /** + * @brief 获取 Esp32OtaCore 单例实例。 + * @return Esp32OtaCore 引用。 + */ static Esp32OtaCore& instance(); // ── 阶段 1:开启 OTA 会话 ── // total_size: 固件总大小(字节),用于分区大小校验和传给 esp_ota_begin // 返回 ESP_OK / ESP_ERR_INVALID_STATE(被其它通道占用)/ ESP_FAIL + /** + * @brief 开启 OTA 会话。 + * @param total_size 固件总大小(字节)。 + * @return ESP_OK 成功,ESP_ERR_INVALID_STATE 被占用,ESP_FAIL 其他错误。 + */ esp_err_t begin(size_t total_size); // ── 阶段 2:写入固件数据(可在 begin 后多次调用)── // 纯 esp_ota_write,不做增量 SHA(SHA 在 finalize 时统一从分区读回计算) + /** + * @brief 写入固件数据块。 + * @param data 数据缓冲区。 + * @param len 数据长度。 + * @return ESP_OK 成功,否则错误码。 + */ esp_err_t write(const uint8_t *data, size_t len); // ── 阶段 3:收尾(end → 校验 SHA → set_boot → 可选重启)── @@ -41,6 +56,13 @@ public: // ESP_OK 成功(若 reboot=true,本调用不会返回) // ESP_ERR_INVALID_VERSION SHA 不匹配(已 abort 并清理) // 其他 end/set_boot 失败(已清理) + /** + * @brief 收尾 OTA 会话:end、校验 SHA、set_boot、可选重启。 + * @param frontend_sha_hex 前端提供的 64 字符 hex SHA-256。 + * @param reboot 成功后是否重启。 + * @param out_device_sha_hex 输出设备端 SHA hex 缓冲区。 + * @return ESP_OK 成功,ESP_ERR_INVALID_VERSION SHA 不匹配。 + */ esp_err_t finalize(const char *frontend_sha_hex, bool reboot, char *out_device_sha_hex); @@ -48,25 +70,44 @@ public: // 供前端只读校验场景(如 WiFi verify_only 模式) // out_device_sha_hex: 输出缓冲区,至少 65 字节 // 返回 ESP_OK / 失败 + /** + * @brief 仅计算设备端 SHA(不 end、不 set_boot、不重启)。 + * @param out_device_sha_hex 输出缓冲区,至少 65 字节。 + * @return ESP_OK 成功,否则错误码。 + */ esp_err_t computeDeviceSha(char *out_device_sha_hex); // ── 中止并清理(abort + 释放 mutex + 状态复位)── // 若 esp_ota_end 已成功(m_ended=true),则跳过 esp_ota_abort 仅复位状态 + /** + * @brief 中止并清理 OTA 会话。 + */ void abort(); // ── 状态查询 ── + /** @brief 查询 OTA 是否激活。 */ bool isActive() const { return m_active; } + /** @brief 获取已接收的字节数。 */ size_t getReceived() const { return m_received; } + /** @brief 查询是否等待重启。 */ bool isRebootPending() const { return m_rebootPending; } + /** @brief 设置重启待处理标志。 */ void setRebootPending(bool v) { m_rebootPending = v; } private: Esp32OtaCore() = default; // 从 OTA 分区读回计算 SHA-256(WiFi 风格),结果写入 m_sha256,置 m_shaComputed=true + /** + * @brief 从 OTA 分区读回计算 SHA-256。 + * @return ESP_OK 成功,否则错误码。 + */ esp_err_t computeShaFromPartition(); // 状态全复位(不调 esp_ota_abort,由调用方根据 m_ended 决定) + /** + * @brief 复位所有状态(不调 esp_ota_abort)。 + */ void resetState(); bool m_active = false; diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 876aab2..500a0c1 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -36,11 +36,18 @@ QueueHandle_t OtaRelay::dispatchQueue = NULL; QueueHandle_t OtaRelay::resultQueue = NULL; SemaphoreHandle_t OtaRelay::s_stateMutex = NULL; +/** + * @brief 获取OtaRelay单例引用. + * @return OtaRelay单例引用. + */ OtaRelay& OtaRelay::instance() { static OtaRelay inst; return inst; } +/** + * @brief 初始化状态互斥锁. + */ void OtaRelay::initMutex() { if (!s_stateMutex) { @@ -48,30 +55,50 @@ void OtaRelay::initMutex() } } +/** + * @brief 获取当前中继状态. + * @return 当前中继状态. + */ relay_state_t OtaRelay::getState() { StateLockGuard lock; return relayState; } +/** + * @brief 获取中继当前序号. + * @return 当前序号值. + */ uint32_t OtaRelay::getRelaySeq() { StateLockGuard lock; return relaySeq; } +/** + * @brief 获取中继总包数. + * @return 总包数. + */ uint32_t OtaRelay::getRelayTotalPackets() { StateLockGuard lock; return relayTotalPackets; } +/** + * @brief 获取GD32 APP存活标志位. + * @return true 存活,false 未存活. + */ bool OtaRelay::getGd32AppAlive() { StateLockGuard lock; return gd32AppAlive; } +/** + * @brief 获取GD32固件版本. + * @param out 输出参数,版本结构体指针. + */ void OtaRelay::getGd32Version(gd32_version_t *out) { if (!out) return; @@ -79,30 +106,50 @@ void OtaRelay::getGd32Version(gd32_version_t *out) *out = gd32Version; } +/** + * @brief 设置中继状态. + * @param s 目标状态值. + */ void OtaRelay::setState(relay_state_t s) { StateLockGuard lock; relayState = s; } +/** + * @brief 设置中继当前序号. + * @param seq 序号值. + */ void OtaRelay::setRelaySeq(uint32_t seq) { StateLockGuard lock; relaySeq = seq; } +/** + * @brief 设置中继总包数. + * @param total 总包数. + */ void OtaRelay::setRelayTotalPackets(uint32_t total) { StateLockGuard lock; relayTotalPackets = total; } +/** + * @brief 设置GD32 APP存活标志位. + * @param alive 存活状态. + */ void OtaRelay::setGd32AppAlive(bool alive) { StateLockGuard lock; gd32AppAlive = alive; } +/** + * @brief 设置GD32固件版本. + * @param ver 版本结构体引用. + */ void OtaRelay::setGd32Version(const gd32_version_t &ver) { StateLockGuard lock; @@ -115,6 +162,13 @@ void OtaRelay::setGd32Version(const gd32_version_t &ver) // 内部工具函数 // ============================================================ +/** + * @brief 检查缓冲区是否全部为指定值. + * @param buf 缓冲区指针. + * @param len 缓冲区长度. + * @param value 待检查的值. + * @return true 全部为指定值,false 不是. + */ static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value) { if (!buf || len == 0) return false; @@ -124,6 +178,13 @@ static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value) return true; } +/** + * @brief 使用AES-128-ECB加密单个16字节块. + * @param key 16字节密钥. + * @param in 16字节输入数据. + * @param out 16字节输出数据. + * @return 0 成功,-1 失败. + */ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out) { esp_aes_context ctx; @@ -134,6 +195,13 @@ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, return ret; } +/** + * @brief 记录响应载荷日志. + * @param stage 阶段名称字符串. + * @param resp_type 响应类型. + * @param payload 载荷指针. + * @param len 载荷长度. + */ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len) { char hex[193] = {0}; @@ -145,6 +213,17 @@ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_ n > 0 ? hex : "", len > n ? "..." : ""); } +/** + * @brief 循环等待直到响应不再是WAIT状态. + * @param stage 阶段名称字符串. + * @param resp_type 响应类型输出. + * @param resp_payload 响应载荷缓冲区. + * @param resp_max 缓冲区最大长度. + * @param resp_len 实际响应长度输出. + * @param timeout_ms 单次轮询超时毫秒. + * @param max_wait_rounds 最大WAIT轮次. + * @return ESP_OK 成功,其他失败. + */ 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) @@ -162,6 +241,13 @@ esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type, return ESP_OK; } +/** + * @brief 判断是否为带序号的结构化ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @param expected_seq 期望的序号. + * @return true 匹配成功,false 不匹配. + */ bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq) { if (!payload || len < 6) return false; @@ -171,6 +257,14 @@ bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32 return seq == expected_seq; } +/** + * @brief 解析结构化错误帧. + * @param payload 载荷指针. + * @param len 载荷长度. + * @param seq_out 输出错误序号. + * @param err_out 输出错误码. + * @return true 解析成功,false 解析失败. + */ bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out) { if (!payload || len < 7) return false; @@ -181,18 +275,48 @@ bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t return true; } +/** + * @brief 判断是否为握手回显ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是握手回显ACK,false 不是. + */ bool OtaRelay::isHandshakeEchoAck(const uint8_t *payload, size_t len) { return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU; } +/** + * @brief 判断是否为挑战应答ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是挑战应答ACK,false 不是. + */ bool OtaRelay::isChallengePayloadAck(const uint8_t *payload, size_t len) { return payload && len == GD32_CHALLENGE_RESPONSE_LEN; } +/** + * @brief 判断是否为普通确认ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是确认ACK,false 不是. + */ bool OtaRelay::isConfirmPlainAck(const uint8_t *payload, size_t len) { return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U; } +/** + * @brief 判断是否为大小序号0的ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是大小序号0ACK,false 不是. + */ bool OtaRelay::isSizeSeq0Ack(const uint8_t *payload, size_t len) { return isStructuredAckWithSeq(payload, len, 0U); } +/** + * @brief 判断是否为普通的结束ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是结束ACK,false 不是. + */ bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len) { if (!payload) return false; @@ -201,6 +325,19 @@ bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len) return false; } +/** + * @brief 轮询直到收到预期的ACK形状. + * @param stage 阶段名称字符串. + * @param resp_type 响应类型输出. + * @param resp_payload 响应载荷缓冲区. + * @param resp_max 缓冲区最大长度. + * @param resp_len 实际响应长度输出. + * @param poll_timeout_ms 轮询超时毫秒. + * @param wait_rounds WAIT状态最大轮询次数. + * @param maxExtraPolls 额外轮询次数. + * @param matcher 匹配回调函数. + * @return ESP_OK 成功,其他失败. + */ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage, uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, @@ -233,6 +370,14 @@ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage, return ESP_OK; } +/** + * @brief 解析并验证挑战应答载荷. + * @param resp_payload 响应载荷指针. + * @param resp_len 响应长度. + * @param out_k_session 输出会话密钥. + * @param out_uid 输出GD32 UID. + * @return true 验证通过,false 验证失败. + */ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len, uint8_t *out_k_session, uint8_t *out_uid) { @@ -265,6 +410,13 @@ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_ return true; } +/** + * @brief 使用AES-128-ECB加密48字节数据. + * @param key 16字节密钥. + * @param in 48字节输入数据. + * @param out 48字节输出数据. + * @return true 加密成功,false 加密失败. + */ bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out) { for (int i = 0; i < 3; ++i) @@ -272,6 +424,12 @@ bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, ui return true; } +/** + * @brief 使用密钥计算48字节数据的CRC32. + * @param key 16字节密钥. + * @param data48 48字节输入数据. + * @param output 输出4字节CRC32结果. + */ void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output) { uint32_t crc = 0xFFFFFFFFU; @@ -613,6 +771,10 @@ esp_err_t OtaRelay::requestGd32AppEnterBootloader() } +/** + * @brief 阶段1 - 发送握手请求并等待确认. + * @return 操作结果码. + */ ota_result_t OtaRelay::sendHandshakeReq() { DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ==="); diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index 83c6343..45e719f 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -57,9 +57,21 @@ typedef enum { class OtaRelay { public: + /** + * @brief 获取OtaRelay单例引用. + * @return OtaRelay单例引用. + */ static OtaRelay& instance(); + /** + * @brief OTA中继FreeRTOS任务入口. + * @param pvParameters FreeRTOS任务参数. + */ static void taskEntry(void *pvParameters); + /** + * @brief 从Flash向GD32转发固件(主流程). + * @return 操作结果码. + */ ota_result_t forwardFirmware(); /* ---- 线程安全的状态访问接口 ---- @@ -67,27 +79,117 @@ public: * 状态机内部(OTA 任务)可直接写 static 成员, * 跨任务读/写必须走这些 API。 */ + /** + * @brief 获取当前中继状态. + * @return 当前中继状态. + */ static relay_state_t getState(); + /** + * @brief 获取中继当前序号. + * @return 当前序号值. + */ static uint32_t getRelaySeq(); + /** + * @brief 获取中继总包数. + * @return 总包数. + */ static uint32_t getRelayTotalPackets(); + /** + * @brief 获取GD32 APP存活标志位. + * @return true 存活,false 未存活. + */ static bool getGd32AppAlive(); + /** + * @brief 获取GD32固件版本. + * @param out 输出参数,版本结构体指针. + */ static void getGd32Version(gd32_version_t *out); + /** + * @brief 设置中继状态. + * @param s 目标状态值. + */ static void setState(relay_state_t s); + /** + * @brief 设置中继当前序号. + * @param seq 序号值. + */ static void setRelaySeq(uint32_t seq); + /** + * @brief 设置中继总包数. + * @param total 总包数. + */ static void setRelayTotalPackets(uint32_t total); + /** + * @brief 设置GD32 APP存活标志位. + * @param alive 存活状态. + */ static void setGd32AppAlive(bool alive); + /** + * @brief 获取GD32固件版本. + * @param ver 待设置的版本结构体引用. + */ static void setGd32Version(const gd32_version_t &ver); + /** + * @brief 重置状态机. + */ void reset(); + /** + * @brief Flash写入接口(支持非4字节对齐写入). + * @param offset 分区内写入偏移. + * @param data 待写入的数据指针. + * @param len 数据长度(字节). + * @return ESP_OK 成功,其他失败. + */ static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len); + /** + * @brief 从OTA缓存分区读取数据块. + * @param offset 分区内读取偏移. + * @param data 读取缓冲区指针. + * @param len 读取长度(字节). + * @return ESP_OK 成功,其他失败. + */ static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len); + /** + * @brief 从Flash加载元数据. + * @param meta 输出参数,加载后的元数据结构体指针. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t metaLoad(ota_meta_t *meta); + /** + * @brief 保存元数据到Flash. + * @param meta 待保存的元数据结构体指针. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t metaSave(const ota_meta_t *meta); + /** + * @brief 传输过程中更新元数据. + * @param stored_size 已存储的固件大小(字节). + * @param last_seq 最后成功的分块序号. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq); + /** + * @brief OTA完成或中止时清除元数据. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t metaClear(); + /** + * @brief 计算gd32_ota分区中已存储固件的SHA-256. + * @param out_sha256 输出32字节SHA-256摘要. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t computeFlashSha256(uint8_t out_sha256[32]); + /** + * @brief 固件接收完毕后计算SHA-256并写入元数据. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t metaFinalizeChecksum(); + /** + * @brief 校验固件完整性(SHA-256). + * @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败. + */ static esp_err_t metaVerifyChecksum(); // 公开状态(状态机内部直接访问;跨任务请用 getter/setter) @@ -101,17 +203,31 @@ public: // 线程安全锁 static SemaphoreHandle_t s_stateMutex; + /** + * @brief 初始化状态互斥锁. + */ static void initMutex(); static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); class StateLockGuard { public: + /** + * @brief 构造时获取互斥锁. + * @param timeout 等待超时(时钟节拍),默认50ms. + */ explicit StateLockGuard(TickType_t timeout = kLockTimeout) { m_locked = (s_stateMutex && xSemaphoreTake(s_stateMutex, timeout) == pdTRUE); } + /** + * @brief 析构时释放互斥锁. + */ ~StateLockGuard() { if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); } } + /** + * @brief 检查是否成功获取锁. + * @return true 已锁定,false 未锁定. + */ bool isLocked() const { return m_locked; } StateLockGuard(const StateLockGuard&) = delete; StateLockGuard& operator=(const StateLockGuard&) = delete; @@ -129,28 +245,137 @@ public: static constexpr size_t GD32_UID_LEN = 12; static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36; + /** + * @brief 请求GD32 APP进入Bootloader. + * @return 操作结果码. + */ esp_err_t requestGd32AppEnterBootloader(); + /** + * @brief 发送握手请求. + * @return 操作结果码. + */ ota_result_t sendHandshakeReq(); + /** + * @brief 发送随机数并与GD32相互认证. + * @return 操作结果码. + */ ota_result_t sendNonceAndAuthenticate(); + /** + * @brief 向GD32发送固件总大小. + * @param size 固件总字节数. + * @return 操作结果码. + */ ota_result_t sendFirmwareSize(uint32_t size); + /** + * @brief 从Flash向GD32转发固件包. + * @param deadline_tick 截止时间戳(可选),0表示无超时. + * @return 操作结果码. + */ ota_result_t relayPacketsFromFlash(TickType_t deadline_tick = 0); + /** + * @brief 完成OTA并发送结束指令. + * @return 操作结果码. + */ ota_result_t finishOta(); + /** + * @brief 使用AES-128-ECB加密单个16字节块. + * @param key 16字节密钥. + * @param in 16字节输入数据. + * @param out 16字节输出数据. + * @return 0 成功,-1 失败. + */ static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out); + /** + * @brief 记录响应载荷日志. + * @param stage 阶段名称字符串. + * @param resp_type 响应类型. + * @param payload 载荷指针. + * @param len 载荷长度. + */ static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len); + /** + * @brief 循环等待直到响应不再是WAIT状态. + * @param stage 阶段名称字符串. + * @param resp_type 响应类型输出. + * @param resp_payload 响应载荷缓冲区. + * @param resp_max 缓冲区最大长度. + * @param resp_len 实际响应长度输出. + * @param timeout_ms 单次轮询超时毫秒. + * @param max_wait_rounds 最大WAIT轮次. + * @return ESP_OK 成功,其他失败. + */ 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 判断是否为带序号的结构化ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @param expected_seq 期望的序号. + * @return true 匹配成功,false 不匹配. + */ static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq); + /** + * @brief 解析结构化错误帧. + * @param payload 载荷指针. + * @param len 载荷长度. + * @param seq_out 输出错误序号. + * @param err_out 输出错误码. + * @return true 解析成功,false 解析失败. + */ static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out); + /** + * @brief 判断是否为握手回显ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是握手回显ACK,false 不是. + */ static bool isHandshakeEchoAck(const uint8_t *payload, size_t len); + /** + * @brief 判断是否为挑战应答ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是挑战应答ACK,false 不是. + */ static bool isChallengePayloadAck(const uint8_t *payload, size_t len); + /** + * @brief 判断是否为普通确认ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是确认ACK,false 不是. + */ static bool isConfirmPlainAck(const uint8_t *payload, size_t len); + /** + * @brief 判断是否为大小序号0的ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是大小序号0ACK,false 不是. + */ static bool isSizeSeq0Ack(const uint8_t *payload, size_t len); + /** + * @brief 判断是否为普通的结束ACK. + * @param payload 载荷指针. + * @param len 载荷长度. + * @return true 是结束ACK,false 不是. + */ static bool isFinishPlainAck(const uint8_t *payload, size_t len); + /** + * @brief 轮询直到收到预期的ACK形状. + * @param stage 阶段名称字符串. + * @param resp_type 响应类型输出. + * @param resp_payload 响应载荷缓冲区. + * @param resp_max 响应缓冲区最大长度. + * @param resp_len 实际响应长度输出. + * @param poll_timeout_ms 轮询超时毫秒. + * @param wait_rounds WAIT状态最大轮询次数. + * @param maxExtraPolls 额外轮询次数. + * @param matcher 匹配回调函数. + * @return ESP_OK 成功,其他失败. + */ static esp_err_t pollUntilExpectedAckShape(const char *stage, uint8_t *resp_type, uint8_t *resp_payload, @@ -160,9 +385,30 @@ public: uint32_t wait_rounds, uint32_t maxExtraPolls, bool (*matcher)(const uint8_t *, size_t)); + /** + * @brief 解析并验证挑战应答载荷. + * @param resp_payload 响应载荷指针. + * @param resp_len 响应长度. + * @param out_k_session 输出会话密钥. + * @param out_uid 输出GD32 UID. + * @return true 验证通过,false 验证失败. + */ static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len, uint8_t *out_k_session, uint8_t *out_uid); + /** + * @brief 使用AES-128-ECB加密48字节数据. + * @param key 16字节密钥. + * @param in 48字节输入数据. + * @param out 48字节输出数据. + * @return true 加密成功,false 加密失败. + */ static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out); + /** + * @brief 使用密钥计算48字节数据的CRC32. + * @param key 16字节密钥. + * @param data48 48字节输入数据. + * @param output 输出4字节CRC32结果. + */ static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output); private: diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index 36647a8..8d6f885 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -71,6 +71,10 @@ static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes); static void sendFinishEvent(uint8_t stage, uint32_t value); // ============================================================ +/** + * @brief 获取 BleOta 单例实例。 + * @return BleOta 类引用。 + */ BleOta& BleOta::instance() { static BleOta inst; @@ -103,6 +107,10 @@ static void ble_esp32_restart_task(void *pvParameters) // 不会返回 } +/** + * @brief 异步完成 ESP32 OTA 收尾并触发重启。 + * @param pvParameters 前端 SHA-256 十六进制字符串(堆分配,任务负责释放)。 + */ static void ble_esp32_finish_and_reboot_task(void *pvParameters) { // pvParameters: heap-allocated char[65] frontend SHA hex string (or nullptr) diff --git a/main/server/ble_ota.h b/main/server/ble_ota.h index 6013e86..863bfda 100644 --- a/main/server/ble_ota.h +++ b/main/server/ble_ota.h @@ -14,12 +14,37 @@ class BleOta { public: + /** + * @brief 获取 BleOta 单例实例。 + * @return BleOta 类引用。 + */ static BleOta& instance(); + /** + * @brief BLE OTA FreeRTOS 任务入口。 + * @param pvParameters 任务参数,未使用。 + */ static void taskEntry(void *pvParameters); + /** + * @brief 启动 BLE GATT 服务器并注册 OTA 服务。 + * @return ESP_OK 成功,其他失败。 + */ esp_err_t serverStart(); + /** + * @brief 通过 BLE Notify 推送响应数据。 + * @param data 待推送的数据指针。 + * @param len 数据长度。 + * @return ESP_OK 成功,其他失败。 + */ esp_err_t notifyResponse(const uint8_t *data, size_t len); + /** + * @brief 检查 BLE 客户端连接状态。 + * @return 已连接返回 true,未连接返回 false。 + */ bool isConnected(); + /** + * @brief 触发中继模块向 GD32 转发固件。 + */ void signalRelay(); // 以下状态为 public,供 C 回调函数访问 diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 73c5277..025d4d2 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -34,6 +34,10 @@ // ============================================================ +/** + * @brief 获取 WifiOta 单例实例(静态局部变量实现)。 + * @return WifiOta 单例引用。 + */ WifiOta& WifiOta::instance() { static WifiOta inst; @@ -42,6 +46,12 @@ WifiOta& WifiOta::instance() static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U; +/** + * @brief 按指定对齐值向上取整。 + * @param value 待对齐的值。 + * @param alignment 对齐字节数。 + * @return 对齐后的值。 + */ static uint32_t align_up_u32(uint32_t value, uint32_t alignment) { return ((value + alignment - 1U) / alignment) * alignment; diff --git a/main/server/wifi_ota.h b/main/server/wifi_ota.h index 2c6a77d..0b4e5db 100644 --- a/main/server/wifi_ota.h +++ b/main/server/wifi_ota.h @@ -21,31 +21,71 @@ typedef enum { class WifiOta { public: + /** + * @brief 获取 WifiOta 单例实例。 + * @return WifiOta 单例引用。 + */ static WifiOta& instance(); + /** + * @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。 + * @return true 表示 STA 初始化中,false 需回退至 AP 模式。 + */ bool earlyInit(); + /** + * @brief WiFi OTA FreeRTOS 任务入口,提供 HTTP 上传和中继触发。 + * @param pvParameters FreeRTOS 任务参数。 + */ static void taskEntry(void *pvParameters); + /** + * @brief 通知 OTA 中继开始向 GD32 转发固件。 + */ void signalRelay(); + /** + * @brief 获取当前 WiFi 工作模式。 + * @return 当前 WiFi 模式(AP 或 STA)。 + */ static ota_wifi_mode_t getMode(); + /** + * @brief 获取当前 WiFi 信号强度。 + * @return 信号强度值(dBm)。 + */ static int8_t getRssi(); + /** + * @brief 判断 STA 是否已连接到上级 WiFi。 + * @return true 已连接,false 未连接。 + */ static bool isConnected(); // ── WiFi 连接状态(WiFi 事件回调 / HTTP / reconnect task 共享,需 mutex 保护)── + /** @brief 获取 STA 连接状态。 */ bool getStaConnected() const { return m_staConnected.load(); } + /** @brief 设置 STA 连接状态。 */ void setStaConnected(bool v) { m_staConnected.store(v); } + /** @brief 获取 STA 重试次数。 */ int getStaRetryCount() const { return m_staRetryCount.load(); } + /** @brief 设置 STA 重试次数。 */ void setStaRetryCount(int v) { m_staRetryCount.store(v); } + /** @brief 获取 STA 重试是否已耗尽。 */ bool getStaRetryExhausted() const { return m_staRetryExhausted.load(); } + /** @brief 设置 STA 重试耗尽标志。 */ void setStaRetryExhausted(bool v) { m_staRetryExhausted.store(v); } + /** @brief 获取 GD32 固件存储完成标志。 */ bool getGd32StorageDone() const { return m_gd32StorageDone.load(); } + /** @brief 设置 GD32 固件存储完成标志。 */ void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); } + /** @brief 获取中继触发标志。 */ bool getRelayTriggered() const { return m_relayTriggered.load(); } + /** @brief 设置中继触发标志。 */ void setRelayTriggered(bool v) { m_relayTriggered.store(v); } // ── 状态互斥锁(保护复合读-改-写操作)── + /** @brief 获取状态互斥锁句柄。 */ SemaphoreHandle_t getStateMutex() const { return m_stateMutex; } + /** @brief 锁定状态互斥锁。 */ void stateLock() { if (m_stateMutex) xSemaphoreTake(m_stateMutex, portMAX_DELAY); } + /** @brief 解锁状态互斥锁。 */ void stateUnlock() { if (m_stateMutex) xSemaphoreGive(m_stateMutex); } // ── 初始化/运行时状态(仅 OTA 任务访问,无需额外锁)── @@ -66,13 +106,44 @@ private: std::atomic m_relayTriggered{false}; SemaphoreHandle_t m_stateMutex = NULL; + /** + * @brief 初始化 SoftAP 模式,供局域网 OTA 使用。 + */ void initAp(); + /** + * @brief 初始化 STA 模式并连接预配置路由器。 + * @return ESP_OK 成功,其他失败。 + */ esp_err_t initSta(); + /** + * @brief 设置 STA 静态 IP、网关和子网掩码。 + */ void setStaticIp(); + /** + * @brief WiFi/IP 事件回调,处理连接、断开和 IP 获取。 + * @param arg 回调用户参数。 + * @param event_base 事件基类。 + * @param event_id 具体事件 ID。 + * @param event_data 事件附带数据。 + */ static void wifiEventHandler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + /** + * @brief STA 后台重连任务,处理退避延迟和重连。 + * @param pvParameters FreeRTOS 任务参数。 + */ static void reconnectTask(void *pvParameters); + /** + * @brief 启动 HTTP OTA 服务器,注册各接口路由。 + * @return ESP_OK 成功,其他失败。 + */ esp_err_t httpServerStart(); + /** + * @brief 等待 STA 连接成功并获取 IP 地址。 + * @param ip_str 输出缓冲区,存放 IP 字符串。 + * @param ip_len 缓冲区长度。 + * @return ESP_OK 连接成功,ESP_ERR_TIMEOUT 超时。 + */ esp_err_t waitStaConnected(char *ip_str, size_t ip_len); }; diff --git a/main/transport/protocol.cpp b/main/transport/protocol.cpp index 85b9079..c5453e8 100644 --- a/main/transport/protocol.cpp +++ b/main/transport/protocol.cpp @@ -20,12 +20,19 @@ // ============================================================ // Protocol 单例 // ============================================================ +/** + * @brief 获取 Protocol 单例实例。 + * @return Protocol 单例引用。 + */ Protocol& Protocol::instance() { static Protocol inst; return inst; } +/** + * @brief 初始化协议层,打印初始化信息并设置初始化标志。 + */ void Protocol::init() { if (m_initialized) return; diff --git a/main/transport/protocol.h b/main/transport/protocol.h index 0bbc6d2..c1a7c13 100644 --- a/main/transport/protocol.h +++ b/main/transport/protocol.h @@ -37,13 +37,37 @@ typedef struct { */ class Protocol { public: + /** + * @brief 获取 Protocol 单例实例。 + * @return Protocol 单例引用。 + */ static Protocol& instance(); + /** + * @brief 初始化协议层。 + */ void init(); + /** + * @brief 生成基于时间戳、地址和随机种子的扰码 nonce。 + * @param timestamp 时间戳。 + * @param mem_addr 内存地址。 + * @param rand_seed 随机种子。 + * @param nonce_out 输出的 16 字节 nonce 缓冲区。 + */ static void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr, uint32_t rand_seed, uint8_t *nonce_out); + /** + * @brief 会话密钥初始化,将密钥拷贝到会话上下文。 + * @param sess 会话上下文指针。 + * @param k_session 会话密钥数据。 + * @return 成功返回 0,参数无效返回 -1。 + */ static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session); + /** + * @brief 清除会话上下文的密钥和 IV 数据。 + * @param sess 会话上下文指针。 + */ static void sessionKeyClear(session_ctx_t *sess); private: diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index cbd1344..f3f0613 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -23,6 +23,10 @@ static constexpr size_t kSpiTransferMaxBytes = 256U; // ============================================================ // 单例 // ============================================================ +/** + * @brief 获取 SpiMaster 单例实例(单例模式)。 + * @return SpiMaster 单例引用。 + */ SpiMaster& SpiMaster::instance() { static SpiMaster inst; @@ -32,6 +36,15 @@ SpiMaster& SpiMaster::instance() // ============================================================ // 内部 SPI 传输 // ============================================================ +/** + * @brief 执行底层 SPI 时钟同步传输。 + * @param tx_buf 发送数据缓冲区,可为 NULL。 + * @param tx_len 发送数据长度。 + * @param rx_buf 接收数据缓冲区,可为 NULL。 + * @param rx_len 接收数据长度。 + * @param timeout_ms 超时时间(毫秒),0 表示无限等待。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, uint8_t *rx_buf, size_t rx_len, uint32_t timeout_ms) @@ -100,6 +113,16 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, return ESP_OK; } +/** + * @brief 执行 SPI 请求-响应组合传输。 + * @param cmd 命令字节(0 表示无命令)。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param rx_buf 接收数据缓冲区。 + * @param rx_len 期望接收的数据长度。 + * @param timeout_ms 超时时间(毫秒),0 表示无限等待。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len, uint8_t *rx_buf, size_t rx_len, @@ -146,6 +169,12 @@ esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd, // ============================================================ // OTA 帧工具 // ============================================================ +/** + * @brief 计算 OTA 帧的 8 位 CRC 校验和。 + * @param data 数据缓冲区。 + * @param len 数据长度。 + * @return 计算得到的 CRC 值。 + */ uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len) { uint8_t crc = 0x00; @@ -155,6 +184,15 @@ uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len) return crc; } +/** + * @brief 构建 OTA 协议帧。 + * @param type 帧类型。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param out 输出帧缓冲区。 + * @param out_len 输出实际帧长度。 + * @return true 表示构建成功,false 表示失败。 + */ bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len) { @@ -173,6 +211,15 @@ bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t paylo return true; } +/** + * @brief 解析 OTA 协议帧。 + * @param frame 帧数据缓冲区。 + * @param frame_len 帧数据长度。 + * @param type 输出解析得到的帧类型。 + * @param payload 输出指向负载数据的指针。 + * @param payload_len 输出负载数据长度。 + * @return true 表示解析成功,false 表示失败。 + */ bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len, uint8_t *type, const uint8_t **payload, size_t *payload_len) { @@ -192,6 +239,16 @@ bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len, return true; } +/** + * @brief 在数据缓冲区中搜索并解析 OTA 帧。 + * @param buf 数据缓冲区。 + * @param buf_len 数据缓冲区长度。 + * @param type 输出帧类型。 + * @param payload 输出指向负载数据的指针。 + * @param payload_len 输出负载数据长度。 + * @param frame_offset 输出帧在缓冲区中的偏移位置。 + * @return true 表示找到并成功解析,false 表示未找到。 + */ 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) @@ -222,6 +279,10 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, // ============================================================ // 初始化 // ============================================================ +/** + * @brief 初始化 SPI2 总线与目标从设备。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::init() { LockGuard lock(m_mutex); @@ -274,6 +335,16 @@ esp_err_t SpiMaster::init() // ============================================================ // 公开 SPI 传输接口 // ============================================================ +/** + * @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param recv_buf 接收数据缓冲区。 + * @param recv_len 接收数据长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload, size_t payload_len, uint8_t *recv_buf, size_t recv_len, uint32_t timeout_ms) @@ -282,12 +353,27 @@ esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload, return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms); } +/** + * @brief 仅发送 SPI 请求数据,不关心返回内容。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param len 负载数据长度。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len) { LockGuard lock(m_mutex); return transferRequestResponse(cmd, payload, len, NULL, 0, 0U); } +/** + * @brief 仅接收 SPI 数据,必要时先发送命令字节。 + * @param cmd 命令字节。 + * @param recv_buf 接收数据缓冲区。 + * @param len 接收数据长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms) { LockGuard lock(m_mutex); @@ -297,6 +383,18 @@ esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t t // ============================================================ // SPI OTA 协议 // ============================================================ +/** + * @brief 按 GD32 SPI OTA 最小协议执行一次请求加响应读取。 + * @param type 请求帧类型。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param resp_type 输出响应类型。 + * @param resp_payload 输出响应负载缓冲区。 + * @param resp_max 响应负载缓冲区最大长度。 + * @param resp_len 输出实际响应长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ 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) @@ -319,6 +417,15 @@ esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t pa return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms); } +/** + * @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 + * @param resp_type 输出响应类型。 + * @param resp_payload 输出响应负载缓冲区。 + * @param resp_max 响应负载缓冲区最大长度。 + * @param resp_len 输出实际响应长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ 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) { @@ -326,6 +433,15 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload, return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms); } +/** + * @brief 无锁版本的 OTA 响应轮询实现。 + * @param resp_type 输出响应类型。 + * @param resp_payload 输出响应负载缓冲区。 + * @param resp_max 响应负载缓冲区最大长度。 + * @param resp_len 输出实际响应长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, uint32_t timeout_ms) { @@ -390,6 +506,10 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p return ESP_ERR_TIMEOUT; } +/** + * @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::appRequestBootloader() { LockGuard lock(m_mutex); @@ -429,6 +549,13 @@ esp_err_t SpiMaster::appRequestBootloader() return ESP_ERR_TIMEOUT; } +/** + * @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 + * @param timeout_ms 超时时间(毫秒)。 + * @param uptime_sec 输出设备运行时间(秒)。 + * @param version 输出 GD32 版本信息(可选)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version) { LockGuard lock(m_mutex); @@ -546,6 +673,16 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver return ESP_ERR_TIMEOUT; } +/** + * @brief 执行带重试的 SPI 同步传输。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param recv_buf 接收数据缓冲区。 + * @param recv_len 接收数据长度。 + * @param max_retries 最大重试次数。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ 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) @@ -579,6 +716,17 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload, // ============================================================ // CANFD 接口 // ============================================================ +/** + * @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。 + * @param channel CAN 通道编号(0~2)。 + * @param ext_id 扩展帧 ID(29 位)。 + * @param data CAN 数据负载。 + * @param dlc 数据长度。 + * @param recv_buf 接收数据缓冲区。 + * @param recv_max 接收缓冲区最大长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return 成功时返回接收数据长度,失败返回 -1。 + */ 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, @@ -608,6 +756,14 @@ int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id, return (int)recv_max; } +/** + * @brief 发送一帧无需响应的 CANFD 数据。 + * @param channel CAN 通道编号(0~2)。 + * @param ext_id 扩展帧 ID(29 位)。 + * @param data CAN 数据负载。 + * @param dlc 数据长度。 + * @return 成功返回 0,失败返回 -1。 + */ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id, const uint8_t *data, uint8_t dlc) { @@ -638,6 +794,16 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id, // ============================================================ // 无锁内部版本实现 // ============================================================ +/** + * @brief 无锁版本的同步 SPI 传输实现。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param recv_buf 接收数据缓冲区。 + * @param recv_len 接收数据长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::syncTransferUnlocked(uint8_t cmd, const uint8_t *payload, size_t payload_len, uint8_t *recv_buf, size_t recv_len, uint32_t timeout_ms) @@ -645,6 +811,13 @@ esp_err_t SpiMaster::syncTransferUnlocked(uint8_t cmd, const uint8_t *payload, return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms); } +/** + * @brief 无锁版本的 SPI 发送实现。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param len 负载数据长度。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t SpiMaster::sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len) { return transferRequestResponse(cmd, payload, len, NULL, 0, 0U); diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index ed0e2ab..df92843 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -74,6 +74,10 @@ typedef struct { */ class SpiMaster { public: + /** + * @brief 获取 SpiMaster 单例实例。 + * @return SpiMaster 单例引用。 + */ static SpiMaster& instance(); /** @brief 初始化 SPI2 总线与目标从设备。 */ @@ -136,9 +140,21 @@ public: spi_device_handle_t deviceHandle() const { return m_spiDevice; } private: + /** + * @brief 私有默认构造函数。 + */ SpiMaster() = default; + /** + * @brief 私有析构函数。 + */ ~SpiMaster() = default; + /** + * @brief 禁止复制构造。 + */ SpiMaster(const SpiMaster&) = delete; + /** + * @brief 禁止复制赋值。 + */ SpiMaster& operator=(const SpiMaster&) = delete; spi_device_handle_t m_spiDevice = NULL; @@ -147,40 +163,132 @@ private: // RAII 锁守卫 class LockGuard { public: + /** + * @brief 构造加锁守卫,获取互斥锁。 + * @param mutex 互斥锁句柄。 + */ explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) { if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); } } + /** + * @brief 析构解锁守卫,释放互斥锁。 + */ ~LockGuard() { if (m_lock) { xSemaphoreGive(m_lock); } } + /** + * @brief 禁止复制构造。 + */ LockGuard(const LockGuard&) = delete; + /** + * @brief 禁止复制赋值。 + */ LockGuard& operator=(const LockGuard&) = delete; private: SemaphoreHandle_t m_lock; }; // 内部 SPI 传输 + /** + * @brief 执行底层 SPI 时钟同步传输。 + * @param tx_buf 发送数据缓冲区。 + * @param tx_len 发送数据长度。 + * @param rx_buf 接收数据缓冲区。 + * @param rx_len 接收数据长度。 + * @param timeout_ms 超时时间(毫秒),0 表示无限等待。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len, uint8_t *rx_buf, size_t rx_len, uint32_t timeout_ms = 0U); + /** + * @brief 执行 SPI 请求-响应组合传输。 + * @param cmd 命令字节(0 表示无命令)。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param rx_buf 接收数据缓冲区。 + * @param rx_len 期望接收的数据长度。 + * @param timeout_ms 超时时间(毫秒),0 表示无限等待。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len, uint8_t *rx_buf, size_t rx_len, uint32_t timeout_ms = 0U); // 无锁内部版本(供已持锁的公共方法调用,避免递归死锁) + /** + * @brief 无锁版本的同步 SPI 传输(调用者须已持有锁)。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param recv_buf 接收数据缓冲区。 + * @param recv_len 接收数据长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t syncTransferUnlocked(uint8_t cmd, const uint8_t *payload, size_t payload_len, uint8_t *recv_buf, size_t recv_len, uint32_t timeout_ms); + /** + * @brief 无锁版本的 SPI 发送(调用者须已持有锁)。 + * @param cmd 命令字节。 + * @param payload 负载数据缓冲区。 + * @param len 负载数据长度。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len); + /** + * @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。 + * @param resp_type 输出响应类型。 + * @param resp_payload 输出响应负载缓冲区。 + * @param resp_max 响应负载缓冲区最大长度。 + * @param resp_len 输出实际响应长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return ESP_OK 表示成功,否则返回错误码。 + */ esp_err_t otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, uint32_t timeout_ms); // OTA 帧工具(静态,无状态) + /** + * @brief 计算 OTA 帧的 8 位 CRC 校验和。 + * @param data 数据缓冲区。 + * @param len 数据长度。 + * @return 计算得到的 CRC 值。 + */ static uint8_t otaCrc8(const uint8_t *data, size_t len); + /** + * @brief 构建 OTA 协议帧。 + * @param type 帧类型。 + * @param payload 负载数据缓冲区。 + * @param payload_len 负载数据长度。 + * @param out 输出帧缓冲区。 + * @param out_len 输出实际帧长度。 + * @return true 表示构建成功,false 表示失败。 + */ static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len); + /** + * @brief 解析 OTA 协议帧。 + * @param frame 帧数据缓冲区。 + * @param frame_len 帧数据长度。 + * @param type 输出解析得到的帧类型。 + * @param payload 输出指向负载数据的指针。 + * @param payload_len 输出负载数据长度。 + * @return true 表示解析成功,false 表示失败。 + */ static bool otaParseFrame(const uint8_t *frame, size_t frame_len, uint8_t *type, const uint8_t **payload, size_t *payload_len); + /** + * @brief 在数据缓冲区中搜索并解析 OTA 帧。 + * @param buf 数据缓冲区。 + * @param buf_len 数据缓冲区长度。 + * @param type 输出帧类型。 + * @param payload 输出指向负载数据的指针。 + * @param payload_len 输出负载数据长度。 + * @param frame_offset 输出帧在缓冲区中的偏移位置。 + * @return true 表示找到并成功解析,false 表示未找到。 + */ 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);