feat(ota): 添加OTA回滚保护和固件完整性校验功能

- 启用CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE和CONFIG_APP_ROLLBACK_ENABLE配置选项
- 在main.cpp中添加esp_ota_mark_app_valid_cancel_rollback()调用,防止意外回滚
- 实现SHA-256固件完整性校验,包括计算、存储和验证功能
- 添加ota_mutex.h头文件用于ESP32 OTA全局互斥锁管理

feat(health_monitor): 实现线程安全的健康状态管理

- 将HealthMonitor类的健康状态改为私有成员并通过mutex保护
- 添加getState()和setState()方法提供线程安全的状态访问
- 修复多任务并发访问健康状态时的竞争条件问题

feat(ota_relay): 增强中继模块的线程安全性和可靠性

- 为OtaRelay类添加静态mutex保护状态变量访问
- 实现StateLockGuard RAII锁管理器确保异常安全
- 添加全局超时机制防止OTA过程无限等待
- 改进flash写入逻辑支持非4字节对齐数据处理

refactor(app_config): 更新版本信息和配置常量

- 更新BUILD_TIME至最新编译时间
- 更新GIT_HASH至最新提交
- 添加BLE_OTA_CTRL_VERIFY_SHA256命令常量定义
This commit is contained in:
小龙虾
2026-06-14 17:10:24 +08:00
parent 6a6bc9dab4
commit 2caf10b9de
26 changed files with 5221 additions and 949 deletions
+21
View File
@@ -11,6 +11,7 @@
#include "esp_system.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "esp_ota_ops.h"
#include "monitor/health_monitor.h"
#include "relay/ota_relay.h"
@@ -19,15 +20,25 @@
#include "server/ble_ota.h"
#include "transport/protocol.h"
#include "monitor/debug_log.h"
#include "app_config.h"
#include "ota_mutex.h"
/**
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
*/
// 全局 ESP32 OTA 互斥变量(声明在 ota_mutex.h)
SemaphoreHandle_t g_esp32OtaMutex = NULL;
volatile bool g_esp32OtaOccupied = false;
extern "C" void app_main(void)
{
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
// 创建 ESP32 OTA 全局互斥 Mutex
g_esp32OtaMutex = xSemaphoreCreateMutex();
g_esp32OtaOccupied = false;
// 初始化网络接口 + 事件循环(必须最早)
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
@@ -123,6 +134,16 @@ extern "C" void app_main(void)
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
}
// ★ OTA 回滚保护:标记当前固件有效
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
if (ota_valid_err == ESP_OK) {
DEBUG_INFO("MAIN", "OTA 回滚保护: 当前固件已标记有效");
} else {
DEBUG_WARN("MAIN", "OTA 回滚保护: 标记失败 (%s),可能非 OTA 启动", esp_err_to_name(ota_valid_err));
}
DEBUG_INFO("MAIN", "========================================");
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
+3 -1
View File
@@ -1,4 +1,4 @@
/*
/*
* app_config.h - 统一配置文件(C++ 版本)
* ESP32-C3 OTA 中继项目
*
@@ -7,6 +7,7 @@
*/
#pragma once
#include <cstdint>
#include "freertos/FreeRTOS.h"
#include "version.h"
// ============================================================
@@ -95,6 +96,7 @@ inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7;
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8;
inline constexpr uint8_t BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9; // SHA-256哈希验证命令
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00;
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01;
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480;
+36
View File
@@ -0,0 +1,36 @@
/*
* ota_mutex.h - ESP32 OTA 全局互斥(BLE / WiFi 通道共享)
*
* 同一时刻只允许一个 OTA 源(BLE 或 WiFi)操作 ESP32 OTA 分区。
* 变量定义 + Mutex 创建在 main.cpp app_main()。
*/
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <cstdint>
extern SemaphoreHandle_t g_esp32OtaMutex;
extern volatile bool g_esp32OtaOccupied;
// 获取全局 OTA 占用标志(带超时),成功返回 true
// 内部: Take Mutex → 检查标志 → 若空闲则置 true → Give Mutex
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;
if (g_esp32OtaOccupied) {
xSemaphoreGive(g_esp32OtaMutex);
return false;
}
g_esp32OtaOccupied = true;
xSemaphoreGive(g_esp32OtaMutex);
return true;
}
// 释放全局 OTA 占用标志
// 内部: Take Mutex → 清除标志 → Give Mutex
static inline void esp32_ota_mutex_release(void) {
if (!g_esp32OtaMutex) return;
xSemaphoreTake(g_esp32OtaMutex, portMAX_DELAY);
g_esp32OtaOccupied = false;
xSemaphoreGive(g_esp32OtaMutex);
}
+2 -2
View File
@@ -8,7 +8,7 @@
#define FW_VERSION_STRING "0.3.12"
// 编译时间
#define BUILD_TIME "2026-06-13 01:22:52"
#define BUILD_TIME "2026-06-14 17:05:57"
// Git 信息
#define GIT_HASH "d8e4222"
#define GIT_HASH "6a6bc9d"
+60 -37
View File
@@ -3,6 +3,8 @@
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
*
* 所有功能封装为 HealthMonitor 类方法。
* 所有对 m_healthState / m_lastTemperature / m_lastWifiRssi 的访问
* 均通过 m_mutex 保护,消除跨任务竞态。
*/
#include <stdio.h>
#include <string.h>
@@ -18,11 +20,6 @@
#include "debug_log.h"
#include "server/wifi_ota.h"
// ============================================================
// 静态成员
// ============================================================
health_state_t HealthMonitor::healthState = SYS_NORMAL;
// ============================================================
// 单例
// ============================================================
@@ -44,36 +41,54 @@ void HealthMonitor::init()
}
// ============================================================
// 获取当前健康状态
// 线程安全的健康状态读写
// ============================================================
health_state_t HealthMonitor::getState()
{
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
health_state_t s = m_healthState;
if (m_mutex) xSemaphoreGive(m_mutex);
return s;
}
void HealthMonitor::setState(health_state_t newState)
{
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
m_healthState = newState;
if (m_mutex) xSemaphoreGive(m_mutex);
}
// ============================================================
// 获取当前健康状态(public 接口)
// ============================================================
health_state_t HealthMonitor::getCurrent()
{
health_state_t state;
if (m_mutex) {
xSemaphoreTake(m_mutex, portMAX_DELAY);
}
state = healthState;
if (m_mutex) {
xSemaphoreGive(m_mutex);
}
return state;
return getState();
}
float HealthMonitor::getLastTemperature()
{
return m_lastTemperature;
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
float t = m_lastTemperature;
if (m_mutex) xSemaphoreGive(m_mutex);
return t;
}
bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
{
if (m_lastWifiRssiMs <= 0) {
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
int64_t lastMs = m_lastWifiRssiMs;
int8_t rssi = m_lastWifiRssi;
if (m_mutex) xSemaphoreGive(m_mutex);
if (lastMs <= 0) {
return false;
}
int64_t now_ms = esp_timer_get_time() / 1000;
uint32_t age_ms = (now_ms > m_lastWifiRssiMs) ? (uint32_t)(now_ms - m_lastWifiRssiMs) : 0;
uint32_t age_ms = (now_ms > lastMs) ? (uint32_t)(now_ms - lastMs) : 0;
if (out_rssi) *out_rssi = m_lastWifiRssi;
if (out_rssi) *out_rssi = rssi;
if (out_age_ms) *out_age_ms = age_ms;
return true;
}
@@ -91,15 +106,18 @@ void HealthMonitor::tempMonitorCallback(void *arg)
return;
}
if (self.m_mutex) xSemaphoreTake(self.m_mutex, portMAX_DELAY);
self.m_lastTemperature = temp;
if (self.m_mutex) xSemaphoreGive(self.m_mutex);
if (temp >= TEMP_CRITICAL) {
healthState = SYS_SAFE_SHUTDOWN;
self.setState(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 (healthState < SYS_WARNING) {
healthState = SYS_WARNING;
health_state_t cur = self.getState();
if (cur < SYS_WARNING) {
self.setState(SYS_WARNING);
DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
}
@@ -144,8 +162,9 @@ void HealthMonitor::taskEntry(void *pvParameters)
if (free_heap < HEAP_WARNING_MIN) {
DEBUG_WARN("MON", "堆内存不足: %lu < %lu", free_heap, HEAP_WARNING_MIN);
}
if (healthState >= SYS_CRITICAL) {
DEBUG_ERR("MON", "健康状态危险: %d", healthState);
health_state_t cur = self.getState();
if (cur >= SYS_CRITICAL) {
DEBUG_ERR("MON", "健康状态危险: %d", cur);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
@@ -165,14 +184,15 @@ void HealthMonitor::logEvent(const char *event, int value)
health_status_t HealthMonitor::checkBeforeOta()
{
health_status_t status = HEALTH_MON_OK;
health_state_t cur = getState();
if (healthState >= SYS_SAFE_SHUTDOWN) {
if (cur >= SYS_SAFE_SHUTDOWN) {
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输");
return HEALTH_MON_SHUTDOWN;
} else if (healthState >= SYS_CRITICAL) {
} else if (cur >= SYS_CRITICAL) {
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输");
return HEALTH_MON_CRITICAL;
} else if (healthState >= SYS_WARNING) {
} else if (cur >= SYS_WARNING) {
status = HEALTH_MON_WARNING;
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
}
@@ -210,18 +230,19 @@ health_status_t HealthMonitor::checkBeforeOta()
health_status_t HealthMonitor::checkBeforeOtaRelay()
{
health_status_t status = HEALTH_MON_OK;
health_state_t cur = getState();
if (healthState >= SYS_SAFE_SHUTDOWN) {
if (cur >= SYS_SAFE_SHUTDOWN) {
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_SAFE_SHUTDOWN,禁止传输");
return HEALTH_MON_SHUTDOWN;
} else if (healthState >= SYS_CRITICAL) {
} else if (cur >= SYS_CRITICAL) {
/* SYS_CRITICAL 可能是由 WiFi RSSI 过低导致的,
* 但 SPI 中继阶段不依赖 WiFi,只检查温度/硬件等本地状态。
* 温度达到 TEMP_CRITICAL(100°C) 时 g_health_state 会被设为
* 温度达到 TEMP_CRITICAL(100°C) 时 m_healthState 会被设为
* SYS_SAFE_SHUTDOWN,在上面的分支已经拦截。
* 所以 SYS_CRITICAL 在中继阶段只可能是 WiFi 导致的,允许继续。 */
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_CRITICAL (WiFi 相关),SPI 中继允许继续");
} else if (healthState >= SYS_WARNING) {
} else if (cur >= SYS_WARNING) {
status = HEALTH_MON_WARNING;
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_WARNING,继续传输");
}
@@ -243,24 +264,26 @@ health_status_t HealthMonitor::checkBeforeOtaRelay()
health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
{
if (rssi < WIFI_RSSI_CRITICAL) {
healthState = SYS_CRITICAL;
setState(SYS_CRITICAL);
DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
logEvent("WIFI危险", rssi);
return HEALTH_MON_CRITICAL;
} else if (rssi < WIFI_RSSI_NORMAL) {
if (healthState < SYS_WARNING) {
healthState = SYS_WARNING;
health_state_t cur = getState();
if (cur < SYS_WARNING) {
setState(SYS_WARNING);
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
logEvent("WIFI警告", rssi);
}
return HEALTH_MON_WARNING;
}
if (healthState == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
healthState = SYS_NORMAL;
health_state_t cur = getState();
if (cur == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
setState(SYS_NORMAL);
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
logEvent("WIFI恢复", rssi);
}
return HEALTH_MON_OK;
}
}
+8 -4
View File
@@ -54,7 +54,7 @@ public:
/** @brief 记录一条健康事件日志。 */
void logEvent(const char *event, int value);
/** @brief 获取当前系统健康状态。 */
/** @brief 获取当前系统健康状态(线程安全)。 */
health_state_t getCurrent();
/** @brief 根据 WiFi RSSI 更新健康状态。 */
@@ -66,10 +66,13 @@ public:
/** @brief 获取最近一次 WiFi RSSI 及其年龄。 */
bool getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms);
/** @brief 全局健康状态(public,供外部读取)。 */
static health_state_t healthState;
private:
/** @brief 加锁读取健康状态。 */
health_state_t getState();
/** @brief 加锁设置健康状态。 */
void setState(health_state_t newState);
HealthMonitor() = default;
~HealthMonitor() = default;
HealthMonitor(const HealthMonitor&) = delete;
@@ -77,6 +80,7 @@ private:
SemaphoreHandle_t m_mutex = NULL;
temperature_sensor_handle_t m_tempHandle = NULL;
health_state_t m_healthState = SYS_NORMAL;
float m_lastTemperature = 0.0f;
int8_t m_lastWifiRssi = -127;
int64_t m_lastWifiRssiMs = 0;
+332 -16
View File
@@ -1,10 +1,11 @@
/*
/*
* ota_relay.cpp - OTA 中继核心状态机实现
* T2.3, T2.4, T3.1 - T3.7
*
* 所有功能封装为 OtaRelay 类方法。
*/
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -14,6 +15,8 @@
#include "nvs_flash.h"
#include "nvs.h"
#include "aes/esp_aes.h"
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
#include "mbedtls/private/sha256.h"
#include "ota_relay.h"
#include "spi_master.h"
@@ -31,12 +34,81 @@ volatile bool OtaRelay::gd32AppAlive = false;
gd32_version_t OtaRelay::gd32Version = {};
QueueHandle_t OtaRelay::dispatchQueue = NULL;
QueueHandle_t OtaRelay::resultQueue = NULL;
SemaphoreHandle_t OtaRelay::s_stateMutex = NULL;
OtaRelay& OtaRelay::instance()
{
static OtaRelay inst;
return inst;
}
void OtaRelay::initMutex()
{
if (!s_stateMutex) {
s_stateMutex = xSemaphoreCreateMutex();
}
}
relay_state_t OtaRelay::getState()
{
StateLockGuard lock;
return relayState;
}
uint32_t OtaRelay::getRelaySeq()
{
StateLockGuard lock;
return relaySeq;
}
uint32_t OtaRelay::getRelayTotalPackets()
{
StateLockGuard lock;
return relayTotalPackets;
}
bool OtaRelay::getGd32AppAlive()
{
StateLockGuard lock;
return gd32AppAlive;
}
void OtaRelay::getGd32Version(gd32_version_t *out)
{
if (!out) return;
StateLockGuard lock;
*out = gd32Version;
}
void OtaRelay::setState(relay_state_t s)
{
StateLockGuard lock;
relayState = s;
}
void OtaRelay::setRelaySeq(uint32_t seq)
{
StateLockGuard lock;
relaySeq = seq;
}
void OtaRelay::setRelayTotalPackets(uint32_t total)
{
StateLockGuard lock;
relayTotalPackets = total;
}
void OtaRelay::setGd32AppAlive(bool alive)
{
StateLockGuard lock;
gd32AppAlive = alive;
}
void OtaRelay::setGd32Version(const gd32_version_t &ver)
{
StateLockGuard lock;
gd32Version = ver;
}
// ============================================================
@@ -236,6 +308,13 @@ esp_err_t OtaRelay::metaLoad(ota_meta_t *meta)
if (nvs_get_u32(handle, "last_seq", &val) == ESP_OK) meta->last_seq = val;
if (nvs_get_u32(handle, "chunk_count", &val) == ESP_OK) meta->chunk_count = val;
size_t sha_len = sizeof(meta->checksum);
if (nvs_get_blob(handle, "checksum", meta->checksum, &sha_len) == ESP_OK && sha_len == sizeof(meta->checksum)) {
// checksum 已从 NVS 恢复
} else {
memset(meta->checksum, 0, sizeof(meta->checksum));
}
nvs_close(handle);
return ESP_OK;
}
@@ -260,6 +339,7 @@ esp_err_t OtaRelay::metaSave(const ota_meta_t *meta)
nvs_set_u32(handle, "stored_size", meta->stored_size);
nvs_set_u32(handle, "last_seq", meta->last_seq);
nvs_set_u32(handle, "chunk_count", meta->chunk_count);
nvs_set_blob(handle, "checksum", meta->checksum, sizeof(meta->checksum));
err = nvs_commit(handle);
nvs_close(handle);
@@ -298,6 +378,131 @@ esp_err_t OtaRelay::metaClear()
return OtaRelay::metaSave(&meta);
}
/**
* @brief 计算 gd32_ota 分区中已存储固件的 SHA-256
* @param out_sha256 输出 32 字节 SHA-256 摘要。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t OtaRelay::computeFlashSha256(uint8_t out_sha256[32])
{
if (!out_sha256) return ESP_ERR_INVALID_ARG;
ota_meta_t meta;
esp_err_t err = metaLoad(&meta);
if (err != ESP_OK || meta.total_size == 0) {
DEBUG_ERR("OTA", "computeSha256: 元数据无效");
return ESP_ERR_INVALID_STATE;
}
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
if (mbedtls_sha256_starts(&ctx, 0) != 0) {
mbedtls_sha256_free(&ctx);
return ESP_FAIL;
}
static constexpr size_t kChunkSize = 1024;
uint8_t *buf = (uint8_t *)malloc(kChunkSize);
if (!buf) {
mbedtls_sha256_free(&ctx);
return ESP_ERR_NO_MEM;
}
uint32_t remaining = meta.total_size;
uint32_t offset = 0;
while (remaining > 0) {
size_t to_read = (remaining > kChunkSize) ? kChunkSize : remaining;
err = flashReadChunk(offset, buf, to_read);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "computeSha256: Flash 读取失败 offset=%" PRIu32, offset);
free(buf);
mbedtls_sha256_free(&ctx);
return err;
}
if (mbedtls_sha256_update(&ctx, buf, to_read) != 0) {
free(buf);
mbedtls_sha256_free(&ctx);
return ESP_FAIL;
}
offset += to_read;
remaining -= to_read;
}
free(buf);
int ret = mbedtls_sha256_finish(&ctx, out_sha256);
mbedtls_sha256_free(&ctx);
return (ret == 0) ? ESP_OK : ESP_FAIL;
}
/**
* @brief 固件接收完毕后,计算 SHA-256 并写入元数据
* @return ESP_OK 成功,其他失败。
*/
esp_err_t OtaRelay::metaFinalizeChecksum()
{
ota_meta_t meta;
esp_err_t err = metaLoad(&meta);
if (err != ESP_OK) return err;
if (meta.total_size == 0 || meta.stored_size < meta.total_size) {
DEBUG_ERR("OTA", "metaFinalize: 固件不完整 stored=%" PRIu32 " total=%" PRIu32,
meta.stored_size, meta.total_size);
return ESP_ERR_INVALID_STATE;
}
err = computeFlashSha256(meta.checksum);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "metaFinalize: SHA-256 计算失败");
return err;
}
char hex[65] = {0};
for (int i = 0; i < 32; ++i)
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", meta.checksum[i]);
DEBUG_INFO("OTA", "固件 SHA-256: %s", hex);
return metaSave(&meta);
}
/**
* @brief 中继转发前校验固件完整性
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败。
*/
esp_err_t OtaRelay::metaVerifyChecksum()
{
ota_meta_t meta;
esp_err_t err = metaLoad(&meta);
if (err != ESP_OK) return err;
// 全零 checksum 表示从未计算过(兼容旧固件)
if (bytesAllValue(meta.checksum, sizeof(meta.checksum), 0x00)) {
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,跳过校验(旧数据兼容)");
return ESP_OK;
}
uint8_t computed[32] = {0};
err = computeFlashSha256(computed);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "metaVerify: SHA-256 计算失败");
return err;
}
if (memcmp(computed, meta.checksum, 32) != 0) {
char hex_stored[65] = {0}, hex_computed[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&hex_stored[i * 2], sizeof(hex_stored) - i * 2, "%02X", meta.checksum[i]);
snprintf(&hex_computed[i * 2], sizeof(hex_computed) - i * 2, "%02X", computed[i]);
}
DEBUG_ERR("OTA", "固件完整性校验失败!");
DEBUG_ERR("OTA", " 存储 SHA-256: %s", hex_stored);
DEBUG_ERR("OTA", " 计算 SHA-256: %s", hex_computed);
return ESP_ERR_INVALID_CRC;
}
DEBUG_INFO("OTA", "固件完整性校验通过 (SHA-256 匹配)");
return ESP_OK;
}
/**
* @brief Flash 写入接口
* @param offset 分区内写入偏移。
@@ -316,7 +521,58 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t
}
}
return esp_partition_write(instance().m_gd32Partition, offset, data, len);
// --- offset 必须4字节对齐 ---
if (offset % 4 != 0) {
DEBUG_ERR("OTA", "flashWriteChunk offset非4对齐: offset=%" PRIu32, offset);
return ESP_ERR_INVALID_ARG;
}
const esp_partition_t *part = instance().m_gd32Partition;
// --- len已对齐:直接写入 ---
if (len % 4 == 0) {
if (offset + len > part->size) {
DEBUG_ERR("OTA", "flashWriteChunk越界: offset=%" PRIu32 " len=%zu part_size=%" PRIu32,
offset, len, part->size);
return ESP_ERR_INVALID_SIZE;
}
return esp_partition_write(part, offset, data, len);
}
// --- len未对齐:read-modify-write ---
size_t aligned_len = (len + 3) & ~(size_t)3;
if (offset + aligned_len > part->size) {
DEBUG_ERR("OTA", "flashWriteChunk对齐后越界: offset=%" PRIu32 " len=%zu aligned=%zu part=%" PRIu32,
offset, len, aligned_len, part->size);
return ESP_ERR_INVALID_SIZE;
}
DEBUG_WARN("OTA", "flashWriteChunk len非4对齐: len=%zu aligned_len=%zu offset=%" PRIu32,
len, aligned_len, offset);
// 读回尾部padding字节(offset+len处,最多3字节)
size_t pad_len = aligned_len - len;
uint8_t pad[4] = {0};
esp_err_t err = esp_partition_read(part, offset + len, pad, pad_len);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "flashWriteChunk读回padding失败: offset=%" PRIu32 " pad=%zu err=%s",
offset + len, pad_len, esp_err_to_name(err));
return err;
}
// 拼接:data + pad -> aligned_buf
uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len);
if (!aligned_buf) {
DEBUG_ERR("OTA", "flashWriteChunk malloc失败: %zu字节", aligned_len);
return ESP_ERR_NO_MEM;
}
memcpy(aligned_buf, data, len);
memcpy(aligned_buf + len, pad, pad_len);
err = esp_partition_write(part, offset, aligned_buf, aligned_len);
free(aligned_buf);
return err;
}
/**
@@ -579,7 +835,7 @@ ota_result_t OtaRelay::sendFirmwareSize(uint32_t size)
* @brief 从 Flash 向 GD32 转发存储的固件(48B/包)
* @return 操作结果码。
*/
ota_result_t OtaRelay::relayPacketsFromFlash()
ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick)
{
DEBUG_INFO("OTA", "=== 阶段3: 从 Flash 转发固件 ===");
@@ -613,6 +869,12 @@ ota_result_t OtaRelay::relayPacketsFromFlash()
}
}
/* 全局超时检查:避免单包 WAIT 循环累计导致整体挂起 */
if (deadline_tick != 0 && xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 包 %u/%u 已超限,中止转发", seq, total_packets);
return OTA_RESULT_ERR_USER_ABORT;
}
uint8_t plain48[GD32_OTA_PACKET_SIZE];
memset(plain48, 0xFF, sizeof(plain48));
size_t chunk_size = GD32_OTA_PACKET_SIZE;
@@ -920,10 +1182,19 @@ ota_result_t OtaRelay::startHandshake()
ota_meta_t meta;
OtaRelay::metaLoad(&meta);
// 固件完整性校验
esp_err_t verify_err = metaVerifyChecksum();
if (verify_err != ESP_OK) {
DEBUG_ERR("OTA", "固件完整性校验失败,中止转发");
reset();
return OTA_RESULT_ERR_CRC;
}
res = sendFirmwareSize(meta.total_size);
if (res != OTA_RESULT_OK) { reset(); return res; }
res = relayPacketsFromFlash();
res = relayPacketsFromFlash(0);
if (res != OTA_RESULT_OK) { reset(); return res; }
res = finishOta();
@@ -945,40 +1216,82 @@ ota_result_t OtaRelay::forwardFirmware()
DEBUG_INFO("OTA", "开始固件转发: %u 字节, %u 块",
meta.total_size, meta.chunk_count);
// 固件完整性校验
{
esp_err_t verify_err = metaVerifyChecksum();
if (verify_err != ESP_OK) {
DEBUG_ERR("OTA", "固件完整性校验失败,中止转发");
return OTA_RESULT_ERR_CRC;
}
}
/* 全局超时:动态计算,min = 5min, max = 10min, 按 total_size/1024*2ms 递增 */
constexpr uint32_t kMinGlobalTimeoutMs = 5U * 60U * 1000U;
constexpr uint32_t kMaxGlobalTimeoutMs = 10U * 60U * 1000U;
uint32_t dynamic_ms = kMinGlobalTimeoutMs + (meta.total_size / 1024U) * 2U;
uint32_t global_timeout_ms = (dynamic_ms > kMaxGlobalTimeoutMs) ? kMaxGlobalTimeoutMs : dynamic_ms;
TickType_t start_tick = xTaskGetTickCount();
TickType_t deadline_tick = start_tick + pdMS_TO_TICKS(global_timeout_ms);
DEBUG_INFO("OTA", "全局超时: %u ms (%u 秒), deadline tick=%u",
global_timeout_ms, global_timeout_ms / 1000U, (unsigned)deadline_tick);
requestGd32AppEnterBootloader();
/* 在每个阶段前检查全局超时,超时则 reset() 并返回 OTA_RESULT_ERR_USER_ABORT */
if (xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 在 requestBootloader 阶段已超限 (%u ms),中止 OTA", global_timeout_ms);
reset();
return OTA_RESULT_ERR_USER_ABORT;
}
ota_result_t res = sendHandshakeReq();
if (res != OTA_RESULT_OK) { reset(); return res; }
if (xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 在 sendHandshakeReq 阶段已超限 (%u ms),中止 OTA", global_timeout_ms);
reset();
return OTA_RESULT_ERR_USER_ABORT;
}
res = sendNonceAndAuthenticate();
if (res != OTA_RESULT_OK) { reset(); return res; }
if (xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 在 sendNonceAndAuthenticate 阶段已超限 (%u ms),中止 OTA", global_timeout_ms);
reset();
return OTA_RESULT_ERR_USER_ABORT;
}
res = sendFirmwareSize(meta.total_size);
if (res != OTA_RESULT_OK) { reset(); return res; }
res = relayPacketsFromFlash();
if (xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 在 sendFirmwareSize 阶段已超限 (%u ms),中止 OTA", global_timeout_ms);
reset();
return OTA_RESULT_ERR_USER_ABORT;
}
res = relayPacketsFromFlash(deadline_tick);
if (res != OTA_RESULT_OK) { reset(); return res; }
if (xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 在 relayPacketsFromFlash 阶段已超限 (%u ms),中止 OTA", global_timeout_ms);
reset();
return OTA_RESULT_ERR_USER_ABORT;
}
res = finishOta();
return res;
}
/**
* @brief 获取当前状态
* @return 当前中继状态枚举值。
*/
relay_state_t OtaRelay::getState()
{
/** @brief 当前 OTA 中继状态机状态。 */
return relayState;
}
/**
* @brief 重置状态机
*/
void OtaRelay::reset()
{
StateLockGuard lock;
/** @brief 当前 OTA 中继状态机状态。 */
relayState = RELAY_IDLE;
relaySeq = 0;
@@ -998,6 +1311,8 @@ void OtaRelay::taskEntry(void *pvParameters)
{
DEBUG_INFO("OTA", "OTA 中继任务启动");
initMutex();
/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
dispatchQueue = xQueueCreate(4, sizeof(uint32_t));
/** @brief OTA 结果回传队列,将执行结果返回给调用方。 */
@@ -1026,6 +1341,8 @@ void OtaRelay::taskEntry(void *pvParameters)
DEBUG_INFO("OTA", "OTA 转发成功完成");
/* 保持 RELAY_DONE 状态 3 秒,让网页轮询有机会看到完成状态 */
vTaskDelay(pdMS_TO_TICKS(3000));
} else if (result == OTA_RESULT_ERR_USER_ABORT) {
DEBUG_ERR("OTA", "OTA 全局超时中止 (OTA_RESULT_ERR_USER_ABORT=%d)", result);
} else {
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
}
@@ -1033,4 +1350,3 @@ void OtaRelay::taskEntry(void *pvParameters)
}
}
+45 -7
View File
@@ -1,4 +1,4 @@
/*
/*
* ota_relay.h - OTA 中继核心状态机
* T2.3, T3.1 - T3.7
*
@@ -10,6 +10,7 @@
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "transport/protocol.h"
#include "transport/spi_master.h"
#include "esp_partition.h"
@@ -66,7 +67,23 @@ public:
static void taskEntry(void *pvParameters);
ota_result_t startHandshake();
ota_result_t forwardFirmware();
relay_state_t getState();
/* ---- 线程安全的状态访问接口 ----
* OTA 任务写、HTTP 任务读,通过 mutex 保证一致性。
* 状态机内部(OTA 任务)可直接写 static 成员,
* 跨任务读/写必须走这些 API。
*/
static relay_state_t getState();
static uint32_t getRelaySeq();
static uint32_t getRelayTotalPackets();
static bool getGd32AppAlive();
static void getGd32Version(gd32_version_t *out);
static void setState(relay_state_t s);
static void setRelaySeq(uint32_t seq);
static void setRelayTotalPackets(uint32_t total);
static void setGd32AppAlive(bool alive);
static void setGd32Version(const gd32_version_t &ver);
void reset();
static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
@@ -75,8 +92,11 @@ public:
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();
static esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
static esp_err_t metaFinalizeChecksum();
static esp_err_t metaVerifyChecksum();
// 公开状态
// 公开状态(状态机内部直接访问;跨任务请用 getter/setter)
static relay_state_t relayState;
static uint32_t relaySeq;
static uint32_t relayTotalPackets;
@@ -85,6 +105,26 @@ public:
static QueueHandle_t dispatchQueue;
static QueueHandle_t resultQueue;
// 线程安全锁
static SemaphoreHandle_t s_stateMutex;
static void initMutex();
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50);
class StateLockGuard {
public:
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
m_locked = (s_stateMutex && xSemaphoreTake(s_stateMutex, timeout) == pdTRUE);
}
~StateLockGuard() {
if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); }
}
bool isLocked() const { return m_locked; }
StateLockGuard(const StateLockGuard&) = delete;
StateLockGuard& operator=(const StateLockGuard&) = delete;
private:
bool m_locked = false;
};
// 实例状态(public,供状态机方法访问)
const esp_partition_t *m_gd32Partition = NULL;
session_ctx_t m_sessionCtx;
@@ -100,7 +140,7 @@ public:
ota_result_t sendHandshakeReq();
ota_result_t sendNonceAndAuthenticate();
ota_result_t sendFirmwareSize(uint32_t size);
ota_result_t relayPacketsFromFlash();
ota_result_t relayPacketsFromFlash(TickType_t deadline_tick = 0);
ota_result_t finishOta();
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
@@ -141,6 +181,4 @@ private:
OtaRelay& operator=(const OtaRelay&) = delete;
};
#endif // OTA_RELAY_H
#endif // OTA_RELAY_H
+334 -166
View File
@@ -34,7 +34,12 @@
#include "services/gatt/ble_svc_gatt.h"
#include "host/ble_att.h"
// SHA-256 哈希计算
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
#include "mbedtls/private/sha256.h"
#include "app_config.h"
#include "ota_mutex.h"
#include "ble_ota.h"
#include "ota_relay.h"
#include "health_monitor.h"
@@ -53,6 +58,9 @@
#define BLE_OTA_FINISH_ERR_OTA_END 0x81
#define BLE_OTA_FINISH_ERR_SET_BOOT 0x82
#define BLE_OTA_ACK_WINDOW 32
#define BLE_ATT_ERR_OTA_BUSY 0x80 // ESP32 OTA 被其他通道占用
#define BLE_OTA_FINISH_ERR_MUTEX 0x83 // 互斥拒绝: OTA 被其他通道占用
#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小
// ============================================================
// 前向声明
@@ -97,6 +105,20 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)BleOta::instance().m_esp32Received);
// 完成 SHA-256 哈希计算
if (BleOta::instance().m_sha256Ctx != nullptr) {
mbedtls_sha256_finish(BleOta::instance().m_sha256Ctx, BleOta::instance().m_esp32Sha256);
mbedtls_sha256_free(BleOta::instance().m_sha256Ctx);
BleOta::instance().m_esp32HashComputed = true;
// 打印SHA-256哈希值
char hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", BleOta::instance().m_esp32Sha256[i]);
}
DEBUG_INFO(TAG, "ESP32 固件 SHA-256: %s", hex);
}
int64_t t0_us = esp_timer_get_time();
DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_end()");
sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
@@ -107,6 +129,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
esp_err_to_name(err), (long long)((t1_us - t0_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
@@ -128,6 +151,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
DEBUG_ERR(TAG, "[FINISH] 设置启动分区失败: %s (耗时=%lld ms)",
esp_err_to_name(err), (long long)((t3_us - t2_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
@@ -140,10 +164,12 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
(long long)((t3_us - t2_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000));
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32Received = 0;
// SHA-256哈希已在之前计算完成,保留供前端读取
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,800ms 后重启...");
int64_t t4_us = esp_timer_get_time();
@@ -177,6 +203,20 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
return (int)copy_len;
}
/**
* @brief 清理 SHA-256 上下文和哈希状态
*/
static void ble_esp32_ota_cleanup_sha()
{
if (BleOta::instance().m_sha256Ctx != nullptr) {
mbedtls_sha256_free(BleOta::instance().m_sha256Ctx);
delete BleOta::instance().m_sha256Ctx;
BleOta::instance().m_sha256Ctx = nullptr;
}
BleOta::instance().m_esp32HashComputed = false;
memset(BleOta::instance().m_esp32Sha256, 0, sizeof(BleOta::instance().m_esp32Sha256));
}
/**
* @brief 初始化 ESP32 本机 OTA 写入会话并选择目标分区。
* @param total_size 固件总大小(字节)。
@@ -184,13 +224,11 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
*/
static esp_err_t ble_esp32_ota_begin(size_t total_size)
{
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
boot_partition->label, boot_partition->size, boot_partition->address);
// ★ 如果 BLE 自身已有未完成的 OTA 会话,先中止并释放全局互斥
if (BleOta::instance().m_esp32OtaActive) {
DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA,会先中止旧会话");
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
@@ -199,6 +237,16 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
}
// ★ 全局互斥:检查 ESP32 OTA 是否已被其他通道占用
if (!esp32_ota_mutex_acquire(1000)) {
DEBUG_ERR(TAG, "ESP32 OTA 被其他通道占用,拒绝 BLE OTA begin");
return ESP_ERR_INVALID_STATE;
}
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
boot_partition->label, boot_partition->size, boot_partition->address);
BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (BleOta::instance().m_esp32UpdatePartition == NULL ||
@@ -209,6 +257,7 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
if (BleOta::instance().m_esp32UpdatePartition == NULL) {
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
esp32_ota_mutex_release();
return ESP_FAIL;
}
@@ -221,6 +270,7 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
DEBUG_ERR(TAG, "ESP32 固件太大: %u > 分区大小: %u",
total_size, BleOta::instance().m_esp32UpdatePartition->size);
BleOta::instance().m_esp32UpdatePartition = NULL;
esp32_ota_mutex_release();
return ESP_FAIL;
}
@@ -233,6 +283,7 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32OtaHandle = 0;
esp32_ota_mutex_release();
return err;
}
@@ -241,6 +292,16 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
BleOta::instance().m_esp32RebootPending = false;
BleOta::instance().m_esp32LastCmd = 0;
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
// 初始化 SHA-256 计算上下文
if (BleOta::instance().m_sha256Ctx == nullptr) {
BleOta::instance().m_sha256Ctx = new mbedtls_sha256_context();
}
mbedtls_sha256_init(BleOta::instance().m_sha256Ctx);
mbedtls_sha256_starts(BleOta::instance().m_sha256Ctx, 0);
BleOta::instance().m_esp32HashComputed = false;
memset(BleOta::instance().m_esp32Sha256, 0, sizeof(BleOta::instance().m_esp32Sha256));
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", BleOta::instance().m_esp32UpdatePartition->label);
return ESP_OK;
}
@@ -256,6 +317,12 @@ static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len)
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) {
return ESP_ERR_INVALID_STATE;
}
// 更新 SHA-256 哈希计算
if (BleOta::instance().m_sha256Ctx != nullptr) {
mbedtls_sha256_update(BleOta::instance().m_sha256Ctx, data, len);
}
esp_err_t err = esp_ota_write(BleOta::instance().m_esp32OtaHandle, data, len);
if (err == ESP_OK) {
BleOta::instance().m_esp32Received += len;
@@ -386,6 +453,52 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
} else {
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(由设备端异步完成)");
}
} else if (buf[0] == BLE_OTA_CTRL_VERIFY_SHA256) {
// SHA-256 哈希验证命令
// 期望数据格式: [cmd(1)] + [sha256(32)]
if (len < 33) {
DEBUG_WARN(TAG, "SHA-256 验证数据长度不足: %d (需要33)", len);
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
// 提取前端发送的SHA-256哈希
uint8_t frontend_sha[32];
memcpy(frontend_sha, &buf[1], 32);
// 打印前端SHA-256
char frontend_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&frontend_hex[i * 2], sizeof(frontend_hex) - i * 2, "%02X", frontend_sha[i]);
}
DEBUG_INFO(TAG, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
DEBUG_INFO(TAG, "📊 SHA-256 哈希对比验证");
DEBUG_INFO(TAG, " 前端计算: %s", frontend_hex);
// 检查设备端是否已计算SHA-256
if (!BleOta::instance().m_esp32HashComputed) {
DEBUG_WARN(TAG, "⚠️ 设备端尚未完成SHA-256计算");
sendFinishEvent(0xA0, 0); // 自定义事件:哈希未就绪
return 0;
}
// 打印设备端SHA-256
char device_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&device_hex[i * 2], sizeof(device_hex) - i * 2, "%02X", BleOta::instance().m_esp32Sha256[i]);
}
DEBUG_INFO(TAG, " 设备计算: %s", device_hex);
// 对比哈希值
if (memcmp(frontend_sha, BleOta::instance().m_esp32Sha256, 32) == 0) {
DEBUG_INFO(TAG, "✅ SHA-256 验证通过!固件传输完整无误");
DEBUG_INFO(TAG, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
sendFinishEvent(0xA1, 1); // 自定义事件:验证成功
} else {
DEBUG_ERR(TAG, "❌ SHA-256 验证失败!固件传输过程中出现错误");
DEBUG_ERR(TAG, " 请检查BLE链路质量并重试");
DEBUG_ERR(TAG, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
sendFinishEvent(0xA2, 0); // 自定义事件:验证失败
}
}
return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0
@@ -397,8 +510,8 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
* [0] cmd (0x00=数据)
* [1-2] seq uint16 LE - 分块序号
* [3-6] total_size uint32 LE - 固件总大小 (仅 seq=0 时有效)
* [7..n-4] payload 固件数据
* [n-4..n] crc 校验 (预留)
* [7..n] payload 固件数据
* 注: 已移除CRC字段(BLE链路层已有CRC校验)
*/
/**
* @brief GATT DATA_IN 特征访问回调,接收并处理 OTA 数据块。
@@ -419,187 +532,242 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
}
uint8_t buf[BLE_MTU_SIZE] = {0};
// 使用较大静态缓冲区:writeValueWithoutResponse 快速发送时,
// NimBLE 可能将多个写请求合并到同一个 mbuf 链中
// ★ static: NimBLE host task 栈只有 4096 字节,不能用栈分配
// ★ 4096 字节可容纳 8 个合并包(8×491=3928),避免截断丢包
static constexpr size_t BLE_OTA_BUF_SIZE = 4096;
static uint8_t buf[BLE_OTA_BUF_SIZE];
int recv_len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
if (recv_len < 12) {
DEBUG_WARN(TAG, "DATA_IN 数据太短: %d 字节 (最少 12)", recv_len);
if (recv_len < 8) {
DEBUG_WARN(TAG, "DATA_IN 数据太短: %d 字节 (最少 8: 7头+1数据)", recv_len);
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
// 解析包头
uint8_t cmd = buf[0];
uint16_t seq = (uint16_t)(buf[1] | (buf[2] << 8));
uint32_t total_size = (uint32_t)(buf[3] | (buf[4] << 8) |
(buf[5] << 16) | (buf[6] << 24));
// 循环处理 mbuf 中的所有完整数据包
size_t offset = 0;
while (offset + 7 <= (size_t)recv_len) {
// 解析包头
uint8_t cmd = buf[offset + 0];
uint16_t seq = (uint16_t)(buf[offset + 1] | (buf[offset + 2] << 8));
uint32_t total_size = (uint32_t)(buf[offset + 3] | (buf[offset + 4] << 8) |
(buf[offset + 5] << 16) | (buf[offset + 6] << 24));
size_t data_offset = 7;
size_t data_len = (size_t)recv_len - data_offset;
size_t data_offset = offset + 7;
// 尾 4 字节 CRC 预留,实际 payload = data_len - 4
if (data_len < 4) {
DEBUG_WARN(TAG, "DATA_IN 负载不足: data_len=%zu", data_len);
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
data_len -= 4; // 减去尾部 CRC 预留
// 根据包序号和固件总大小计算期望 payload,而非依赖 recv_len
// 这样即使 NimBLE mbuf 合并导致缓冲区截断,也不会误判包长度
uint32_t expected_payload = (total_size > 0)
? ((uint32_t)BLE_OTA_CHUNK_PAYLOAD < (total_size - (uint32_t)seq * (uint32_t)BLE_OTA_CHUNK_PAYLOAD)
? (uint32_t)BLE_OTA_CHUNK_PAYLOAD
: (total_size - (uint32_t)seq * (uint32_t)BLE_OTA_CHUNK_PAYLOAD))
: (uint32_t)BLE_OTA_CHUNK_PAYLOAD;
DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu",
cmd, seq, total_size, data_len);
size_t data_len = (size_t)expected_payload;
size_t remaining_after_header = (size_t)recv_len - data_offset;
// --- ESP32 自身 OTA:直接写 OTA 分区 ---
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
if (seq == 0) {
if (total_size == 0) {
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return BLE_ATT_ERR_UNLIKELY;
}
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
const esp_partition_t *check_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (check_partition == NULL || check_partition->address == boot_partition->address) {
check_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (check_partition == NULL) {
DEBUG_ERR(TAG, "找不到 ESP32 OTA 目标分区");
return BLE_ATT_ERR_UNLIKELY;
}
if (total_size > check_partition->size) {
DEBUG_ERR(TAG, "ESP32 固件太大: %" PRIu32 " > OTA分区 %u",
total_size, check_partition->size);
return BLE_ATT_ERR_UNLIKELY;
}
esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) {
return BLE_ATT_ERR_UNLIKELY;
// 如果缓冲区中实际可用数据不足,截断 data_len
if (remaining_after_header < expected_payload) {
if (remaining_after_header == 0) {
// 没有更多数据,退出循环
break;
}
// 缓冲区截断,使用实际可用数据
data_len = remaining_after_header;
}
esp_err_t err = ble_esp32_ota_write(&buf[data_offset], data_len);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
seq, esp_err_to_name(err));
return BLE_ATT_ERR_UNLIKELY;
}
// 当前包总长度: header(7) + payload(data_len)
size_t packet_len = 7 + data_len;
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
seq, (unsigned int)BleOta::instance().m_esp32Received);
DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu offset=%zu",
cmd, seq, total_size, data_len, offset);
// 尾包检测: data_len < 标准负载说明是最后一包(不满一个完整 payload)
bool isLastPacket = (data_len < BLE_OTA_CHUNK_PAYLOAD);
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
(total_size > 0 && BleOta::instance().m_esp32Received >= total_size) ||
isLastPacket) {
if (isLastPacket) {
DEBUG_INFO(TAG, "ESP32 尾包检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
seq, data_len, (unsigned int)BleOta::instance().m_esp32Received, total_size);
}
sendWindowAck(cmd, seq, (uint32_t)BleOta::instance().m_esp32Received);
}
if ((total_size > 0 && BleOta::instance().m_esp32Received >= total_size) || isLastPacket) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s",
(unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1,
isLastPacket ? " [尾包]" : "");
BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq;
if (!BleOta::instance().m_esp32RebootPending) {
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
NULL,
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
BleOta::instance().m_esp32RebootPending = false;
// --- ESP32 自身 OTA:直接写 OTA 分区 ---
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
if (seq == 0) {
if (total_size == 0) {
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启");
} else {
DEBUG_WARN(TAG, "ESP32 OTA 收尾任务已存在,忽略重复完成触发");
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
const esp_partition_t *check_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (check_partition == NULL || check_partition->address == boot_partition->address) {
check_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (check_partition == NULL) {
DEBUG_ERR(TAG, "找不到 ESP32 OTA 目标分区");
return BLE_ATT_ERR_UNLIKELY;
}
if (total_size > check_partition->size) {
DEBUG_ERR(TAG, "ESP32 固件太大: %" PRIu32 " > OTA分区 %u",
total_size, check_partition->size);
return BLE_ATT_ERR_UNLIKELY;
}
esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return BLE_ATT_ERR_OTA_BUSY;
}
return BLE_ATT_ERR_UNLIKELY;
}
}
esp_err_t err = ble_esp32_ota_write(&buf[data_offset], data_len);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
seq, esp_err_to_name(err));
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
ble_esp32_ota_cleanup_sha(); // 清理SHA状态
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
seq, (unsigned int)BleOta::instance().m_esp32Received);
// 尾包检测: 期望 payload < 标准负载说明是真正的最后一包
// 使用 expected_payload 而非 data_len,避免缓冲区截断导致误触发重启
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
(total_size > 0 && BleOta::instance().m_esp32Received >= total_size) ||
isLastPacket) {
if (isLastPacket) {
DEBUG_INFO(TAG, "ESP32 尾包检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
seq, data_len, (unsigned int)BleOta::instance().m_esp32Received, total_size);
}
sendWindowAck(cmd, seq, (uint32_t)BleOta::instance().m_esp32Received);
}
if ((total_size > 0 && BleOta::instance().m_esp32Received >= total_size) || isLastPacket) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s",
(unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1,
isLastPacket ? " [尾包]" : "");
BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq;
if (!BleOta::instance().m_esp32RebootPending) {
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
NULL,
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32RebootPending = false;
ble_esp32_ota_cleanup_sha(); // 清理SHA状态
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启");
} else {
DEBUG_WARN(TAG, "ESP32 OTA 收尾任务已存在,忽略重复完成触发");
}
}
}
return 0;
}
// --- GD32 OTA:首个分块时创建存储元数据 ---
else if (cmd == BLE_OTA_CMD_GD32_DATA) {
if (seq == 0 && total_size > 0) {
uint32_t rounded_size = (uint32_t)(((total_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
if (total_size == 0U || total_size > OTA_MAX_FIRMWARE_SIZE || rounded_size > OTA_MAX_FIRMWARE_SIZE) {
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " rounded48=%" PRIu32 " max=%zu",
total_size, rounded_size, OTA_MAX_FIRMWARE_SIZE);
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
return BLE_ATT_ERR_UNLIKELY;
}
// --- GD32 OTA:首个分块时创建存储元数据 ---
if (seq == 0 && total_size > 0) {
uint32_t rounded_size = (uint32_t)(((total_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
if (total_size == 0U || total_size > OTA_MAX_FIRMWARE_SIZE || rounded_size > OTA_MAX_FIRMWARE_SIZE) {
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " rounded48=%" PRIu32 " max=%zu",
total_size, rounded_size, OTA_MAX_FIRMWARE_SIZE);
return BLE_ATT_ERR_UNLIKELY;
ota_meta_t meta = {};
meta.total_size = total_size;
meta.stored_size = 0;
meta.last_seq = 0;
meta.chunk_count = (total_size + expected_payload - 1U) / expected_payload;
esp_err_t err = OtaRelay::metaSave(&meta);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "元数据保存失败");
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 " chunk_count=%" PRIu32 ", payload=%zu",
meta.total_size, meta.chunk_count, data_len);
}
ota_meta_t meta = {};
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
return BLE_ATT_ERR_UNLIKELY;
}
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
DEBUG_ERR(TAG, "GD32 分包序号不连续: seq=%u last_seq=%" PRIu32,
seq, meta.last_seq);
return BLE_ATT_ERR_UNLIKELY;
}
// --- GD32 写入 Flash ---
uint32_t flash_offset = meta.stored_size;
if (flash_offset + (uint32_t)data_len > meta.total_size) {
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
flash_offset, data_len, meta.total_size);
return BLE_ATT_ERR_UNLIKELY;
}
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));
return BLE_ATT_ERR_UNLIKELY;
}
uint32_t stored_size = flash_offset + (uint32_t)data_len;
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) ||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD)) {
sendWindowAck(cmd, seq, stored_size);
}
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
seq, flash_offset, stored_size);
// --- 检查 GD32 固件是否接收完毕 ---
if (OtaRelay::metaLoad(&meta) == ESP_OK &&
meta.total_size > 0 &&
meta.stored_size >= meta.total_size) {
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
meta.stored_size, meta.total_size, seq + 1);
// 计算并存储 SHA-256 校验和
esp_err_t sha_err = OtaRelay::metaFinalizeChecksum();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
}
}
} else {
DEBUG_WARN(TAG, "DATA_IN 未知 cmd: 0x%02X offset=%zu", cmd, offset);
}
ota_meta_t meta = {};
meta.total_size = total_size;
meta.stored_size = 0;
meta.last_seq = 0;
meta.chunk_count = (total_size + (uint32_t)data_len - 1U) / (uint32_t)data_len;
// 推进到下一个包
offset += packet_len;
esp_err_t err = OtaRelay::metaSave(&meta);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "元数据保存失败");
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 " chunk_count=%" PRIu32 ", payload=%zu",
meta.total_size, meta.chunk_count, data_len);
}
ota_meta_t meta = {};
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
return BLE_ATT_ERR_UNLIKELY;
}
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
DEBUG_ERR(TAG, "GD32 分包序号不连续: seq=%u last_seq=%" PRIu32,
seq, meta.last_seq);
return BLE_ATT_ERR_UNLIKELY;
}
// --- GD32 写入 Flash ---
// 按已存储字节数顺序追加,兼容更大的 BLE payload,避免把步长硬编码死。
uint32_t flash_offset = meta.stored_size;
if (flash_offset + (uint32_t)data_len > meta.total_size) {
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
flash_offset, data_len, meta.total_size);
return BLE_ATT_ERR_UNLIKELY;
}
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));
return BLE_ATT_ERR_UNLIKELY;
}
uint32_t stored_size = flash_offset + (uint32_t)data_len;
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) ||
(data_len < BLE_OTA_CHUNK_PAYLOAD)) {
sendWindowAck(cmd, seq, stored_size);
}
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
seq, flash_offset, stored_size);
// --- 检查 GD32 固件是否接收完毕 ---
if (OtaRelay::metaLoad(&meta) == ESP_OK &&
meta.total_size > 0 &&
meta.stored_size >= meta.total_size) {
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
meta.stored_size, meta.total_size, seq + 1);
// 检查剩余数据是否足够组成一个完整的包头
if (offset + 7 > (size_t)recv_len) break;
}
return 0;
@@ -1093,4 +1261,4 @@ void BleOta::taskEntry(void *pvParameters)
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
}
+8
View File
@@ -9,6 +9,9 @@
#include "esp_err.h"
#include "esp_ota_ops.h"
// 前向声明 mbedtls_sha256_context
typedef struct mbedtls_sha256_context mbedtls_sha256_context;
class BleOta {
public:
static BleOta& instance();
@@ -31,6 +34,11 @@ public:
bool m_esp32RebootPending = false;
uint8_t m_esp32LastCmd = 0;
uint16_t m_esp32LastSeq = 0xFFFFU;
// SHA-256 哈希校验
mbedtls_sha256_context *m_sha256Ctx = nullptr; // SHA-256计算上下文
uint8_t m_esp32Sha256[32] = {0}; // 计算得到的SHA-256哈希值
bool m_esp32HashComputed = false; // 是否已计算哈希
};
#endif // BLE_OTA_H
+417 -103
View File
@@ -21,7 +21,12 @@
#include "esp_event.h"
#include "esp_mac.h"
// SHA-256 哈希计算
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
#include "mbedtls/private/sha256.h"
#include "app_config.h"
#include "ota_mutex.h"
#include "wifi_ota.h"
#include "ota_relay.h"
#include "health_monitor.h"
@@ -74,6 +79,69 @@ static esp_err_t http_options_handler(httpd_req_t *req)
return ESP_OK;
}
// ----------------------------------------
// STA 后台重连任务(避免在系统事件回调中调用 esp_wifi_connect 死锁)
// ----------------------------------------
/**
* @brief 独立 FreeRTOS 任务,处理 STA 连接/重连的退避延迟和 esp_wifi_connect() 调用。
* 事件回调仅通过 xTaskNotifyGive 唤醒本任务,不直接在系统事件任务中阻塞或调用 WiFi API。
*/
void WifiOta::reconnectTask(void* pvParameters)
{
(void)pvParameters;
while (1) {
// 等待事件回调通知(阻塞,不消耗 CPU)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
WifiOta& self = WifiOta::instance();
// 已连接或已放弃,无需处理
if (self.getStaConnected() || self.getStaRetryExhausted()) {
continue;
}
self.stateLock();
// 达到最大重试次数
if (self.getStaRetryCount() >= WIFI_STA_MAX_RETRY) {
DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", self.getStaRetryCount());
self.setStaRetryExhausted(true);
self.stateUnlock();
continue;
}
self.setStaRetryCount(self.getStaRetryCount() + 1);
int retryCount = self.getStaRetryCount();
// 计算退避延迟:冷启动前 3 次递增退避,其余固定 1s
int delay_ms = 0;
if (self.m_coldBoot && retryCount <= 3) {
delay_ms = 2000 * retryCount; // 2s, 4s, 6s
DEBUG_INFO("WIFI", "冷启动退避重连第 %d 次 (等 %dms)",
retryCount, delay_ms);
} else if (retryCount > 1) {
delay_ms = 1000; // 普通重连 1s 延迟
DEBUG_INFO("WIFI", "重连第 %d 次...", retryCount);
} else {
DEBUG_INFO("WIFI", "首次连接...");
}
self.stateUnlock();
if (delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
// 延迟后再次检查状态,防止期间已连接或已放弃
if (!self.getStaConnected() && !self.getStaRetryExhausted()) {
esp_err_t err = esp_wifi_connect();
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "esp_wifi_connect() 失败: %s", esp_err_to_name(err));
}
}
}
}
// ----------------------------------------
// WiFi/IP 事件回调(事件驱动连接)
// ----------------------------------------
@@ -87,45 +155,49 @@ static esp_err_t http_options_handler(httpd_req_t *req)
void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
WifiOta& inst = WifiOta::instance();
if (event_base == WIFI_EVENT) {
switch (event_id) {
case WIFI_EVENT_STA_START:
DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
esp_wifi_connect();
inst.stateLock();
inst.setStaRetryCount(0);
inst.setStaRetryExhausted(false);
inst.stateUnlock();
if (inst.m_reconnectTask) {
xTaskNotifyGive(inst.m_reconnectTask);
}
break;
case WIFI_EVENT_STA_CONNECTED:
DEBUG_INFO("WIFI", "STA 已关联到 AP");
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);
inst.stateLock();
inst.setStaConnected(true);
inst.setStaRetryCount(0);
inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志
inst.stateUnlock();
if (inst.m_staConnectedSem) {
xSemaphoreGive(inst.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, WifiOta::instance().m_staRetryCount);
WifiOta::instance().m_staConnected = false;
inst.stateLock();
inst.setStaConnected(false);
bool exhausted = inst.getStaRetryExhausted();
int retryCount = inst.getStaRetryCount();
inst.stateUnlock();
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 (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)...",
WifiOta::instance().m_staRetryCount, disconn->reason, delay_ms);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
} else {
DEBUG_INFO("WIFI", "重连第 %d 次...", WifiOta::instance().m_staRetryCount);
if (!exhausted) {
DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d",
disconn->reason, retryCount);
if (inst.m_reconnectTask) {
xTaskNotifyGive(inst.m_reconnectTask);
}
esp_wifi_connect();
} else {
DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", WifiOta::instance().m_staRetryCount);
WifiOta::instance().m_staRetryExhausted = true;
DEBUG_WARN("WIFI", "STA 断开, 重试次数已耗尽");
}
break;
}
@@ -225,8 +297,8 @@ esp_err_t WifiOta::initSta()
DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_STA;
WifiOta::instance().m_staRetryCount = 0;
WifiOta::instance().m_staRetryExhausted = false;
WifiOta::instance().setStaRetryCount(0);
WifiOta::instance().setStaRetryExhausted(false);
// ★ 设置国家代码:中国,允许信道 1-13
// ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
@@ -307,18 +379,18 @@ esp_err_t WifiOta::waitStaConnected(char *ip_str, size_t ip_len)
}
DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
}
WifiOta::instance().m_staConnected = true;
WifiOta::instance().setStaConnected(true);
return ESP_OK;
}
// ★ 重试已耗尽,不再等待
if (WifiOta::instance().m_staRetryExhausted) {
DEBUG_ERR("WIFI", "STA 重试已耗尽 (%d 次),立即切换 AP", WifiOta::instance().m_staRetryCount);
if (WifiOta::instance().getStaRetryExhausted()) {
DEBUG_ERR("WIFI", "STA 重试已耗尽 (%d 次),立即切换 AP", WifiOta::instance().getStaRetryCount());
return ESP_ERR_TIMEOUT;
}
}
DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, WifiOta::instance().m_staRetryCount);
DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, WifiOta::instance().getStaRetryCount());
return ESP_ERR_TIMEOUT;
}
@@ -355,6 +427,18 @@ void WifiOta::setStaticIp()
// ----------------------------------------
// ESP32 OTA 内部函数
/**
* @brief 清理 WiFi OTA 的 SHA-256 上下文和哈希状态
*/
static void wifi_esp32_ota_cleanup_sha()
{
WifiOta::instance().m_esp32HashComputed = false;
memset(WifiOta::instance().m_esp32Sha256, 0, sizeof(WifiOta::instance().m_esp32Sha256));
WifiOta::instance().m_hasFrontendSha256 = false;
memset(WifiOta::instance().m_frontendSha256, 0, sizeof(WifiOta::instance().m_frontendSha256));
}
/**
* @brief 初始化 ESP32 自身的 OTA 写入流程。
* @param total_size
@@ -362,6 +446,12 @@ void WifiOta::setStaticIp()
*/
static esp_err_t esp32_ota_begin(size_t total_size)
{
// ★ 全局互斥:检查 ESP32 OTA 是否已被其他通道占用
if (!esp32_ota_mutex_acquire(1000)) {
DEBUG_ERR("OTA", "ESP32 OTA 被其他通道占用,拒绝 WiFi OTA begin");
return ESP_ERR_INVALID_STATE;
}
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx",
boot_partition->label, boot_partition->size, boot_partition->address);
@@ -376,6 +466,7 @@ static esp_err_t esp32_ota_begin(size_t total_size)
if (WifiOta::instance().m_esp32UpdatePartition == NULL) {
DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
esp32_ota_mutex_release();
return ESP_FAIL;
}
@@ -387,6 +478,7 @@ static esp_err_t esp32_ota_begin(size_t total_size)
if (total_size > WifiOta::instance().m_esp32UpdatePartition->size) {
DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
total_size, WifiOta::instance().m_esp32UpdatePartition->size);
esp32_ota_mutex_release();
return ESP_FAIL;
}
@@ -394,49 +486,199 @@ static esp_err_t esp32_ota_begin(size_t total_size)
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
WifiOta::instance().m_esp32UpdatePartition = NULL;
esp32_ota_mutex_release();
return err;
}
WifiOta::instance().m_esp32Received = 0;
WifiOta::instance().m_esp32OtaActive = true;
// 清理旧状态(SHA-256 将在重启前从分区读回计算,上传期间不再计算)
wifi_esp32_ota_cleanup_sha();
DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", WifiOta::instance().m_esp32UpdatePartition->label);
return ESP_OK;
}
/**
* @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
* verify_only=true: 仅校验 SHA-256,不设置启动分区,不重启
* verify_only=false或缺省: 完整流程(校验 + 设置启动分区 + 重启)
* @param req
* @return
*/
static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
{
(void)req;
http_set_cors_headers(req);
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 字节, 设置启动分区...", WifiOta::instance().m_esp32Received);
// 读取请求体,获取前端 SHA-256 和 verify_only 标志
char frontend_sha_hex[65] = {0};
bool has_frontend_sha = false;
bool verify_only = false;
if (req->content_len > 0 && req->content_len < 256) {
char buf[256] = {0};
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret > 0) {
buf[ret] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root) {
cJSON *sha_item = cJSON_GetObjectItem(root, "sha256");
if (sha_item && cJSON_IsString(sha_item)) {
const char *sha_str = cJSON_GetStringValue(sha_item);
if (sha_str && strlen(sha_str) == 64) {
strncpy(frontend_sha_hex, sha_str, 64);
frontend_sha_hex[64] = '\0';
has_frontend_sha = true;
strncpy(WifiOta::instance().m_frontendSha256, sha_str, 64);
WifiOta::instance().m_frontendSha256[64] = '\0';
WifiOta::instance().m_hasFrontendSha256 = true;
DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex);
}
}
cJSON *verify_item = cJSON_GetObjectItem(root, "verify_only");
if (verify_item && cJSON_IsBool(verify_item)) {
verify_only = cJSON_IsTrue(verify_item);
}
cJSON_Delete(root);
}
}
}
esp_err_t err = esp_ota_end(WifiOta::instance().m_esp32OtaHandle);
if (verify_only) {
DEBUG_INFO("OTA", "ESP32 OTA verify-only mode, %u bytes", (unsigned)WifiOta::instance().m_esp32Received);
} else {
DEBUG_INFO("OTA", "ESP32 OTA complete, %u bytes, setting boot partition...", (unsigned)WifiOta::instance().m_esp32Received);
esp_err_t err = esp_ota_end(WifiOta::instance().m_esp32OtaHandle);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_end failed: %s", esp_err_to_name(err));
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
}
}
// ── SHA-256 验证:从 OTA 分区读回并计算哈希 ──
if (has_frontend_sha && WifiOta::instance().m_esp32UpdatePartition != NULL) {
DEBUG_INFO("OTA", "Verifying: reading OTA partition for SHA-256...");
const esp_partition_t *part = WifiOta::instance().m_esp32UpdatePartition;
mbedtls_sha256_context verify_ctx;
mbedtls_sha256_init(&verify_ctx);
mbedtls_sha256_starts(&verify_ctx, 0);
uint8_t *read_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!read_buf) {
DEBUG_ERR("OTA", "SHA-256 verify: malloc failed");
mbedtls_sha256_free(&verify_ctx);
} else {
size_t offset = 0;
while (offset < WifiOta::instance().m_esp32Received) {
size_t to_read = WifiOta::instance().m_esp32Received - offset;
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
esp_err_t rd_err = esp_partition_read(part, offset, read_buf, to_read);
if (rd_err != ESP_OK) {
DEBUG_ERR("OTA", "SHA-256 verify: partition read failed at offset %u", (unsigned int)offset);
break;
}
mbedtls_sha256_update(&verify_ctx, read_buf, to_read);
offset += to_read;
}
free(read_buf);
mbedtls_sha256_finish(&verify_ctx, WifiOta::instance().m_esp32Sha256);
mbedtls_sha256_free(&verify_ctx);
WifiOta::instance().m_esp32HashComputed = true;
char device_sha_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&device_sha_hex[i * 2], sizeof(device_sha_hex) - i * 2, "%02x", WifiOta::instance().m_esp32Sha256[i]);
}
DEBUG_INFO("OTA", "Device SHA-256: %s", device_sha_hex);
bool match = true;
for (int i = 0; i < 64; i++) {
if (tolower((unsigned char)frontend_sha_hex[i]) != tolower((unsigned char)device_sha_hex[i])) {
match = false;
break;
}
}
if (match) {
DEBUG_INFO("OTA", "SHA-256 verified OK");
if (verify_only) {
char ok_resp[256] = {0};
snprintf(ok_resp, sizeof(ok_resp),
"{\"status\": \"ok\", \"message\": \"SHA-256 verified\", \"sha256\": \"%s\"}",
device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, ok_resp, strlen(ok_resp));
return ESP_OK;
}
} else {
DEBUG_ERR("OTA", "SHA-256 mismatch! frontend=%s device=%s, abort", frontend_sha_hex, device_sha_hex);
if (!verify_only) {
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle);
}
wifi_esp32_ota_cleanup_sha();
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
WifiOta::instance().m_esp32UpdatePartition = NULL;
WifiOta::instance().m_esp32Received = 0;
char err_resp[256] = {0};
snprintf(err_resp, sizeof(err_resp),
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", \"frontend\": \"%s\", \"device\": \"%s\"}",
frontend_sha_hex, device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, err_resp, strlen(err_resp));
return ESP_FAIL;
}
}
}
// ── 非verify_only模式:设置启动分区并重启 ──
if (verify_only) {
// 没有前端SHA或验证被跳过,在verify_only下也返回ok
char ok_resp[128] = {0};
snprintf(ok_resp, sizeof(ok_resp),
"{\"status\": \"ok\", \"message\": \"verify skipped, no SHA-256 provided\"}");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, ok_resp, strlen(ok_resp));
return ESP_OK;
}
esp_err_t err = esp_ota_set_boot_partition(WifiOta::instance().m_esp32UpdatePartition);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err));
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(WifiOta::instance().m_esp32UpdatePartition);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
DEBUG_ERR("OTA", "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
}
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
DEBUG_INFO("OTA", "ESP32 OTA finished, rebooting...");
char resp[256] = {0};
if (WifiOta::instance().m_esp32HashComputed) {
char device_sha_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&device_sha_hex[i * 2], sizeof(device_sha_hex) - i * 2, "%02x", WifiOta::instance().m_esp32Sha256[i]);
}
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\", \"sha256\": \"%s\"}",
device_sha_hex);
} else {
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}");
}
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
@@ -580,14 +822,22 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
esp_err_t err = esp32_ota_begin(total_size);
if (err != ESP_OK) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
if (err == ESP_ERR_INVALID_STATE) {
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
} else {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
}
return ESP_FAIL;
}
// 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
uint8_t *fw_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!fw_buf) {
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
return ESP_FAIL;
}
@@ -601,15 +851,34 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
if (ret <= 0) break;
if (ret <= 0) {
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret);
free(fw_buf);
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
return ESP_FAIL;
}
err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, fw_buf, (size_t)ret);
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "OTA write failed");
break;
free(fw_buf);
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
return ESP_FAIL;
}
total_firmware_written += ret;
total_received += ret;
WifiOta::instance().m_esp32Received += ret;
if (total_size > 0) {
int percent = (int)((total_firmware_written * 100) / total_size);
@@ -645,12 +914,14 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
return ESP_FAIL;
}
if (OtaRelay::relayState != RELAY_IDLE) {
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::relayState);
if (OtaRelay::getState() != RELAY_IDLE) {
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
OtaRelay::instance().reset();
}
WifiOta::instance().m_gd32StorageDone = false;
WifiOta::instance().m_relayTriggered = false;
WifiOta::instance().stateLock();
WifiOta::instance().setGd32StorageDone(false);
WifiOta::instance().setRelayTriggered(false);
WifiOta::instance().stateUnlock();
const esp_partition_t *gd32_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
@@ -761,8 +1032,16 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
ota_meta_t final_meta;
OtaRelay::metaLoad(&final_meta);
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
WifiOta::instance().m_gd32StorageDone = true;
DEBUG_INFO("WIFI", "GD32 firmware fully stored, auto-triggering relay");
WifiOta::instance().stateLock();
WifiOta::instance().setGd32StorageDone(true);
WifiOta::instance().stateUnlock();
DEBUG_INFO("WIFI", "GD32 firmware fully stored, computing SHA-256...");
// 计算并存储 SHA-256 校验和
esp_err_t sha_err = OtaRelay::metaFinalizeChecksum();
if (sha_err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
}
DEBUG_INFO("WIFI", "auto-triggering relay");
// 自动触发中继
if (OtaRelay::dispatchQueue) {
uint32_t cmd = 1; // 1 = start relay
@@ -794,14 +1073,18 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
return ESP_FAIL;
}
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_APP_VERIFY && OtaRelay::relayState != RELAY_ERROR) {
relay_state_t curState = OtaRelay::getState();
WifiOta::instance().stateLock();
if (WifiOta::instance().getRelayTriggered() && curState != RELAY_IDLE &&
curState != RELAY_DONE && curState != RELAY_APP_VERIFY && curState != RELAY_ERROR) {
WifiOta::instance().stateUnlock();
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered");
return ESP_FAIL;
}
/* 上一轮中继已结束/失败/空闲,重置标志允许重新触发 */
WifiOta::instance().m_relayTriggered = false;
WifiOta::instance().m_gd32StorageDone = false;
WifiOta::instance().setRelayTriggered(false);
WifiOta::instance().setGd32StorageDone(false);
WifiOta::instance().stateUnlock();
if (!OtaRelay::dispatchQueue) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay queue not ready");
@@ -809,7 +1092,7 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
}
WifiOta::instance().signalRelay();
if (!WifiOta::instance().m_relayTriggered) {
if (!WifiOta::instance().getRelayTriggered()) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay trigger failed");
return ESP_FAIL;
}
@@ -840,18 +1123,20 @@ 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,
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
OtaRelay::relayState,
OtaRelay::gd32AppAlive ? "true" : "false");
WifiOta::instance().getGd32StorageDone() ? "true" : "false",
OtaRelay::getState(),
OtaRelay::getGd32AppAlive() ? "true" : "false");
gd32_version_t snapVer = {};
OtaRelay::getGd32Version(&snapVer);
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s",
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
OtaRelay::gd32AppAlive ? "true" : "false",
OtaRelay::gd32Version.app_major, OtaRelay::gd32Version.app_minor,
OtaRelay::gd32Version.app_patch, OtaRelay::gd32Version.app_build,
OtaRelay::gd32Version.boot_major, OtaRelay::gd32Version.boot_minor,
OtaRelay::gd32Version.boot_patch, OtaRelay::gd32Version.boot_build);
WifiOta::instance().getGd32StorageDone() ? "true" : "false",
OtaRelay::getGd32AppAlive() ? "true" : "false",
snapVer.app_major, snapVer.app_minor,
snapVer.app_patch, snapVer.app_build,
snapVer.boot_major, snapVer.boot_minor,
snapVer.boot_patch, snapVer.boot_build);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
@@ -964,7 +1249,6 @@ static esp_err_t can_send_handler(httpd_req_t *req)
if (tx_dlc == 0 && dlc > 0) {
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
}
cJSON_Delete(root);
// 发送 CAN 帧到 GD32
@@ -1051,7 +1335,6 @@ static esp_err_t spi_send_handler(httpd_req_t *req)
DEBUG_INFO("SPI", "WEB SPI: type=0x%02X, payload=%zu 字节",
frame_type, payload_len);
cJSON_Delete(root);
char resp[1024];
@@ -1114,7 +1397,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 (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().m_staConnected) {
if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().getStaConnected()) {
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) {
@@ -1130,7 +1413,7 @@ static esp_err_t wifi_status_handler(httpd_req_t *req)
"{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_AP ? "AP" : "STA",
ip_str, rssi,
(WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA) ? (WifiOta::instance().m_staConnected ? "true" : "false") : "N/A");
(WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA) ? (WifiOta::instance().getStaConnected() ? "true" : "false") : "N/A");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
@@ -1144,7 +1427,7 @@ static esp_err_t wifi_status_handler(httpd_req_t *req)
static esp_err_t version_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
gd32_version_t gv = OtaRelay::gd32Version;
gd32_version_t gv = {}; OtaRelay::getGd32Version(&gv);
char resp[512];
int len = snprintf(resp, sizeof(resp),
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", "
@@ -1152,7 +1435,7 @@ static esp_err_t version_handler(httpd_req_t *req)
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH,
gv.app_major, gv.app_minor, gv.app_patch, gv.app_build,
gv.boot_major, gv.boot_minor, gv.boot_patch, gv.boot_build,
OtaRelay::gd32AppAlive ? "true" : "false");
OtaRelay::getGd32AppAlive() ? "true" : "false");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
return ESP_OK;
@@ -1170,7 +1453,7 @@ static esp_err_t system_status_handler(httpd_req_t *req)
int8_t rssi = -127;
bool connected = false;
if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().m_staConnected) {
if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().getStaConnected()) {
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) {
@@ -1182,11 +1465,13 @@ static esp_err_t system_status_handler(httpd_req_t *req)
}
connected = true;
}
gd32_version_t pingVer = {};
OtaRelay::getGd32Version(&pingVer);
/* 若尚无 GD32 版本信息且 OTA 中继空闲,尝试一次 SPI PING 获取 */
if (OtaRelay::gd32Version.app_major == 0 &&
OtaRelay::gd32Version.boot_major == 0 &&
OtaRelay::relayState == RELAY_IDLE) {
if (pingVer.app_major == 0 &&
pingVer.boot_major == 0 &&
OtaRelay::getState() == RELAY_IDLE) {
static uint32_t last_ping_ms = 0;
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
if (now_ms - last_ping_ms > 5000) { /* 5 秒节流 */
@@ -1194,10 +1479,10 @@ static esp_err_t system_status_handler(httpd_req_t *req)
uint32_t uptime = 0;
gd32_version_t ver = {};
if (SpiMaster::instance().appPing(500, &uptime, &ver) == ESP_OK) {
OtaRelay::gd32AppAlive = true;
OtaRelay::gd32Version = ver;
OtaRelay::setGd32AppAlive(true);
OtaRelay::setGd32Version(ver);
} else {
OtaRelay::gd32AppAlive = false;
OtaRelay::setGd32AppAlive(false);
}
}
}
@@ -1206,6 +1491,8 @@ static esp_err_t system_status_handler(httpd_req_t *req)
memset(&meta, 0, sizeof(meta));
OtaRelay::metaLoad(&meta);
gd32_version_t snapVer2 = {};
OtaRelay::getGd32Version(&snapVer2);
char resp[1024];
int len = snprintf(resp, sizeof(resp),
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%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, \"gd32_app_alive\": %s, \"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}}",
@@ -1220,15 +1507,15 @@ static esp_err_t system_status_handler(httpd_req_t *req)
connected ? "true" : "false",
(unsigned long)meta.total_size,
(unsigned long)meta.stored_size,
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
OtaRelay::relayState,
(unsigned long)OtaRelay::relaySeq,
(unsigned long)OtaRelay::relayTotalPackets,
OtaRelay::gd32AppAlive ? "true" : "false",
OtaRelay::gd32Version.app_major, OtaRelay::gd32Version.app_minor,
OtaRelay::gd32Version.app_patch, OtaRelay::gd32Version.app_build,
OtaRelay::gd32Version.boot_major, OtaRelay::gd32Version.boot_minor,
OtaRelay::gd32Version.boot_patch, OtaRelay::gd32Version.boot_build);
WifiOta::instance().getGd32StorageDone() ? "true" : "false",
OtaRelay::getState(),
(unsigned long)OtaRelay::getRelaySeq(),
(unsigned long)OtaRelay::getRelayTotalPackets(),
OtaRelay::getGd32AppAlive() ? "true" : "false",
snapVer2.app_major, snapVer2.app_minor,
snapVer2.app_patch, snapVer2.app_build,
snapVer2.boot_major, snapVer2.boot_minor,
snapVer2.boot_patch, snapVer2.boot_build);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
@@ -1247,6 +1534,8 @@ esp_err_t WifiOta::httpServerStart()
config.server_port = HTTP_SERVER_PORT;
config.stack_size = HTTP_SERVER_STACK_SIZE;
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
config.max_open_sockets = 4; // LWIP_MAX_SOCKETS 默认 7,HTTP 内部占 3,最多 4
config.lru_purge_enable = true; // socket 满时自动清理最久未用的连接
config.recv_wait_timeout = 30; // GD32 上传前会擦除 Flash,5 秒默认值过紧
config.send_wait_timeout = 30;
@@ -1264,7 +1553,7 @@ esp_err_t WifiOta::httpServerStart()
ota_meta_t boot_meta;
if (OtaRelay::metaLoad(&boot_meta) == ESP_OK &&
boot_meta.total_size > 0 && boot_meta.stored_size >= boot_meta.total_size) {
WifiOta::instance().m_gd32StorageDone = true;
WifiOta::instance().setGd32StorageDone(true);
DEBUG_INFO("WIFI", "从 NVS 恢复: 固件已就绪 (%u bytes)", boot_meta.total_size);
}
}
@@ -1501,16 +1790,19 @@ void WifiOta::signalRelay()
(unsigned long)meta.stored_size, (unsigned long)meta.total_size);
return;
}
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_APP_VERIFY && OtaRelay::relayState != RELAY_ERROR) {
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", OtaRelay::relayState);
relay_state_t st = OtaRelay::getState();
WifiOta::instance().stateLock();
if (WifiOta::instance().getRelayTriggered() && st != RELAY_IDLE &&
st != RELAY_DONE && st != RELAY_APP_VERIFY && st != RELAY_ERROR) {
WifiOta::instance().stateUnlock();
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", st);
return;
}
/* 上一轮中继已结束/失败/空闲,重置标志 */
WifiOta::instance().m_relayTriggered = false;
WifiOta::instance().m_gd32StorageDone = false;
WifiOta::instance().m_relayTriggered = true;
WifiOta::instance().setRelayTriggered(false);
WifiOta::instance().setGd32StorageDone(false);
WifiOta::instance().setRelayTriggered(true);
WifiOta::instance().stateUnlock();
if (OtaRelay::dispatchQueue) {
uint32_t msg = 1;
@@ -1519,11 +1811,11 @@ void WifiOta::signalRelay()
DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
} else {
DEBUG_ERR("WIFI", "OTA 调度信号发送失败");
WifiOta::instance().m_relayTriggered = false;
WifiOta::instance().setRelayTriggered(false);
}
} else {
DEBUG_ERR("WIFI", "OTA 中继队列尚未就绪");
WifiOta::instance().m_relayTriggered = false;
WifiOta::instance().setRelayTriggered(false);
}
}
@@ -1542,7 +1834,7 @@ ota_wifi_mode_t WifiOta::getMode()
*/
int8_t WifiOta::getRssi()
{
if (WifiOta::instance().m_wifiMode != OTA_WIFI_MODE_STA || !WifiOta::instance().m_staConnected) {
if (WifiOta::instance().m_wifiMode != OTA_WIFI_MODE_STA || !WifiOta::instance().getStaConnected()) {
return -127;
}
wifi_ap_record_t ap_info;
@@ -1558,7 +1850,7 @@ int8_t WifiOta::getRssi()
*/
bool WifiOta::isConnected()
{
return WifiOta::instance().m_staConnected;
return WifiOta::instance().getStaConnected();
}
// ----------------------------------------
@@ -1631,12 +1923,36 @@ bool WifiOta::earlyInit()
}
}
// ★ 创建状态互斥锁(保护 WiFi 连接状态、OTA 中继标志等跨任务共享变量)
if (WifiOta::instance().getStateMutex() == NULL) {
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
if (mtx) {
// 通过临时获取再赋值(私有成员在同类内可访问)
WifiOta::instance().m_stateMutex = mtx;
DEBUG_INFO("WIFI", "状态互斥锁已创建");
} else {
DEBUG_ERR("WIFI", "创建状态互斥锁失败");
}
}
WifiOta::instance().m_staConnectedSem = xSemaphoreCreateBinary();
WifiOta::instance().m_staGotIpSem = xSemaphoreCreateBinary();
if (!WifiOta::instance().m_staConnectedSem || !WifiOta::instance().m_staGotIpSem) {
DEBUG_ERR("WIFI", "创建信号量失败");
}
// 创建后台重连任务:避免在系统事件任务中调用 esp_wifi_connect()/vTaskDelay 导致死锁
if (WifiOta::instance().m_reconnectTask == nullptr) {
BaseType_t ret = xTaskCreate(&WifiOta::reconnectTask, "wifi_reconnect",
4096, NULL, 5,
&WifiOta::instance().m_reconnectTask);
if (ret != pdPASS) {
DEBUG_ERR("WIFI", "创建重连任务失败");
} else {
DEBUG_INFO("WIFI", "WiFi 重连任务已创建");
}
}
// 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
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));
@@ -1776,5 +2092,3 @@ void WifiOta::taskEntry(void *pvParameters)
}
}
+38 -7
View File
@@ -1,4 +1,4 @@
/*
/*
* wifi_ota.h - WiFi OTA 服务器
* T4.1 - T4.5
*
@@ -11,7 +11,9 @@
#include "esp_ota_ops.h"
#include "esp_http_server.h"
#include "esp_event.h"
#include <atomic>
// SHA-256 验证所需类型前向声明
typedef enum {
OTA_WIFI_MODE_AP,
OTA_WIFI_MODE_STA
@@ -29,28 +31,57 @@ public:
static int8_t getRssi();
static bool isConnected();
// 以下状态为 public,供 C 回调函数访问
// ── WiFi 连接状态(WiFi 事件回调 / HTTP / reconnect 任务共享,需 mutex 保护)──
bool getStaConnected() const { return m_staConnected.load(); }
void setStaConnected(bool v) { m_staConnected.store(v); }
int getStaRetryCount() const { return m_staRetryCount.load(); }
void setStaRetryCount(int v) { m_staRetryCount.store(v); }
bool getStaRetryExhausted() const { return m_staRetryExhausted.load(); }
void setStaRetryExhausted(bool v) { m_staRetryExhausted.store(v); }
bool getGd32StorageDone() const { return m_gd32StorageDone.load(); }
void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); }
bool getRelayTriggered() const { return m_relayTriggered.load(); }
void setRelayTriggered(bool v) { m_relayTriggered.store(v); }
// ── 状态互斥锁(保护复合读-改-写操作)──
SemaphoreHandle_t getStateMutex() const { return m_stateMutex; }
void stateLock() { if (m_stateMutex) xSemaphoreTake(m_stateMutex, portMAX_DELAY); }
void stateUnlock() { if (m_stateMutex) xSemaphoreGive(m_stateMutex); }
// ── 初始化/运行时状态(仅 OTA 任务访问,无需额外锁)──
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;
TaskHandle_t m_reconnectTask = nullptr;
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;
// ── SHA-256 哈希校验 ──
uint8_t m_esp32Sha256[32] = {0}; // 重启前验证计算的SHA-256哈希值
bool m_esp32HashComputed = false; // 是否已计算哈希
char m_frontendSha256[65] = {0}; // 前端提供的SHA-256(上传时存储)
bool m_hasFrontendSha256 = false; // 前端是否提供了SHA-256
private:
// ── 跨任务共享状态(atomic + m_stateMutex 保护)──
std::atomic<bool> m_staConnected{false};
std::atomic<int> m_staRetryCount{0};
std::atomic<bool> m_staRetryExhausted{false};
std::atomic<bool> m_gd32StorageDone{false};
std::atomic<bool> m_relayTriggered{false};
SemaphoreHandle_t m_stateMutex = NULL;
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);
static void reconnectTask(void *pvParameters);
esp_err_t httpServerStart();
esp_err_t waitStaConnected(char *ip_str, size_t ip_len);
};
+74 -19
View File
@@ -10,6 +10,7 @@
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/spi_master.h"
#include "soc/spi_struct.h"
#include "esp_err.h"
@@ -32,7 +33,8 @@ SpiMaster& SpiMaster::instance()
// 内部 SPI 传输
// ============================================================
esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len)
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms)
{
if (m_spiDevice == NULL) {
return ESP_ERR_INVALID_STATE;
@@ -75,21 +77,33 @@ esp_err_t SpiMaster::transferClocked(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(m_spiDevice, &t);
TickType_t ticks = (timeout_ms == 0U) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
esp_err_t ret = spi_device_queue_trans(m_spiDevice, &t, ticks);
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "spi_device_transmit fail: %s len=%u tx=%p rx=%p",
esp_err_to_name(ret), (unsigned)(total_len * 8U), t.tx_buffer, t.rx_buffer);
DEBUG_WARN("SPI", "spi_device_queue_trans fail: %s len=%u",
esp_err_to_name(ret), (unsigned)(total_len * 8U));
return ret;
}
if (ret == ESP_OK && rx_len > 0U && rx_ptr != rx_buf) {
spi_transaction_t *ret_trans = NULL;
ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, ticks);
(void)ret_trans;
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "spi_device_get_trans_result timeout/fail: %s len=%u tx=%p rx=%p",
esp_err_to_name(ret), (unsigned)(total_len * 8U), t.tx_buffer, t.rx_buffer);
return ret;
}
if (rx_len > 0U && rx_ptr != rx_buf) {
memcpy(rx_buf, rx_ptr, rx_len);
}
return ret;
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)
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms)
{
if ((payload_len > 0U && payload == NULL) || (rx_len > 0U && rx_buf == NULL)) {
return ESP_ERR_INVALID_ARG;
@@ -121,7 +135,8 @@ esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd,
esp_err_t ret = transferClocked(tx_staging, total_len,
(rx_len > 0U) ? rx_staging : NULL,
(rx_len > 0U) ? total_len : 0U);
(rx_len > 0U) ? total_len : 0U,
timeout_ms);
if (ret == ESP_OK && rx_len > 0U) {
memcpy(rx_buf, &rx_staging[cmd_len + payload_len], rx_len);
}
@@ -209,6 +224,14 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
// ============================================================
esp_err_t SpiMaster::init()
{
LockGuard lock(m_mutex);
// 创建 SPI 总线互斥锁(必须在 SPI 设备初始化之前)
m_mutex = xSemaphoreCreateMutex();
if (m_mutex == NULL) {
DEBUG_ERR("SPI", "SPI 互斥锁创建失败");
return ESP_ERR_NO_MEM;
}
DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ===");
spi_bus_config_t bus_cfg = {};
@@ -255,19 +278,20 @@ 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;
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len);
LockGuard lock(m_mutex);
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
}
esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len)
{
return transferRequestResponse(cmd, payload, len, NULL, 0);
LockGuard lock(m_mutex);
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
}
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);
LockGuard lock(m_mutex);
return transferRequestResponse(cmd, NULL, 0, recv_buf, len, timeout_ms);
}
// ============================================================
@@ -277,6 +301,7 @@ esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t pa
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
{
LockGuard lock(m_mutex);
uint8_t tx_frame[SPI_OTA_FRAME_MAX];
size_t tx_len = 0;
if (!otaBuildFrame(type, payload, payload_len, tx_frame, &tx_len)) {
@@ -285,17 +310,24 @@ esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t pa
DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u first4=%02X%02X%02X%02X",
type, (unsigned)tx_len, tx_frame[0], tx_frame[1], tx_frame[2], tx_frame[3]);
esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0);
esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0, timeout_ms);
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret));
return ret;
}
return otaPollResponse(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
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)
{
LockGuard lock(m_mutex);
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
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)
{
constexpr size_t kPollBytes = 96U;
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
@@ -305,7 +337,12 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
uint8_t dummy[kPollBytes];
memset(dummy, 0xFF, sizeof(dummy));
esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw));
/* 用剩余时间作为单次 SPI 事务超时,避免总等待远超 deadline */
TickType_t now = xTaskGetTickCount();
uint32_t remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U;
if (remain_ms == 0U) break;
esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw), remain_ms);
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "OTA 响应读取失败: %s", esp_err_to_name(ret));
vTaskDelay(pdMS_TO_TICKS(10));
@@ -355,6 +392,7 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
esp_err_t SpiMaster::appRequestBootloader()
{
LockGuard lock(m_mutex);
DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader...");
constexpr uint32_t kMaxRetries = 3U;
@@ -393,6 +431,7 @@ esp_err_t SpiMaster::appRequestBootloader()
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
{
LockGuard lock(m_mutex);
constexpr uint8_t kAppPingType = 0xA1U;
constexpr size_t kAppPingPollBytes = 32U;
@@ -511,13 +550,14 @@ 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)
{
LockGuard lock(m_mutex);
esp_err_t ret;
int retry_count = 0;
DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries);
while (retry_count < max_retries) {
ret = syncTransfer(cmd, payload, payload_len,
ret = syncTransferUnlocked(cmd, payload, payload_len,
recv_buf, recv_len, 3000);
if (ret == ESP_OK) {
if (retry_count > 0) {
@@ -544,6 +584,7 @@ int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms)
{
LockGuard lock(m_mutex);
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
return -1;
}
@@ -559,7 +600,7 @@ int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id,
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);
esp_err_t ret = syncTransferUnlocked(0, tx_buf, tx_len, recv_buf, recv_max, timeout_ms);
if (ret != ESP_OK) {
return -1;
}
@@ -570,6 +611,7 @@ int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id,
int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc)
{
LockGuard lock(m_mutex);
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
return -1;
}
@@ -585,7 +627,7 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
memcpy(&tx_buf[tx_len], data, dlc);
tx_len += dlc;
esp_err_t ret = send(0, tx_buf, tx_len);
esp_err_t ret = sendUnlocked(0, tx_buf, tx_len);
if (ret != ESP_OK) {
return -1;
}
@@ -593,4 +635,17 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
return 0;
}
// ============================================================
// 无锁内部版本实现
// ============================================================
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)
{
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
}
esp_err_t SpiMaster::sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len)
{
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
}
+31 -3
View File
@@ -9,6 +9,8 @@
#include <stdint.h>
#include "driver/spi_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
// SPI 配置常量
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
@@ -23,7 +25,7 @@
#define SPI_OTA_CMD_DATA (0x02)
#define SPI_OTA_CMD_CANCEL (0x03)
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
#define SPI_APP_ENTER_BOOT_ACK_OK (0xA5)
#define SPI_APP_ENTER_BOOT_ACK_OK (0xB5) // GD32 APP 固件须同步修改 ACK 字节为 0xB5
#define SPI_OTA_RSP_ACK (0x80)
#define SPI_OTA_RSP_WAIT (0x81)
#define SPI_OTA_RSP_ERROR (0x82)
@@ -140,13 +142,39 @@ private:
SpiMaster& operator=(const SpiMaster&) = delete;
spi_device_handle_t m_spiDevice = NULL;
SemaphoreHandle_t m_mutex = NULL;
// RAII 锁守卫
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) {
if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); }
}
~LockGuard() {
if (m_lock) { xSemaphoreGive(m_lock); }
}
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
private:
SemaphoreHandle_t m_lock;
};
// 内部 SPI 传输
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len);
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms = 0U);
esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len,
uint8_t *rx_buf, size_t rx_len);
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms = 0U);
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
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);
esp_err_t sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len);
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 帧工具(静态,无状态)
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,