refactor(arch): 精简架构去除样板代码并修复BLE OTA虚假ACK问题

- 移除OtaCoordinator中重复的状态查询锁操作,改为单次取值后比较
- 将GD32存储wrapper函数改为inline实现并移入头文件,删除对应的cpp实现
- HTTP路由注册改为路由表+循环注册,消除复制粘贴的URI注册代码
- OtaRelay事件分发中提取通用拒绝逻辑到commonReject helper函数
- SpiMaster中折叠加锁/无锁镜像函数,通过内部Impl实现统一路径
- HealthMonitor使用RAII LockGuard替代重复的mutex操作代码
- 修复BLE OTA中ESP32路径的虚假ACK问题,窗口边界ACK仅在实际写入时触发
- 修复BLE OTA中GD32路径的重复包ACK问题,重复包静默跳过不发送ACK
- 调整发送端首轮ACK超时从3000ms提升至5000ms,适配设备端flash写入速度
- 清理临时文件和无用的配置宏定义
- 在main.cpp中添加GD32版本探测逻辑
This commit is contained in:
小龙虾
2026-07-15 02:23:25 +08:00
parent 164855a36c
commit 5a89cec34b
25 changed files with 218 additions and 464 deletions
+43 -1
View File
@@ -1,4 +1,4 @@
/*
/*
* ESP32-C3FH4 OTA 中继 - 程序入口
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
*
@@ -170,6 +170,48 @@ extern "C" void app_main(void)
DEBUG_INFO("MAIN", "========================================");
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
// ★ 启动最后步骤:读取 GD32 APP 版本号
// 参考 OtaRelay::doWaitForAppAlive 刷写后读取逻辑:
// 先 hello() 建立 SPI v2 会话,再 probeGd32AppOnce 读取版本
// GD32 APP 完整初始化需 5-6 秒,使用 20 次重试 × 300ms 窗口
{
DEBUG_INFO("MAIN", "启动 GD32 版本探测...");
gd32_version_t gd32_ver = {};
bool session_established = false;
bool gd32_ok = false;
for (uint32_t attempt = 0; attempt < 20; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(300));
if (!session_established) {
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
if (hr == ESP_OK) {
session_established = true;
} else {
DEBUG_INFO("MAIN", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
continue;
}
}
esp_err_t gd32_ret = OtaCoordinator::instance().probeGd32AppOnce(&gd32_ver, nullptr);
if (gd32_ret == ESP_OK) {
DEBUG_INFO("MAIN", "GD32 APP: v%d.%d.%d.%d (BOOT v%d.%d.%d.%d)",
gd32_ver.app_major, gd32_ver.app_minor,
gd32_ver.app_patch, gd32_ver.app_build,
gd32_ver.boot_major, gd32_ver.boot_minor,
gd32_ver.boot_patch, gd32_ver.boot_build);
gd32_ok = true;
break;
}
DEBUG_INFO("MAIN", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
}
if (!gd32_ok) {
DEBUG_WARN("MAIN", "GD32 APP 版本读取失败(OtaRelay 任务将重试)");
}
}
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
DEBUG_LOG_IP, DEBUG_LOG_PORT);
DEBUG_INFO("MAIN", "========================================");
-17
View File
@@ -32,15 +32,6 @@ inline constexpr const char* WIFI_STA_PASSWORD = "q13362875622"; ///< STA 模
inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000; ///< STA 连接超时(毫秒)
inline constexpr uint8_t WIFI_STA_MAX_RETRY = 5; // 最大重连次数,超过后切换 AP 模式
// STA 静态 IP
inline constexpr uint8_t WIFI_STA_USE_STATIC_IP = 0; // 先关闭静态 IP,使用 DHCP 排查连接问题
// 静态 IP 配置(始终定义,由 WIFI_STA_USE_STATIC_IP 在运行时决定是否使用)
inline constexpr const char* WIFI_STA_STATIC_IP = "192.168.31.32";
inline constexpr const char* WIFI_STA_STATIC_GW = "192.168.31.1";
inline constexpr const char* WIFI_STA_STATIC_NETMASK = "255.255.255.0";
inline constexpr const char* WIFI_STA_STATIC_DNS = "192.168.31.1";
// ============================================================
// WiFi AP 模式配置(设备当热点)
// ============================================================
@@ -85,7 +76,6 @@ inline constexpr size_t HEAP_WARNING_MIN = 10240; ///< 堆内
// ============================================================
// 调试配置(UDP 日志)
// ============================================================
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; ///< UDP 调试日志开关(1=启用)
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; ///< UDP 日志目标 IP
inline constexpr uint16_t DEBUG_LOG_PORT = 6666; ///< UDP 日志目标端口
@@ -107,7 +97,6 @@ inline constexpr uint8_t BLE_OTA_CTRL_ABORT_OTA = 0xFB; ///< BLE 控
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; ///< BLE 数据通道:GD32 固件包标识
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; ///< BLE 数据通道:ESP32 固件包标识
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; ///< BLE 单包数据载荷大小(字节)
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; ///< BLE 包头+序号+校验开销(字节)
// Reorder buffer 窗口大小(吸收 BLE 窗口内乱序到达的包,须 >= BLE_OTA_ACK_WINDOW)
inline constexpr size_t BLE_OTA_REORDER_WINDOW = 32;
@@ -123,12 +112,6 @@ inline constexpr uint8_t MASTER_KEY[] = {
// SCRAMBLE_CONST 已移至 Protocol 类,运行时由硬件 RNG 随机生成
inline constexpr size_t SCRAMBLE_CONST_LEN = 16;
// GD32_CHIP_UID 仅保留为默认占位,运行时以握手包解出的芯片地址 UID 为准。
inline constexpr uint32_t DEVICE_SERIAL_ADDR_LSB = 0x1FFFF7E8U; ///< GD32 设备序列号低字 Flash 地址
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MID = 0x1FFFF7ECU; ///< GD32 设备序列号中字 Flash 地址
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MSB = 0x1FFFF7F0U; ///< GD32 设备序列号高字 Flash 地址
inline constexpr uint8_t GD32_CHIP_UID[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
};
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 44
#define FW_VERSION_STRING "0.3.44"
#define FW_VERSION_PATCH 45
#define FW_VERSION_STRING "0.3.45"
// 编译时间
#define BUILD_TIME "2026-07-15 00:28:19"
#define BUILD_TIME "2026-07-15 02:19:05"
// Git 信息
#define GIT_HASH "adb3386"
#define GIT_HASH "164855a"
+1 -3
View File
@@ -32,12 +32,10 @@
// 日志级别
typedef enum {
DEBUG_LOG_NONE = 0,
DEBUG_LOG_ERR = 1, // 错误
DEBUG_LOG_WARN = 2, // 警告
DEBUG_LOG_INFO = 3, // 信息
DEBUG_LOG_DBG = 4, // 调试
DEBUG_LOG_ALL = 5 // 全部
DEBUG_LOG_DBG = 4 // 调试
} debug_log_level_t;
/**
-15
View File
@@ -62,21 +62,6 @@ void HealthMonitor::init()
}
}
/**
* @brief 反初始化:停止并删除温度监控定时器,回收 esp_timer 资源。
* 当前 HealthMonitor 任务为无限循环,无显式删除路径,
* 本方法预留给未来停用场景(如安全关机后停止监控)使用。
*/
void HealthMonitor::deinit()
{
if (m_tempTimer != nullptr) {
esp_timer_stop(m_tempTimer);
esp_timer_delete(m_tempTimer);
m_tempTimer = nullptr;
DEBUG_INFO("HEALTH", "温度监控定时器已停止并删除");
}
}
// ============================================================
// 线程安全的健康状态读写
// ============================================================
+3 -12
View File
@@ -31,7 +31,6 @@ typedef enum {
// WiFi RSSI 阈值(来自 app_config.h)
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING ///< WiFi 信号正常阈值(别名)
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL ///< WiFi 信号弱阈值(别名)
/**
* @brief 系统健康监控类(单例)
@@ -49,13 +48,6 @@ public:
/** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */
void init();
/**
* @brief 反初始化:停止并删除温度监控定时器,回收 esp_timer 资源。
* 在任务删除路径中调用;当前 HealthMonitor 任务为无限循环,
* 无显式删除路径,本方法预留给未来停用场景使用。
*/
void deinit();
/** @brief 健康监控 FreeRTOS 任务入口。 */
static void taskEntry(void *pvParameters);
@@ -103,8 +95,8 @@ private:
SemaphoreHandle_t m_mutex = NULL;
temperature_sensor_handle_t m_tempHandle = NULL;
// 温度监控 esp_timer 句柄存为成员,便于 deinit() 时停止并删除定时器,
// 避免任务被删除后定时器仍在运行、回调继续访问 instance() 成员。
// 温度监控 esp_timer 句柄存为成员,避免任务被删除后定时器仍在运行、
// 回调继续访问 instance() 成员。
esp_timer_handle_t m_tempTimer = nullptr;
health_state_t m_healthState = SYS_NORMAL;
float m_lastTemperature = 0.0f;
@@ -112,8 +104,7 @@ private:
int64_t m_lastWifiRssiMs = 0;
TaskHandle_t m_taskHandle = NULL;
// m_wifiConnected 由 m_mutex 保护(setWifiConnected 写 / checkBeforeOta 读)。
// volatile 保留为历史兼容,实际同步由 LockGuard 提供。
volatile bool m_wifiConnected = false; ///< WiFi 连接状态(由 WiFi 事件推送)
bool m_wifiConnected = false; ///< WiFi 连接状态(由 WiFi 事件推送)
static void tempMonitorCallback(void *arg);
+14 -6
View File
@@ -530,13 +530,21 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
return err;
}
// ★ 已持锁版本的 ESP32 状态切换 helper:设置 m_sysState 并打印一条 INFO 日志。
// 供 notifyEsp32*Complete / notifyEsp32Rebooting 等锁内路径复用,避免重复样板代码。
void OtaCoordinator::setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg)
{
m_sysState = s;
DEBUG_INFO(TAG, "%s", dbg_msg);
}
/** @brief 通知协调器:ESP32 固件接收完毕,状态转为 FINALIZING。 */
void OtaCoordinator::notifyEsp32ReceiveComplete()
{
bool h = lock();
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
m_sysState = OTA_SYS_ESP32_FINALIZING;
DEBUG_INFO(TAG, "ESP32 固件接收完毕, 系统状态 → ESP32_FINALIZING");
setEsp32StateLocked(OTA_SYS_ESP32_FINALIZING,
"ESP32 固件接收完毕, 系统状态 → ESP32_FINALIZING");
}
unlock(h);
}
@@ -546,8 +554,8 @@ void OtaCoordinator::notifyEsp32FinalizeComplete()
{
bool h = lock();
if (h) {
m_sysState = OTA_SYS_ESP32_REBOOTING;
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成, 系统状态 → ESP32_REBOOTING");
setEsp32StateLocked(OTA_SYS_ESP32_REBOOTING,
"ESP32 OTA 收尾完成, 系统状态 → ESP32_REBOOTING");
}
unlock(h);
}
@@ -557,8 +565,8 @@ void OtaCoordinator::notifyEsp32Rebooting()
{
bool h = lock();
if (h) {
m_sysState = OTA_SYS_ESP32_REBOOTING;
DEBUG_INFO(TAG, "ESP32 即将重启, 系统状态 → ESP32_REBOOTING");
setEsp32StateLocked(OTA_SYS_ESP32_REBOOTING,
"ESP32 即将重启, 系统状态 → ESP32_REBOOTING");
}
unlock(h);
}
+4
View File
@@ -476,6 +476,10 @@ private:
// 并把状态机复位为 IDLE。abortCurrent / abortCurrentIfEpoch 共享此实现。
void doAbortLocked();
// ★ 已持锁版本的 ESP32 状态切换 helper:设置 m_sysState 并打印一条 INFO 日志。
// 供 notifyEsp32*Complete / notifyEsp32Rebooting 等锁内路径复用,避免重复样板代码。
void setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg);
// 系统状态
ota_system_state_t m_sysState = OTA_SYS_IDLE;
-16
View File
@@ -1,16 +0,0 @@
/*
* ota_snapshot.h - OTA 状态只读快照(便捷聚合头文件)
*
* 本文件本身不包含结构体定义,仅聚合两个快照来源:
* - RelaySnapshot → 定义在 relay/ota_relay.h(一次锁保护读取 relay 进度)
* - OtaSnapshot → 定义在 ota/ota_coordinator.h(coordinator + meta + relay 快照)
*
* HTTP handler 等消费方只需 #include "ota/ota_snapshot.h" 即可使用两种快照。
*/
#ifndef OTA_SNAPSHOT_H
#define OTA_SNAPSHOT_H
#include "relay/ota_relay.h" // RelaySnapshot
#include "ota/ota_coordinator.h" // OtaSnapshot
#endif // OTA_SNAPSHOT_H
+57 -46
View File
@@ -492,6 +492,18 @@ ota_result_t OtaRelay::forwardFirmware()
return res;
}
// ============================================================
// 会话密钥/上下文清理(reset 与 doFinishAndVerify 共用)
// ============================================================
void OtaRelay::clearSessionCryptoState()
{
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));
}
// ============================================================
// 状态机复位
// ============================================================
@@ -506,11 +518,7 @@ void OtaRelay::reset()
m_transferFlashOffset = 0;
m_transferRetryCount = 0;
m_transferDeadline = 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));
clearSessionCryptoState();
SpiMaster::instance().setSuppressRingService(false);
DEBUG_INFO("OTA", "状态机已复位 → IDLE");
}
@@ -890,10 +898,34 @@ ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts)
return OTA_RESULT_ERR_TIMEOUT;
}
/**
* @brief 统一 SPI 事务 + 轮询 ACK 形状的 helper(CHALLENGE / CONFIRM / SEND_SIZE 共用)。
* @return OTA_RESULT_OK 成功;OTA_RESULT_ERR_TIMEOUT 发送或轮询失败。
*/
ota_result_t OtaRelay::sendAndWaitAckShape(const char *stage, uint8_t cmd,
const uint8_t *payload, size_t payload_len,
uint32_t transact_timeout_ms,
uint32_t poll_timeout_ms, uint32_t wait_rounds, uint32_t max_extra_polls,
uint8_t resp_payload[], size_t resp_max, size_t *resp_len,
bool (*matcher)(const uint8_t *, size_t))
{
uint8_t resp_type = 0;
esp_err_t err = SpiMaster::instance().transact(cmd, payload, payload_len,
&resp_type, resp_payload, resp_max, resp_len, transact_timeout_ms);
logRespPayload(stage, resp_type, resp_payload, *resp_len);
err = pollUntilExpectedAckShape(stage, &resp_type, resp_payload, resp_max, resp_len,
poll_timeout_ms, wait_rounds, max_extra_polls, matcher);
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !matcher(resp_payload, *resp_len)) {
DEBUG_ERR("OTA", "%s 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu",
stage, err, resp_type, *resp_len);
return OTA_RESULT_ERR_TIMEOUT;
}
return OTA_RESULT_OK;
}
/** @brief 协议子步骤:执行挑战认证(发送 nonce、验证响应、派生会话密钥、发送确认)。 */
ota_result_t OtaRelay::doChallengeAndAuth()
{
uint8_t resp_type = 0;
uint8_t resp_payload[64] = {0};
size_t resp_len = 0;
@@ -906,16 +938,12 @@ ota_result_t OtaRelay::doChallengeAndAuth()
DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---");
DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08" PRIX32, timestamp);
esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, m_nonceServer, 16,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000);
logRespPayload("CHALLENGE", resp_type, resp_payload, resp_len);
err = pollUntilExpectedAckShape("CHALLENGE", &resp_type, resp_payload,
sizeof(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);
return OTA_RESULT_ERR_TIMEOUT;
}
ota_result_t res = sendAndWaitAckShape("CHALLENGE", SPI_OTA_CMD_REQ,
m_nonceServer, 16, 5000,
1000, 12, 8,
resp_payload, sizeof(resp_payload), &resp_len,
OtaRelay::isChallengePayloadAck);
if (res != OTA_RESULT_OK) return res;
DEBUG_INFO("OTA", "已收到 GD32 握手数据包");
@@ -936,17 +964,12 @@ ota_result_t OtaRelay::doChallengeAndAuth()
DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY/CONFIRM ---");
resp_len = 0;
err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, encrypted_confirm, 16,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("CONFIRM", resp_type, resp_payload, resp_len);
err = pollUntilExpectedAckShape("CONFIRM", &resp_type, resp_payload,
sizeof(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);
return OTA_RESULT_ERR_TIMEOUT;
}
res = sendAndWaitAckShape("CONFIRM", SPI_OTA_CMD_REQ,
encrypted_confirm, 16, 3000,
1000, 8, 8,
resp_payload, sizeof(resp_payload), &resp_len,
OtaRelay::isConfirmPlainAck);
if (res != OTA_RESULT_OK) return res;
{
char hex[73] = {0};
@@ -964,7 +987,6 @@ ota_result_t OtaRelay::doChallengeAndAuth()
/** @brief 协议子步骤:发送固件总大小给 GD32 并等待 seq=0 的确认。 */
ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
{
uint8_t resp_type = 0;
uint8_t resp_payload[32] = {0};
size_t resp_len = 0;
@@ -976,20 +998,13 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
size_payload[2] = (uint8_t)((size >> 16) & 0xFF);
size_payload[3] = (uint8_t)((size >> 24) & 0xFF);
esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload),
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("SEND_SIZE", resp_type, resp_payload, resp_len);
/* GD32 收到 SIZE 后擦 Flash,先 WAIT 再 ACK(seq=0);进度由前端读 status,不打过程 INFO */
err = pollUntilExpectedAckShape("SEND_SIZE", &resp_type, resp_payload,
sizeof(resp_payload), &resp_len,
/*poll_timeout_ms=*/500,
/*wait_rounds=*/120,
/*maxExtraPolls=*/40,
OtaRelay::isSizeSeq0Ack);
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isSizeSeq0Ack(resp_payload, resp_len)) {
DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
return OTA_RESULT_ERR_TIMEOUT;
}
ota_result_t res = sendAndWaitAckShape("SEND_SIZE", SPI_OTA_CMD_DATA,
size_payload, sizeof(size_payload), 3000,
500, 120, 40,
resp_payload, sizeof(resp_payload), &resp_len,
OtaRelay::isSizeSeq0Ack);
if (res != OTA_RESULT_OK) return res;
SpiMaster::instance().confirmTxSeqAccepted();
DEBUG_INFO("OTA", "大小协商完成: %" PRIu32 " 字节,GD32 已就绪收包", size);
@@ -1355,11 +1370,7 @@ ota_result_t OtaRelay::doFinishAndVerify()
}
// 会话密钥/上下文清理
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));
clearSessionCryptoState();
// 元数据清理安静进行;等待/成功总结由 doWaitForAppAlive 统一输出
Gd32FlashStore::instance().metaClear();
+8
View File
@@ -398,6 +398,14 @@ private:
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 清理会话密钥/上下文/nonce/UID 运行时缓存(reset 与 doFinishAndVerify 共用)。 */
void clearSessionCryptoState();
ota_result_t sendAndWaitAckShape(const char *stage, uint8_t cmd,
const uint8_t *payload, size_t payload_len,
uint32_t transact_timeout_ms,
uint32_t poll_timeout_ms, uint32_t wait_rounds, uint32_t max_extra_polls,
uint8_t resp_payload[], size_t resp_max, size_t *resp_len,
bool (*matcher)(const uint8_t *, size_t));
};
#endif // OTA_RELAY_H
+12 -8
View File
@@ -107,6 +107,16 @@ static void esp32_reorder_free();
static void ble_ota_data_worker_task(void *pvParameters);
static void ble_gd32_relay_wait_task(void *pvParameters);
// 清空 s_otaDataQueue 中的残留批次并释放其 data 指针。
// 仅用于纯清理场景;带计数/日志的清理请保留内联实现。
static void drainOtaDataQueue()
{
BleOtaDataBatch stale;
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
if (stale.data) free(stale.data);
}
}
// ============================================================
/**
* @brief 获取 BleOta 单例实例。
@@ -1683,10 +1693,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
// ★ 断开时清空数据队列,丢弃属于旧会话的残留包
if (s_otaDataQueue) {
BleOtaDataBatch stale;
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
free(stale.data);
}
drainOtaDataQueue();
}
BleOta::instance().m_bleConnected = false;
@@ -2018,10 +2025,7 @@ esp_err_t BleOta::suspendStack()
m_suspendedForWifiOta = true;
if (s_otaDataQueue) {
BleOtaDataBatch drop;
while (xQueueReceive(s_otaDataQueue, &drop, 0) == pdTRUE) {
free(drop.data);
}
drainOtaDataQueue();
}
return ESP_OK;
+23 -21
View File
@@ -25,7 +25,6 @@
#include "esp32_ota_core.h"
#include "ota_coordinator.h"
#include "ota_relay.h"
#include "ota_snapshot.h"
#include "storage/gd32_flash_store.h"
#include "health_monitor.h"
#include "monitor/debug_log.h"
@@ -35,6 +34,25 @@
#include "monitor/task_stack_check.h"
#include "ble_ota.h"
static size_t parseHexToBytes(const char *hex, uint8_t *out, size_t max_out)
{
size_t hex_len = strlen(hex);
size_t n = 0;
for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) {
char byte_str[3] = { hex[i], hex[i + 1], 0 };
out[n++] = (uint8_t)strtol(byte_str, NULL, 16);
}
return n;
}
static void bytesToHexUpper(const uint8_t *in, size_t n, char *out)
{
for (size_t i = 0; i < n; ++i) {
snprintf(out + i * 2, 3, "%02X", in[i]);
}
out[n * 2] = '\0';
}
void http_set_cors_headers(httpd_req_t *req)
{
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
@@ -761,13 +779,7 @@ esp_err_t can_send_handler(httpd_req_t *req)
uint8_t tx_dlc = 0;
if (data_item && cJSON_IsString(data_item)) {
const char *hex_str = cJSON_GetStringValue(data_item);
size_t hex_len = strlen(hex_str);
for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
tx_dlc++;
}
tx_dlc = (uint8_t)parseHexToBytes(cJSON_GetStringValue(data_item), tx_data, CAN_MAX_DLC);
}
if (tx_dlc == 0 && dlc > 0) {
@@ -783,9 +795,7 @@ esp_err_t can_send_handler(httpd_req_t *req)
char resp[512];
if (resp_len >= 0) {
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
for (int i = 0; i < resp_len; i++) {
sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
}
bytesToHexUpper(resp_data, (size_t)resp_len, resp_hex);
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
@@ -834,13 +844,7 @@ esp_err_t spi_send_handler(httpd_req_t *req)
uint8_t payload_data[SPI_OTA_FRAME_MAX] = {0};
size_t payload_len = 0;
if (data_item && cJSON_IsString(data_item)) {
const char *hex_str = cJSON_GetStringValue(data_item);
size_t hex_len = strlen(hex_str);
for (size_t i = 0; i < hex_len / 2 && i < SPI_OTA_FRAME_MAX; i++) {
char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
payload_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
payload_len++;
}
payload_len = parseHexToBytes(cJSON_GetStringValue(data_item), payload_data, SPI_OTA_FRAME_MAX);
}
DEBUG_INFO("SPI", "WEB SPI: type=0x%02X, payload=%zu 字节",
@@ -857,9 +861,7 @@ esp_err_t spi_send_handler(httpd_req_t *req)
if (spi_err == ESP_OK) {
char resp_hex[SPI_OTA_FRAME_MAX * 2 + 1] = {0};
for (size_t i = 0; i < resp_len; i++) {
sprintf(&resp_hex[i * 2], "%02X", resp_payload[i]);
}
bytesToHexUpper(resp_payload, resp_len, resp_hex);
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"type\": \"%02X\", \"payload_len\": %zu, \"resp_type\": \"%02X\", \"resp_len\": %zu, \"recv\": \"%s\"}",
frame_type, payload_len, resp_type, resp_len, resp_hex);
+28 -54
View File
@@ -33,6 +33,28 @@
#include "http_handlers.h"
// ----------------------------------------
// 文件作用域 static helper
// ----------------------------------------
/**
* @brief 清除指定 NVS 命名空间下的全部键(erase_all + commit + close)。
* 纯函数,内部不打日志,由调用方负责日志输出。
* @param ns NVS 命名空间字符串(例如 "nvs.net80211"、"phy")。
* @return ESP_OK 成功;否则返回 nvs_open 的错误码。
*/
static esp_err_t clearNvsNamespace(const char *ns)
{
nvs_handle_t h;
esp_err_t err = nvs_open(ns, NVS_READWRITE, &h);
if (err != ESP_OK) {
return err;
}
nvs_erase_all(h);
nvs_commit(h);
nvs_close(h);
return ESP_OK;
}
// ============================================================
/**
@@ -45,10 +67,6 @@ WifiOta& WifiOta::instance()
return inst;
}
/**
* @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
*/
// ----------------------------------------
// STA 后台重连任务(避免在系统事件回调中调用 esp_wifi_connect 死锁)
// ----------------------------------------
@@ -150,9 +168,6 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
inst.stateUnlock();
// ★ M2: 推送 WiFi 连接状态给 HealthMonitor(解耦监控层主动查询)
HealthMonitor::instance().setWifiConnected(true);
if (inst.m_staConnectedSem) {
xSemaphoreGive(inst.m_staConnectedSem);
}
break;
case WIFI_EVENT_STA_DISCONNECTED: {
@@ -298,8 +313,6 @@ esp_err_t WifiOta::initSta()
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
setStaticIp();
// ★ 冷启动时在 wifi_start 前增加延迟
// 长时间断电后首次上电,WiFi PHY 校准需要额外时间
// esp_wifi_start() 内部会触发 PHY 初始化和校准
@@ -325,8 +338,8 @@ esp_err_t WifiOta::initSta()
*/
esp_err_t WifiOta::waitStaConnected(char *ip_str, size_t ip_len)
{
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";
SemaphoreHandle_t wait_sem = WifiOta::instance().m_staGotIpSem;
const char *wait_name = "STA_GOT_IP";
if (wait_sem == NULL) {
DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
@@ -368,37 +381,6 @@ esp_err_t WifiOta::waitStaConnected(char *ip_str, size_t ip_len)
return ESP_ERR_TIMEOUT;
}
/**
* @brief 设置 STA 静态 IP
*/
void WifiOta::setStaticIp()
{
if (WIFI_STA_USE_STATIC_IP) {
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (sta_netif) {
DEBUG_INFO("WIFI", "STA 使用静态 IP 模式,停止 DHCP Client");
ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
esp_netif_ip_info_t info;
memset(&info, 0, sizeof(info));
inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
// 设置 DNS
esp_netif_dns_info_t dns_info;
memset(&dns_info, 0, sizeof(dns_info));
inet_pton(AF_INET, WIFI_STA_STATIC_DNS, &dns_info.ip.u_addr.ip4.addr);
ESP_ERROR_CHECK(esp_netif_set_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns_info));
DEBUG_INFO("WIFI", "DNS 已设置: %s", WIFI_STA_STATIC_DNS);
}
} else {
DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
}
}
// ----------------------------------------
// ESP32 OTA 内部函数
// 注:begin/write/finalize/abort 已抽离到 Esp32OtaCore(main/ota/esp32_ota_core.{h,cpp})
@@ -587,23 +569,16 @@ bool WifiOta::earlyInit()
// ★ 冷启动时强制清除 WiFi NVS 缓存,避免残留脏数据
// 长时间断电后 NVS 中的 WiFi 配置缓存可能不一致
if (WifiOta::instance().m_coldBoot) {
nvs_handle_t nvs_handle;
err = nvs_open("nvs.net80211", NVS_READWRITE, &nvs_handle);
// 清除 WiFi 驱动内部 NVS 缓存键
err = clearNvsNamespace("nvs.net80211");
if (err == ESP_OK) {
// 清除 WiFi 驱动内部 NVS 缓存键
nvs_erase_all(nvs_handle);
nvs_commit(nvs_handle);
nvs_close(nvs_handle);
DEBUG_DBG("WIFI", "冷启动: 已清除 WiFi NVS 缓存 (nvs.net80211)");
} else {
DEBUG_INFO("WIFI", "冷启动: 无 WiFi NVS 缓存分区,跳过清除");
}
// 同样清除 PHY 校准数据分区,强制下次全量校准
err = nvs_open("phy", NVS_READWRITE, &nvs_handle);
err = clearNvsNamespace("phy");
if (err == ESP_OK) {
nvs_erase_all(nvs_handle);
nvs_commit(nvs_handle);
nvs_close(nvs_handle);
DEBUG_DBG("WIFI", "冷启动: 已清除 PHY 校准缓存,将重新校准");
}
}
@@ -620,9 +595,8 @@ bool WifiOta::earlyInit()
}
}
WifiOta::instance().m_staConnectedSem = xSemaphoreCreateBinary();
WifiOta::instance().m_staGotIpSem = xSemaphoreCreateBinary();
if (!WifiOta::instance().m_staConnectedSem || !WifiOta::instance().m_staGotIpSem) {
if (!WifiOta::instance().m_staGotIpSem) {
DEBUG_ERR("WIFI", "创建信号量失败");
}
-5
View File
@@ -80,7 +80,6 @@ public:
// ── 初始化/运行时状态(仅 OTA 任务访问,无需额外锁)──
ota_wifi_mode_t m_wifiMode = OTA_WIFI_MODE_AP; ///< 当前 WiFi 工作模式
bool m_coldBoot = false; ///< 是否为冷启动(首次上电)
SemaphoreHandle_t m_staConnectedSem = NULL; ///< STA 连接成功信号量
SemaphoreHandle_t m_staGotIpSem = NULL; ///< STA 获取 IP 信号量
TaskHandle_t m_reconnectTask = nullptr; ///< STA 后台重连任务句柄
@@ -117,10 +116,6 @@ private:
* @return ESP_OK 成功,其他失败。
*/
esp_err_t initSta();
/**
* @brief 设置 STA 静态 IP、网关和子网掩码。
*/
void setStaticIp();
/**
* @brief WiFi/IP 事件回调,处理连接、断开和 IP 获取。
* @param arg 回调用户参数。
+1 -26
View File
@@ -106,12 +106,6 @@ bool SpiMaster::pushEvent(uint8_t type, const uint8_t *payload, size_t len)
}
m_eventHead = (uint8_t)((m_eventHead + 1U) % kEventSlots);
m_eventCount++;
/* 兼容单槽 takeRingRx */
m_ringRxType = type;
m_ringRxLen = slot.len;
if (slot.len > 0U) {
memcpy(m_ringRxBuf, slot.data, slot.len);
}
if (m_eventSem) {
(void)xSemaphoreGive(m_eventSem);
}
@@ -574,14 +568,7 @@ esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_
v2_header_t hdr;
const uint8_t *pload = nullptr;
if (!v2_parse_frame(out_frame, budget, &hdr, &pload)) {
bool all_ff = true;
for (size_t i = 0; i < 8U && i < budget; ++i) {
if (out_frame[i] != 0xFFU) {
all_ff = false;
break;
}
}
return all_ff ? ESP_ERR_INVALID_RESPONSE : ESP_ERR_INVALID_RESPONSE;
return ESP_ERR_INVALID_RESPONSE;
}
const size_t total = V2_HEADER_LEN + (size_t)hdr.payload_len + V2_CRC_LEN;
@@ -1162,18 +1149,6 @@ bool SpiMaster::takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_
}
return true;
}
if (m_ringRxLen != 0U) {
/* 兼容单槽回落 */
if (type) { *type = m_ringRxType; }
size_t copy_len = (m_ringRxLen <= max_len) ? m_ringRxLen : max_len;
if (len) { *len = copy_len; }
if (payload && copy_len > 0U) {
memcpy(payload, m_ringRxBuf, copy_len);
}
m_ringRxLen = 0U;
m_ringRxType = 0U;
return true;
}
}
/* 队列空:若 timeout_ms==0 直接返回,否则在信号量上等待 */
-5
View File
@@ -271,11 +271,6 @@ private:
/** 事件计数信号量(worker push / takeEvent pop)。 */
SemaphoreHandle_t m_eventSem = NULL;
/** GD32 按铃请求帧缓冲区(兼容 takeRingRx 单槽)。 */
uint8_t m_ringRxBuf[SPI_OTA_FRAME_MAX] = {};
size_t m_ringRxLen = 0;
uint8_t m_ringRxType = 0;
// ── L4 多槽事件队列(worker 写入,业务 takeEvent 读出)──
static constexpr uint8_t kEventSlots = 4U;
struct EventSlot {