docs(readme): 更新调试日志与上位机通信说明

更新了 README.md 中关于调试日志与上位机通信的部分,详细说明了当
前通信架构分为两类:
1. 业务通信:HTTP 接口用于 WebUI 与 ESP32 通信
2. 调试镜像:UART0 + UDP 用于独立调试接收器

明确区分了 WebUI 使用的 HTTP 接口功能,并移除了不再需要的心跳包
说明。

refactor(health): 移除健康监控模块的备份文件

删除了 _assistant_backups 目录中的 health_monitor.cpp 和
health_monitor.h 备份文件,清理项目结构。
This commit is contained in:
小龙虾
2026-05-05 04:48:31 +08:00
parent 478e5e5a31
commit f598b9acb3
20 changed files with 955 additions and 1420 deletions
+11 -7
View File
@@ -20,9 +20,15 @@
#include "transport/protocol.h"
#include "monitor/debug_log.h"
// 状态监控任务
/**
* @brief 状态监控任务
* @param pvParameters
*/
static void status_monitor_task(void *pvParameters);
/**
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
*/
extern "C" void app_main(void)
{
esp_chip_info_t chip_info;
@@ -33,12 +39,10 @@ extern "C" void app_main(void)
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
// 必须在 wifi_early_init 之前,这样 WiFi 初始化日志也能输出到上位机
debug_log_init();
// ★ WiFi 提前初始化(调度器启动前)
// 包括:NVS → netif 创建 → event handler 注册 → WiFi driver init → 配置 → connect
// 连接尝试立即开始,事件排队等调度器启动后处理
bool wifi_sta_ok = wifi_early_init();
health_monitor_init();
@@ -68,11 +72,9 @@ extern "C" void app_main(void)
DEBUG_INFO("MAIN", "健康监控任务已创建");
}
// 初始化协议层(CRC、AES)
protocol_init();
DEBUG_INFO("MAIN", "协议层初始化完成");
// 初始化 SPI 总线
esp_err_t ret = spi_master_init();
if (ret != ESP_OK) {
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret);
@@ -153,7 +155,10 @@ extern "C" void app_main(void)
// ============================================================
// 状态监控任务 - 每 10 秒打印系统状态
// ============================================================
/**
* @brief 周期性打印系统运行状态,包括内存、WiFi 和 OTA 中继信息。
* @param pvParameters
*/
static void status_monitor_task(void *pvParameters)
{
(void)pvParameters;
@@ -164,7 +169,6 @@ static void status_monitor_task(void *pvParameters)
while (1) {
loop_count++;
// 获取系统信息
uint32_t free_heap = esp_get_free_heap_size();
uint32_t min_free_heap = esp_get_minimum_free_heap_size();
health_state_t health = health_get_current();
+59 -15
View File
@@ -1,16 +1,18 @@
/*
* debug_log.c - WiFi 调试日志输出实现
* debug_log.c - 调试日志输出实现
*
* 日志会同时写入三处:
* 1. UART0 串口(本地串口调试)
* 2. UDP 单播到指定上位机(独立 UDP 调试接收端可选)
* 3. 内部环形缓冲区(供 HTTP /logs 拉取,WebUI 实际使用这一条)
*
* 通过 UDP socket 将调试日志发送到上位机
* 上位机需监听 <DEBUG_LOG_IP>:<DEBUG_LOG_PORT>
*
* 支持双通道输出:UDP + UART0 串口
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
* UDP: <DEBUG_LOG_IP>:<DEBUG_LOG_PORT>
*
* 时序说明:
* esp_netif_init() 创建 tcpip 线程后才可安全使用 socket
* 本模块在首次 debug_log_send() 时才创建 socket(延迟创建)
* WiFi 连接前 sendto() 会失败(无路由),日志仅走 UART
* WiFi 连接前 sendto() 会失败(无路由),日志仍可写入 UART 与 HTTP 日志缓冲
* WiFi 连接后 UDP 立即生效,无需额外操作
*/
#include <string.h>
@@ -28,10 +30,14 @@
static const char *TAG = "DEBUG_LOG";
/** @brief 当前调试日志过滤级别,运行时可动态调整。 */
debug_log_level_t g_debug_log_level = DEBUG_LOG_ALL;
/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */
static int s_udp_sock = -1;
/** @brief UDP 调试日志目标地址。 */
static struct sockaddr_in s_debug_addr;
/** @brief 日志模块初始化标志。 */
static bool s_initialized = false;
// ============================================================
@@ -43,9 +49,13 @@ static char s_log_ring[LOG_RING_SLOTS][LOG_RING_SIZE];
static int s_log_ring_lens[LOG_RING_SLOTS];
static int s_log_ring_head = 0; // 下次写入位置
static int s_log_ring_count = 0; // 已存条数(超过 LOG_RING_SLOTS 后不再增长,覆盖旧数据)
/** @brief 环形缓冲区互斥锁,保护多线程并发读写。 */
static SemaphoreHandle_t s_log_ring_mutex = NULL;
// 获取日志缓冲区中的条数
/**
* @brief 获取日志缓冲区中的条数
* @return 当前日志条数。
*/
int debug_log_ring_count(void)
{
if (!s_log_ring_mutex) return 0;
@@ -94,7 +104,6 @@ char *debug_log_ring_drain_json(int *out_len)
if (i > 0) json[pos++] = ',';
// JSON 转义日志内容
pos += snprintf(json + pos, buf_size - pos, "\"");
for (int j = 0; j < len && pos < (int)buf_size - 10; j++) {
char c = s_log_ring[idx][j];
@@ -116,6 +125,7 @@ char *debug_log_ring_drain_json(int *out_len)
// 清空缓冲区
s_log_ring_head = 0;
/** @brief 环形缓冲区中已存储的日志条数。 */
s_log_ring_count = 0;
xSemaphoreGive(s_log_ring_mutex);
@@ -124,7 +134,11 @@ char *debug_log_ring_drain_json(int *out_len)
return json;
}
// 写入一条日志到环形缓冲区
/**
* @brief 写入一条日志到环形缓冲区
* @param data
* @param len
*/
static void log_ring_push(const char *data, int len)
{
if (!s_log_ring_mutex || len <= 0) return;
@@ -138,6 +152,7 @@ static void log_ring_push(const char *data, int len)
s_log_ring[s_log_ring_head][copy_len] = '\0';
s_log_ring_lens[s_log_ring_head] = copy_len;
/** @brief 环形缓冲区下一个写入位置。 */
s_log_ring_head = (s_log_ring_head + 1) % LOG_RING_SLOTS;
if (s_log_ring_count < LOG_RING_SLOTS) s_log_ring_count++;
@@ -146,12 +161,16 @@ static void log_ring_push(const char *data, int len)
// UART0 状态
static bool s_uart_initialized = false;
/** @brief 调试串口使用的 UART 端口号。 */
static uart_port_t s_uart_port = UART_NUM_0;
// ============================================================
// UART0 串口调试
// ============================================================
/**
* @brief 初始化 UART0 调试串口,配置波特率和引脚映射。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t uart_debug_init(void)
{
if (s_uart_initialized) {
@@ -187,6 +206,7 @@ esp_err_t uart_debug_init(void)
return ret;
}
/** @brief UART0 初始化标志。 */
s_uart_initialized = true;
ESP_LOGI(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)",
UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_DEBUG_BAUD_RATE);
@@ -194,6 +214,12 @@ esp_err_t uart_debug_init(void)
return ESP_OK;
}
/**
* @brief 通过 UART0 发送一段原始字节数据。
* @param data 待发送的数据指针。
* @param len 数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t uart_debug_send(const uint8_t *data, size_t len)
{
if (!s_uart_initialized) {
@@ -212,7 +238,10 @@ esp_err_t uart_debug_send(const uint8_t *data, size_t len)
// UDP 调试日志
// ============================================================
// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
/**
* @brief 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
* @return ESP_OK 成功,其他失败。
*/
esp_err_t debug_log_init(void)
{
if (s_initialized) {
@@ -221,7 +250,7 @@ esp_err_t debug_log_init(void)
uart_debug_init();
// 日志环形缓冲区互斥锁
/** @brief 环形缓冲区互斥锁,保护多线程并发读写。 */
s_log_ring_mutex = xSemaphoreCreateMutex();
// 目标地址:单播到上位机
@@ -230,19 +259,24 @@ esp_err_t debug_log_init(void)
s_debug_addr.sin_port = htons(DEBUG_LOG_PORT);
s_debug_addr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP);
/** @brief 日志模块初始化标志。 */
s_initialized = true;
ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
return ESP_OK;
}
// 延迟创建 UDP socket(首次发送时调用)
/**
* @brief 延迟创建 UDP socket(首次发送时调用)
* @return
*/
static esp_err_t ensure_udp_socket(void)
{
if (s_udp_sock >= 0) {
return ESP_OK;
}
/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */
s_udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (s_udp_sock < 0) {
ESP_LOGE(TAG, "创建 UDP socket 失败");
@@ -253,7 +287,12 @@ static esp_err_t ensure_udp_socket(void)
return ESP_OK;
}
// 发送原始数据(UDP 单播 + UART 双通道)
/**
* @brief 发送原始数据(UDP 单播 + UART 双通道)
* @param data 日志数据指针。
* @param len 数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t debug_log_send(const uint8_t *data, size_t len)
{
esp_err_t udp_ret = ESP_FAIL;
@@ -289,7 +328,12 @@ esp_err_t debug_log_send(const uint8_t *data, size_t len)
return ESP_FAIL;
}
// 格式化日志输出
/**
* @brief 格式化日志输出
* @param level 日志级别。
* @param tag 日志标签。
* @param fmt 格式化字符串。
*/
void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...)
{
if (!s_initialized) {
+40 -11
View File
@@ -1,10 +1,14 @@
/*
* debug_log.h - WiFi 调试日志输出
* debug_log.h - 调试日志输出
*
* 通过 UDP socket 将调试日志发送到上位机
* 上位机监听 <DEBUG_LOG_PORT> 接收日志
* 当前日志会并行写入:
* - UART0 串口
* - UDP 单播(独立调试接收端可选)
* - 环形缓冲区,供 HTTP /logs 拉取
*
* 注意:当前 tools/WebUI 日志面板走的是 HTTP /logs,
* 不是直接监听 UDP 6666。
*
* 支持双通道输出:UDP + UART0 串口
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
*/
#ifndef DEBUG_LOG_H
@@ -35,26 +39,51 @@ typedef enum {
// 当前日志级别(运行时可改)
extern debug_log_level_t g_debug_log_level;
// 初始化调试日志系统(启动 UDP 客户端 + UART0)
/**
* @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t debug_log_init(void);
// UART0 串口调试初始化
/**
* @brief 初始化 UART0 调试串口。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t uart_debug_init(void);
// UART0 串口发送
/**
* @brief 通过 UART0 直接发送一段原始字节数据。
* @param data 待发送的数据指针。
* @param len 数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t uart_debug_send(const uint8_t *data, size_t len);
// 发送原始数据到上位机
/**
* @brief 发送原始日志数据到底层输出通道。
* @param data 日志数据指针。
* @param len 数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t debug_log_send(const uint8_t *data, size_t len);
// 格式化日志输出(类似 printf)
/**
* @brief 按日志级别与标签格式化输出一条日志。
* @param level 日志级别。
* @param tag 日志标签。
* @param fmt 格式化字符串。
*/
void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...);
// 日志环形缓冲区(供 HTTP /logs 拉取)
/**
* @brief 获取当前环形日志缓冲区中的日志条数。
* @return 当前日志条数。
*/
int debug_log_ring_count(void);
// 读取并清空环形日志缓冲区,返回 JSON 数组字符串,需由调用者 free。
char *debug_log_ring_drain_json(int *out_len); // 调用者 free
// 宏定义方便调用
#define DEBUG_ERR(tag, fmt, ...) debug_log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
#define DEBUG_WARN(tag, fmt, ...) debug_log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
#define DEBUG_INFO(tag, fmt, ...) debug_log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
+59 -8
View File
@@ -15,24 +15,36 @@
#include "health_monitor.h"
#include "debug_log.h"
/** @brief 全局健康状态,受互斥锁保护。 */
health_state_t g_health_state = SYS_NORMAL;
/** @brief 健康状态互斥锁。 */
static SemaphoreHandle_t g_health_mutex = NULL;
/** @brief 内置温度传感器句柄。 */
static temperature_sensor_handle_t s_temp_handle = NULL;
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
static float s_last_temperature = 0.0f;
/** @brief 最近一次估算的系统电压(伏特)。 */
static float s_last_voltage = 3.30f;
// ----------------------------------------
// 健康状态管理
// ----------------------------------------
/**
* @brief 初始化健康监控模块,创建互斥锁和基础状态。
*/
void health_monitor_init(void)
{
/** @brief 健康状态互斥锁。 */
g_health_mutex = xSemaphoreCreateMutex();
if (g_health_mutex == NULL) {
DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
}
}
/**
* @brief 获取当前系统健康状态。
* @return 当前健康状态枚举值。
*/
health_state_t health_get_current(void)
{
health_state_t state;
@@ -46,19 +58,32 @@ health_state_t health_get_current(void)
return state;
}
/**
* @brief 返回最近一次采样到的芯片温度(摄氏度)。
* @return 最近一次温度采样值(摄氏度)。
*/
float health_get_last_temperature(void)
{
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
return s_last_temperature;
}
/**
* @brief 返回最近一次记录的系统电压估算值。
* @return 最近一次电压估算值(伏特)。
*/
float health_get_last_voltage(void)
{
/** @brief 最近一次估算的系统电压(伏特)。 */
return s_last_voltage;
}
// ----------------------------------------
// 温度监控(定时器回调)
// ----------------------------------------
/**
* @brief 温度定时回调,采样并根据阈值更新健康状态。
* @param arg
*/
static void temp_monitor_callback(void *arg)
{
float temp = 0.0f;
@@ -68,14 +93,17 @@ static void temp_monitor_callback(void *arg)
return;
}
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
s_last_temperature = temp;
if (temp >= TEMP_CRITICAL) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_SAFE_SHUTDOWN;
DEBUG_ERR("TEMP", "!!! 危险 !!! 温度 %.1f°C >= %d°C,触发安全关机", temp, TEMP_CRITICAL);
DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN");
} else if (temp >= TEMP_WARNING) {
if (g_health_state < SYS_WARNING) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_WARNING;
DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
@@ -85,18 +113,24 @@ static void temp_monitor_callback(void *arg)
// ----------------------------------------
// 电压监控(启动时调用一次)
// ----------------------------------------
/**
* @brief 电压定时回调,采样估算电压并根据阈值更新健康状态。
* @param arg
*/
static void voltage_monitor_callback(void *arg)
{
float voltage = 3.30f; // ESP32-C3 无内置电压传感器,使用估算
/** @brief 最近一次估算的系统电压(伏特)。 */
s_last_voltage = voltage;
if (voltage < 2.3f) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_SAFE_SHUTDOWN;
DEBUG_ERR("VOLT", "!!! 危险 !!! 电压 %.2fV < 2.3V,触发安全关机", voltage);
DEBUG_ERR("VOLT", "健康状态变更为: SYS_SAFE_SHUTDOWN");
} else if (voltage < 2.5f) {
if (g_health_state < SYS_WARNING) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_WARNING;
DEBUG_WARN("VOLT", "警告: 电压 %.2fV < 2.5V", voltage);
DEBUG_WARN("VOLT", "健康状态变更为: SYS_WARNING");
@@ -106,12 +140,14 @@ static void voltage_monitor_callback(void *arg)
// ----------------------------------------
// 温度电压监控任务入口
// ----------------------------------------
/**
* @brief 启动温度和电压监控定时器,周期性巡检系统健康。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void health_monitor_task(void *pvParameters)
{
DEBUG_INFO("MON", "=== 健康监控任务启动 ===");
// 初始化内置温度传感器(测量范围 20°C ~ 80°C)
temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
esp_err_t err = temperature_sensor_install(&temp_config, &s_temp_handle);
if (err != ESP_OK) {
@@ -134,7 +170,6 @@ void health_monitor_task(void *pvParameters)
DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 1 秒)");
// 启动时检查一次电压
voltage_monitor_callback(NULL);
// 每1秒刷新一次估算电压
@@ -165,7 +200,11 @@ void health_monitor_task(void *pvParameters)
// ----------------------------------------
// 健康事件日志
// ----------------------------------------
/**
* @brief 记录一条健康监控事件日志。
* @param event 事件名称字符串。
* @param value 事件关联的数值。
*/
void health_log_event(const char *event, int value)
{
DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
@@ -173,7 +212,10 @@ void health_log_event(const char *event, int value)
// ----------------------------------------
// OTA 传输前的健康检查
// ----------------------------------------
/**
* @brief OTA 传输前执行健康检查,决定是否允许传输。
* @return 健康检查结果,决定是否允许 OTA。
*/
health_status_t health_check_before_ota(void)
{
health_status_t status = HEALTH_MON_OK;
@@ -192,6 +234,7 @@ health_status_t health_check_before_ota(void)
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
if (ap_info.rssi < WIFI_RSSI_CRITICAL) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_CRITICAL;
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (危险阈值)",
ap_info.rssi, WIFI_RSSI_CRITICAL);
@@ -212,15 +255,22 @@ health_status_t health_check_before_ota(void)
return status;
}
/**
* @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。
* @param rssi 当前 WiFi 信号强度(dBm)。
* @return 健康检查结果。
*/
health_status_t health_update_from_wifi_rssi(int8_t rssi)
{
if (rssi < WIFI_RSSI_CRITICAL) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_CRITICAL;
DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
health_log_event("WIFI危险", rssi);
return HEALTH_MON_CRITICAL;
} else if (rssi < WIFI_RSSI_NORMAL) {
if (g_health_state < SYS_WARNING) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_WARNING;
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
health_log_event("WIFI警告", rssi);
@@ -229,6 +279,7 @@ health_status_t health_update_from_wifi_rssi(int8_t rssi)
}
if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
/** @brief 全局健康状态,受互斥锁保护。 */
g_health_state = SYS_NORMAL;
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
health_log_event("WIFI恢复", rssi);
+40 -1
View File
@@ -28,12 +28,51 @@ extern health_state_t g_health_state;
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
// 函数声明
/**
* @brief 初始化健康监控模块,创建内部互斥锁与基础状态。
*/
void health_monitor_init(void);
/**
* @brief 健康监控任务入口,负责启动温度/电压定时检测并周期性巡检系统状态。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void health_monitor_task(void *pvParameters);
/**
* @brief 在 OTA 传输开始前执行健康检查,返回当前是否允许传输。
* @return 健康检查结果,决定是否允许 OTA。
*/
health_status_t health_check_before_ota(void);
/**
* @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。
* @param event 事件名称字符串。
* @param value 事件关联的数值。
*/
void health_log_event(const char *event, int value);
/**
* @brief 获取当前系统健康状态。
* @return 当前健康状态枚举值。
*/
health_state_t health_get_current(void);
/**
* @brief 根据最新 WiFi RSSI 更新健康状态,并返回对应健康检查结果。
* @param rssi 当前 WiFi 信号强度(dBm)。
* @return 健康检查结果。
*/
health_status_t health_update_from_wifi_rssi(int8_t rssi);
/**
* @brief 获取最近一次采样到的芯片温度(摄氏度)。
* @return 最近一次温度采样值(摄氏度)。
*/
float health_get_last_temperature(void);
/**
* @brief 获取最近一次记录的系统电压估算值(伏特)。
* @return 最近一次电压估算值(伏特)。
*/
float health_get_last_voltage(void);
+125 -16
View File
@@ -17,21 +17,54 @@
#include "health_monitor.h"
#include "monitor/debug_log.h"
/** @brief 当前 OTA 中继状态机状态。 */
relay_state_t g_relay_state = RELAY_IDLE;
static const esp_partition_t *g_gd32_partition = NULL;
/** @brief OTA 认证会话上下文,包含会话密钥和 IV。 */
static session_ctx_t g_session_ctx;
/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
QueueHandle_t g_ota_dispatch_queue = NULL;
/** @brief OTA 结果回传队列,将执行结果返回给调用方。 */
QueueHandle_t g_ota_result_queue = NULL;
/**
* @brief OTA 握手阶段:向 GD32 发送握手请求并等待进入认证流程。
* @return 操作结果码。
*/
static ota_result_t send_handshake_req(void);
/**
* @brief 认证阶段:生成随机数并完成与 GD32 的双向认证。
* @return 操作结果码。
*/
static ota_result_t send_nonce_and_authenticate(void);
/**
* @brief 向 GD32 发送待升级固件的总大小。
* @param size 固件总字节数。
* @return 操作结果码。
*/
static ota_result_t send_firmware_size(uint32_t size);
/**
* @brief 从 Flash 逐包读取固件并中继转发给 GD32。
* @return 操作结果码。
*/
static ota_result_t relay_packets_from_flash(void);
/**
* @brief OTA 收尾阶段:发送结束命令并等待 GD32 确认。
* @return 操作结果码。
*/
static ota_result_t finish_ota(void);
// 从 Flash 加载元数据
/**
* @brief 从 Flash 加载元数据
* @param meta 输出参数,加载后的元数据结构体。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_load(ota_meta_t *meta)
{
if (!meta) return ESP_ERR_INVALID_ARG;
@@ -73,7 +106,11 @@ esp_err_t meta_load(ota_meta_t *meta)
return ESP_OK;
}
// 保存元数据到 Flash
/**
* @brief 保存元数据到 Flash
* @param meta 待保存的元数据结构体。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_save(const ota_meta_t *meta)
{
if (!meta) return ESP_ERR_INVALID_ARG;
@@ -98,7 +135,12 @@ esp_err_t meta_save(const ota_meta_t *meta)
return ESP_OK;
}
// 传输过程中更新元数据
/**
* @brief 传输过程中更新元数据
* @param stored_size 已存储的固件大小(字节)。
* @param last_seq 最后成功的分块序号。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq)
{
ota_meta_t meta;
@@ -111,7 +153,10 @@ esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq)
return meta_save(&meta);
}
// OTA 完成后或中止时清除元数据
/**
* @brief OTA 完成后或中止时清除元数据
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_clear(void)
{
DEBUG_INFO("OTA", "清除 OTA 元数据");
@@ -119,7 +164,13 @@ esp_err_t meta_clear(void)
return meta_save(&meta);
}
// Flash 写入接口
/**
* @brief Flash 写入接口
* @param offset 分区内写入偏移。
* @param data 待写入的数据指针。
* @param len 数据长度(字节)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len)
{
if (!g_gd32_partition) {
@@ -134,6 +185,13 @@ esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len)
return esp_partition_write(g_gd32_partition, offset, data, len);
}
/**
* @brief 从 OTA 缓存分区读取指定偏移和长度的数据块。
* @param offset 分区内读取偏移。
* @param data 读取缓冲区指针。
* @param len 读取长度(字节)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len)
{
if (!g_gd32_partition) {
@@ -148,7 +206,10 @@ esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len)
return esp_partition_read(g_gd32_partition, offset, data, len);
}
// 阶段1 - 发起与 GD32 的握手
/**
* @brief 阶段1 - 发起与 GD32 的握手
* @return 操作结果码。
*/
static ota_result_t send_handshake_req(void)
{
DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ===");
@@ -160,6 +221,7 @@ static ota_result_t send_handshake_req(void)
uint8_t payload[] = {0x00, 0xAC};
frame_build(CMD_HANDSHAKE_REQ, payload, sizeof(payload), tx_frame);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_HANDSHAKE_SENT;
DEBUG_INFO("OTA", "状态: RELAY_HANDSHAKE_SENT");
@@ -167,6 +229,7 @@ static ota_result_t send_handshake_req(void)
rx_frame, sizeof(rx_frame), 3000);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d", err);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_ERROR;
return OTA_RESULT_ERR_TIMEOUT;
}
@@ -177,7 +240,10 @@ static ota_result_t send_handshake_req(void)
return OTA_RESULT_OK;
}
// 发送随机数并与 GD32 相互认证
/**
* @brief 发送随机数并与 GD32 相互认证
* @return 操作结果码。
*/
static ota_result_t send_nonce_and_authenticate(void)
{
uint8_t tx_frame[64];
@@ -192,6 +258,7 @@ static ota_result_t send_nonce_and_authenticate(void)
DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---");
DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08X", timestamp);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_CHALLENGE_SENT;
DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_SENT");
@@ -201,10 +268,12 @@ static ota_result_t send_nonce_and_authenticate(void)
rx_frame, sizeof(rx_frame), 5000);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d", err);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_ERROR;
return OTA_RESULT_ERR_TIMEOUT;
}
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_CHALLENGE_RESPOND;
DEBUG_INFO("OTA", "已收到 GD32 握手数据包");
DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_RESPOND");
@@ -215,6 +284,7 @@ static ota_result_t send_nonce_and_authenticate(void)
DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY ---");
DEBUG_INFO("OTA", "发送变换后的随机数 (16B)");
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_SESSION_KEY_SENT;
DEBUG_INFO("OTA", "状态: RELAY_SESSION_KEY_SENT");
@@ -223,10 +293,12 @@ static ota_result_t send_nonce_and_authenticate(void)
rx_frame, sizeof(rx_frame), 3000);
if (err2 != ESP_OK) {
DEBUG_ERR("OTA", "SESSION_KEY 发送失败, err=%d", err2);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_ERROR;
return OTA_RESULT_ERR_TIMEOUT;
}
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_AUTH_COMPLETE;
DEBUG_INFO("OTA", "状态: RELAY_AUTH_COMPLETE");
DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ===");
@@ -237,7 +309,11 @@ static ota_result_t send_nonce_and_authenticate(void)
return OTA_RESULT_OK;
}
// 向 GD32 发送固件总大小
/**
* @brief 向 GD32 发送固件总大小
* @param size 固件总字节数。
* @return 操作结果码。
*/
static ota_result_t send_firmware_size(uint32_t size)
{
uint8_t tx_frame[32];
@@ -252,6 +328,7 @@ static ota_result_t send_firmware_size(uint32_t size)
size_payload[2] = (uint8_t)((size >> 16) & 0xFF);
size_payload[3] = (uint8_t)((size >> 24) & 0xFF);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_SIZE_SENT;
DEBUG_INFO("OTA", "状态: RELAY_SIZE_SENT");
@@ -260,18 +337,23 @@ static ota_result_t send_firmware_size(uint32_t size)
rx_frame, sizeof(rx_frame), 3000);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d", err);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_ERROR;
return OTA_RESULT_ERR_TIMEOUT;
}
DEBUG_INFO("OTA", "大小已发送: %u 字节,等待 GD32 确认...", size);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_SIZE_ACK;
DEBUG_INFO("OTA", "状态: RELAY_SIZE_ACK");
return OTA_RESULT_OK;
}
// 从 Flash 向 GD32 转发存储的固件(48B/包)
/**
* @brief 从 Flash 向 GD32 转发存储的固件(48B/包)
* @return 操作结果码。
*/
static ota_result_t relay_packets_from_flash(void)
{
DEBUG_INFO("OTA", "=== 阶段3: 从 Flash 转发固件 ===");
@@ -290,10 +372,10 @@ static ota_result_t relay_packets_from_flash(void)
DEBUG_INFO("OTA", "总包数: %u, 每包 48 字节", total_packets);
DEBUG_INFO("OTA", "状态: RELAY_RELAY_ACTIVE");
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_RELAY_ACTIVE;
for (uint32_t seq = 0; seq < total_packets; seq++) {
// 发送每个包前检查健康状态
health_status_t health = health_check_before_ota();
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
@@ -318,6 +400,7 @@ static ota_result_t relay_packets_from_flash(void)
tx_buf[2 + chunk_size] = (uint8_t)(crc & 0xFF);
tx_buf[2 + chunk_size + 1] = (uint8_t)((crc >> 8) & 0xFF);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_PACKET_SENT;
uint8_t cmd = (seq == total_packets - 1) ? CMD_DATA_END : CMD_DATA_PACKET;
@@ -331,6 +414,7 @@ static ota_result_t relay_packets_from_flash(void)
}
flash_offset += chunk_size;
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_PACKET_ACK_OK;
// 每 10% 记录一次进度
@@ -341,12 +425,16 @@ static ota_result_t relay_packets_from_flash(void)
}
}
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_RECEIVE_DONE;
DEBUG_INFO("OTA", "状态: RELAY_RECEIVE_DONE");
return result;
}
// 结束 OTA - 发送 0x0A AC 并清理
/**
* @brief 结束 OTA - 发送 0x0A AC 并清理
* @return 操作结果码。
*/
static ota_result_t finish_ota(void)
{
DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ===");
@@ -358,6 +446,7 @@ static ota_result_t finish_ota(void)
uint8_t payload[] = {0x0A, 0xAC};
frame_build(CMD_FINISH_OTA, payload, sizeof(payload), tx_frame);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_FINISH_SENT;
DEBUG_INFO("OTA", "状态: RELAY_FINISH_SENT");
@@ -365,6 +454,7 @@ static ota_result_t finish_ota(void)
rx_frame, sizeof(rx_frame), 5000);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "FINISH_OTA 失败, err=%d", err);
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_ERROR;
return OTA_RESULT_ERR_TIMEOUT;
}
@@ -374,6 +464,7 @@ static ota_result_t finish_ota(void)
session_key_clear(&g_session_ctx);
memset(&g_session_ctx, 0, sizeof(session_ctx_t));
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_DONE;
DEBUG_INFO("OTA", "状态: RELAY_DONE");
DEBUG_INFO("OTA", "=== OTA 完成 ===");
@@ -383,7 +474,10 @@ static ota_result_t finish_ota(void)
return OTA_RESULT_OK;
}
// 启动完整 OTA 流程的主入口
/**
* @brief 启动完整 OTA 流程的主入口
* @return 操作结果码。
*/
ota_result_t ota_relay_start_handshake(void)
{
ota_result_t res;
@@ -407,7 +501,10 @@ ota_result_t ota_relay_start_handshake(void)
return res;
}
// 从 Flash 向 GD32 转发固件
/**
* @brief 从 Flash 向 GD32 转发固件
* @return 操作结果码。
*/
ota_result_t ota_relay_forward_firmware(void)
{
ota_meta_t meta;
@@ -435,25 +532,37 @@ ota_result_t ota_relay_forward_firmware(void)
return res;
}
// 获取当前状态
/**
* @brief 获取当前状态
* @return 当前中继状态枚举值。
*/
relay_state_t ota_relay_get_state(void)
{
/** @brief 当前 OTA 中继状态机状态。 */
return g_relay_state;
}
// 重置状态机
/**
* @brief 重置状态机
*/
void ota_relay_reset(void)
{
/** @brief 当前 OTA 中继状态机状态。 */
g_relay_state = RELAY_IDLE;
session_key_clear(&g_session_ctx);
}
// OTA 中继 FreeRTOS 任务
/**
* @brief OTA 中继 FreeRTOS 任务
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void ota_relay_task(void *pvParameters)
{
DEBUG_INFO("OTA", "OTA 中继任务启动");
/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
g_ota_dispatch_queue = xQueueCreate(4, sizeof(uint32_t));
/** @brief OTA 结果回传队列,将执行结果返回给调用方。 */
g_ota_result_queue = xQueueCreate(4, sizeof(uint32_t));
uint32_t msg;
+57 -7
View File
@@ -70,29 +70,79 @@ typedef enum {
extern QueueHandle_t g_ota_dispatch_queue; // 从 WiFi/BLE 到 OTA 中继
extern QueueHandle_t g_ota_result_queue; // OTA 结果到 WiFi/BLE
// OTA 中继任务入口
/**
* @brief OTA 中继任务入口,负责执行握手、认证、分包转发和结束流程。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void ota_relay_task(void *pvParameters);
// T2.3: 发起与 GD32 的握手
/**
* @brief 发起与 GD32 的握手流程,建立 OTA 传输前的安全会话。
* @return 操作结果码。
*/
ota_result_t ota_relay_start_handshake(void);
// T3.4: 从 Flash 向 GD32 转发存储的固件(48B/包)
/**
* @brief 从 Flash 中读取已缓存固件并按协议转发给 GD32。
* @return 操作结果码。
*/
ota_result_t ota_relay_forward_firmware(void);
// T3.7: 获取当前中继状态
/**
* @brief 获取当前 OTA 中继状态机状态。
* @return 当前中继状态枚举值。
*/
relay_state_t ota_relay_get_state(void);
// T3.7: 重置中继状态机
/**
* @brief 重置 OTA 中继状态机,清理当前状态以便重新开始流程。
*/
void ota_relay_reset(void);
// T4.2: Flash 写入接口
/**
* @brief 向 OTA 缓存分区写入一段固件数据。
* @param offset 分区内写入偏移。
* @param data 待写入的数据指针。
* @param len 数据长度(字节)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len);
/**
* @brief 从 OTA 缓存分区读取一段固件数据。
* @param offset 分区内读取偏移。
* @param data 读取缓冲区指针。
* @param len 读取长度(字节)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len);
// T3.3: 元数据文件管理
/**
* @brief 从持久化文件中加载 OTA 元数据。
* @param meta 输出参数,加载后的元数据结构体。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_load(ota_meta_t *meta);
/**
* @brief 将 OTA 元数据保存到持久化文件。
* @param meta 待保存的元数据结构体。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_save(const ota_meta_t *meta);
/**
* @brief 更新已存储大小与最后分块序号等 OTA 进度信息。
* @param stored_size 已存储的固件大小(字节)。
* @param last_seq 最后成功的分块序号。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq);
/**
* @brief 清空 OTA 元数据,通常在完成或中止后调用。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t meta_clear(void);
// 当前中继状态(供健康监控外部访问)
+61 -17
View File
@@ -42,12 +42,19 @@
static bool s_ble_ready = false; // NimBLE 同步完成
static bool s_ble_connected = false; // GATT 客户端已连接
/** @brief 当前 BLE 连接句柄。 */
static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
static uint16_t s_data_out_val_handle = 0; // DATA_OUT 值句柄 (供通知用)
// ============================================================
// NimBLE 工具: 从 mbuf 读取原始数据
// ============================================================
/**
* @brief 将 NimBLE mbuf 中的数据拷贝到普通字节缓冲区。
* @param om NimBLE mbuf 数据指针。
* @param buf 输出缓冲区。
* @param buf_len 缓冲区最大长度。
* @return 拷贝的字节数,失败返回 -1。
*/
static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
{
if (!om || !buf) return -1;
@@ -184,7 +191,6 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
return BLE_ATT_ERR_UNLIKELY;
}
// --- 更新元数据 ---
err = meta_update(flash_offset + data_len, seq);
if (err != ESP_OK) {
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
@@ -241,11 +247,16 @@ static int ble_ota_data_out_access(uint16_t conn_handle, uint16_t attr_handle,
// ============================================================
// BLE_UUID16_DECLARE 在 C++ 中产生 "taking address of rvalue" 编译错误。
// ESP-IDF 6.0 NimBLE 的 C++ 兼容写法: 先定义 static ble_uuid16_t 变量, 再取 .u 地址。
// 改用 static ble_uuid16_t 变量,再取 .u 地址。
/** @brief OTA GATT 服务 UUID */
static const ble_uuid16_t GATT_SVC_UUID = BLE_UUID16_INIT(BLE_OTA_SERVICE_UUID);
/** @brief CTRL 特征 UUID,写入 0xF7 触发中继 */
static const ble_uuid16_t GATT_CTRL_UUID = BLE_UUID16_INIT(BLE_OTA_CTRL_UUID);
/** @brief DATA_IN 特征 UUID,接收固件数据块 */
static const ble_uuid16_t GATT_DATA_IN_UUID = BLE_UUID16_INIT(BLE_OTA_DATA_IN_UUID);
/** @brief DATA_OUT 特征 UUID,Notify 推送 GD32 响应 */
static const ble_uuid16_t GATT_DATA_OUT_UUID = BLE_UUID16_INIT(BLE_OTA_DATA_OUT_UUID);
/** @brief VERSION 特征 UUID,返回固件版本字符串 */
static const ble_uuid16_t GATT_VERSION_UUID = BLE_UUID16_INIT(BLE_OTA_VERSION_UUID);
// NimBLE C 结构体在 C++ designated initializer 不能显式初始化全部字段。
@@ -296,7 +307,12 @@ static const struct ble_gatt_svc_def gatt_ota_svcs[] = {
// ============================================================
// GAP 事件回调
// ============================================================
/**
* @brief BLE GAP 事件回调,处理连接和断开事件。
* @param event
* @param arg
* @return 0 成功。
*/
static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
{
(void)arg;
@@ -307,10 +323,12 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
if (event->connect.status == 0) {
// 连接成功
s_ble_connected = true;
/** @brief 当前 BLE 连接句柄。 */
s_conn_handle = event->connect.conn_handle;
DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", s_conn_handle);
} else {
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
/** @brief BLE GATT 客户端连接状态。 */
s_ble_connected = false;
}
return 0;
@@ -318,7 +336,9 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
case BLE_GAP_EVENT_DISCONNECT:
DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d",
event->disconnect.reason);
/** @brief BLE GATT 客户端连接状态。 */
s_ble_connected = false;
/** @brief 当前 BLE 连接句柄。 */
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
// 恢复广播以接受新连接
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
@@ -352,7 +372,9 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
// ============================================================
// BLE 广播
// ============================================================
/**
* @brief 启动 BLE 广播,使设备可被发现和连接。
*/
static void ble_ota_start_advertising(void)
{
uint8_t adv_data[31] = {0};
@@ -420,7 +442,9 @@ static void ble_ota_start_advertising(void)
// ============================================================
// NimBLE 同步/重置回调
// ============================================================
/**
* @brief NimBLE 协议栈同步完成回调,初始化广播。
*/
static void ble_ota_on_sync(void)
{
DEBUG_INFO(TAG, "NimBLE 协议栈同步完成");
@@ -432,21 +456,28 @@ static void ble_ota_on_sync(void)
ble_ota_start_advertising();
}
/**
* @brief NimBLE 协议栈重置回调。
* @param reason
*/
static void ble_ota_on_reset(int reason)
{
DEBUG_WARN(TAG, "NimBLE 协议栈重置: reason=%d", reason);
/** @brief NimBLE 协议栈同步完成标志。 */
s_ble_ready = false;
}
// ============================================================
// NimBLE Host 任务(独立 FreeRTOS 任务)
// ============================================================
/**
* @brief NimBLE 主机任务入口,处理协议栈事件循环。
* @param param
*/
static void ble_ota_nimble_host_task(void *param)
{
(void)param;
DEBUG_INFO(TAG, "NimBLE Host 任务启动");
// nimble_port_run() 阻塞运行 NimBLE 事件循环
nimble_port_run();
// 正常情况下不会到达这里
DEBUG_INFO(TAG, "NimBLE Host 任务退出");
@@ -455,7 +486,10 @@ static void ble_ota_nimble_host_task(void *param)
// ============================================================
// 公开 API
// ============================================================
/**
* @brief 启动 BLE GATT 服务器并注册 OTA 服务。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t ble_ota_server_start(void)
{
int rc;
@@ -465,7 +499,6 @@ esp_err_t ble_ota_server_start(void)
DEBUG_INFO(TAG, " MTU: %" PRIu16, BLE_MTU_SIZE);
DEBUG_INFO(TAG, " Svc: 0x%04X", BLE_OTA_SERVICE_UUID);
// 1. 初始化 NimBLE 底层
esp_err_t err = nimble_port_init();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "nimble_port_init 失败: %s", esp_err_to_name(err));
@@ -473,11 +506,9 @@ esp_err_t ble_ota_server_start(void)
}
DEBUG_INFO(TAG, "nimble_port_init 完成");
// 2. 初始化 GAP / GATT 服务(无需 BLE 存储回调 — 不做 bonding)
ble_svc_gap_init();
ble_svc_gatt_init();
// 4. 设置设备名称
rc = ble_svc_gap_device_name_set(BLE_DEVICE_NAME);
if (rc != 0) {
DEBUG_WARN(TAG, "设置设备名称失败: %d", rc);
@@ -490,7 +521,6 @@ esp_err_t ble_ota_server_start(void)
ble_hs_cfg.reset_cb = ble_ota_on_reset;
// 不做 bonding,不需要 store 回调
// 6. 预注册 GATT 服务(在 sync 回调中也会再注册一次确保)
rc = ble_gatts_count_cfg(gatt_ota_svcs);
if (rc != 0) {
DEBUG_ERR(TAG, "GATT 服务计数失败: %d", rc);
@@ -503,13 +533,18 @@ esp_err_t ble_ota_server_start(void)
return ESP_FAIL;
}
// 7. 启动 NimBLE Host 任务(内部创建 FreeRTOS 任务跑 nimble_port_run)
nimble_port_freertos_init(ble_ota_nimble_host_task);
DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成");
return ESP_OK;
}
/**
* @brief 通过 BLE Notify 将 GD32 响应推送给上位机。
* @param data 待推送的数据指针。
* @param len 数据长度(字节)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
{
if (!s_ble_connected) {
@@ -522,7 +557,6 @@ esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
if (len == 0) return ESP_OK;
// 通过 NimBLE mbuf 发送通知
struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len);
if (om == NULL) {
DEBUG_ERR(TAG, "分配 mbuf 失败 (%zu 字节)", len);
@@ -539,11 +573,19 @@ esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
return ESP_OK;
}
/**
* @brief 检查当前是否有 BLE 客户端连接。
* @return true 已连接,false 未连接。
*/
bool ble_ota_is_connected(void)
{
/** @brief BLE GATT 客户端连接状态。 */
return s_ble_connected;
}
/**
* @brief 通知中继模块开始向 GD32 转发固件。
*/
void ble_ota_signal_relay(void)
{
if (g_ota_dispatch_queue == NULL) {
@@ -568,13 +610,15 @@ void ble_ota_signal_relay(void)
// 实际的 BLE 事件循环在独立的 NimBLE Host 任务中运行。
// 为避免与 ota_relay_task 竞争,此任务不从 g_ota_dispatch_queue 消费。
// ============================================================
/**
* @brief BLE OTA FreeRTOS 任务,初始化 NimBLE 并运行 GATT 服务。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void ble_ota_task(void *pvParameters)
{
(void)pvParameters;
DEBUG_INFO(TAG, "=== BLE OTA 任务启动 ===");
// 初始化并启动 BLE 服务器(同步操作,完成后广播开始)
esp_err_t err = ble_ota_server_start();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用",
+21 -5
View File
@@ -7,19 +7,35 @@
#include "ota_relay.h"
// BLE OTA 任务入口
/**
* @brief BLE OTA 任务入口,负责初始化 NimBLE 并运行 GATT OTA 服务。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void ble_ota_task(void *pvParameters);
// 启动 BLE GATT 服务器
/**
* @brief 启动 BLE GATT 服务器并注册 OTA 相关服务与特征。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t ble_ota_server_start(void);
// 通过 BLE 通知 APP GD32 的响应
/**
* @brief 通过 BLE Notify 将 GD32 的响应数据发送给上位机 APP。
* @param data 待推送的数据指针。
* @param len 数据长度(字节)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len);
// 检查 BLE 是否已连接
/**
* @brief 检查当前是否存在已连接的 BLE 客户端。
* @return true 已连接,false 未连接。
*/
bool ble_ota_is_connected(void);
// 信号通知中继开始(存储完成后调用)
/**
* @brief 在固件存储完成后通知中继模块开始向 GD32 转发。
*/
void ble_ota_signal_relay(void);
#endif // BLE_OTA_H
+160 -39
View File
@@ -28,26 +28,42 @@
#include "spi_master.h"
#include "cJSON.h"
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
/** @brief OTA 中继是否已触发标志。 */
static bool s_relay_triggered = false;
/** @brief STA 模式连接状态。 */
static bool s_sta_connected = false;
/** @brief STA 连接重试次数。 */
static int s_sta_retry_count = 0;
/** @brief STA 连接成功信号量。 */
static SemaphoreHandle_t s_sta_connected_sem = NULL;
/** @brief STA 获取 IP 信号量。 */
static SemaphoreHandle_t s_sta_got_ip_sem = NULL;
// ESP32 OTA 状态
static bool s_esp32_ota_active = false;
/** @brief ESP32 OTA 写入句柄。 */
static esp_ota_handle_t s_esp32_ota_handle = 0;
static const esp_partition_t *s_esp32_update_partition = NULL;
/** @brief ESP32 OTA 已接收字节数。 */
static size_t s_esp32_received = 0;
// GD32 OTA 存储状态
static bool s_gd32_storage_done = false;
/** @brief HTTP 服务器句柄。 */
static httpd_handle_t s_http_server = NULL;
/**
* @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
*/
static void wifi_set_static_ip(void);
/**
* @brief 为 HTTP 响应添加跨域访问控制头。
* @param req HTTP 请求对象。
*/
static void http_set_cors_headers(httpd_req_t *req)
{
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
@@ -56,6 +72,11 @@ static void http_set_cors_headers(httpd_req_t *req)
httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
}
/**
* @brief 处理 HTTP OPTIONS 预检请求,返回 204。
* @param req HTTP 请求对象。
* @return ESP_OK。
*/
static esp_err_t http_options_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -79,7 +100,9 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
case WIFI_EVENT_STA_CONNECTED:
DEBUG_INFO("WIFI", "STA 已关联到 AP");
/** @brief STA 模式连接状态。 */
s_sta_connected = true;
/** @brief STA 连接重试次数。 */
s_sta_retry_count = 0;
if (s_sta_connected_sem) {
xSemaphoreGive(s_sta_connected_sem);
@@ -89,6 +112,7 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
case WIFI_EVENT_STA_DISCONNECTED: {
wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count);
/** @brief STA 模式连接状态。 */
s_sta_connected = false;
if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
@@ -136,32 +160,32 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
// ----------------------------------------
// WiFi AP 模式初始化
// ----------------------------------------
/**
* @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t wifi_init_ap(void)
{
DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
s_wifi_mode = OTA_WIFI_MODE_AP;
// 创建 AP netif 并设置自定义 IP
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (ap_netif == NULL) {
ap_netif = esp_netif_create_default_wifi_ap();
}
if (ap_netif != NULL) {
// 停止 DHCP 服务器(必须先停才能改 IP)
esp_netif_dhcps_stop(ap_netif);
esp_netif_ip_info_t ip_info;
memset(&ip_info, 0, sizeof(ip_info));
// 方案 A:SoftAP 使用 192.168.31.0/24 网段
// ESP32 自己作为 AP 网关,电脑连入后拿到 192.168.31.x
inet_pton(AF_INET, "192.168.31.1", &ip_info.ip);
inet_pton(AF_INET, "192.168.31.1", &ip_info.gw);
inet_pton(AF_INET, "255.255.255.0", &ip_info.netmask);
ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
// 重新启动 DHCP 服务器,给连接的客户端分配 IP
esp_netif_dhcps_start(ap_netif);
DEBUG_INFO("WIFI", "AP IP 已设置: 192.168.31.1");
}
@@ -190,12 +214,17 @@ static esp_err_t wifi_init_ap(void)
// ----------------------------------------
// WiFi STA 模式初始化
// ----------------------------------------
/**
* @brief 初始化 STA 模式并连接预配置的路由器。
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t wifi_init_sta(void)
{
DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
s_wifi_mode = OTA_WIFI_MODE_STA;
/** @brief STA 连接重试次数。 */
s_sta_retry_count = 0;
// ★ 设置国家代码:中国,允许信道 1-13
@@ -222,7 +251,6 @@ static esp_err_t wifi_init_sta(void)
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
// 静态 IP 必须在 start/connect 之前配置,避免与 STA_START 自动连接发生竞态
wifi_set_static_ip();
ESP_ERROR_CHECK(esp_wifi_start());
@@ -233,7 +261,12 @@ static esp_err_t wifi_init_sta(void)
return ESP_OK;
}
// 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
/**
* @brief 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
* @param ip_str 输出缓冲区,存放获取到的 IP 字符串。
* @param len 缓冲区长度。
* @return ESP_OK 连接成功,其他失败。
*/
static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
{
SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? s_sta_connected_sem : s_sta_got_ip_sem;
@@ -254,6 +287,7 @@ static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
}
DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
}
/** @brief STA 模式连接状态。 */
s_sta_connected = true;
return ESP_OK;
}
@@ -262,7 +296,9 @@ static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
return ESP_ERR_TIMEOUT;
}
// 设置 STA 静态 IP
/**
* @brief 设置 STA 静态 IP
*/
static void wifi_set_static_ip(void)
{
if (WIFI_STA_USE_STATIC_IP) {
@@ -293,7 +329,11 @@ static void wifi_set_static_ip(void)
// ----------------------------------------
// ESP32 OTA 内部函数
// ----------------------------------------
/**
* @brief 初始化 ESP32 自身的 OTA 写入流程。
* @param total_size
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t esp32_ota_begin(size_t total_size)
{
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
@@ -327,16 +367,25 @@ static esp_err_t esp32_ota_begin(size_t total_size)
esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
/** @brief ESP32 OTA 目标分区。 */
s_esp32_update_partition = NULL;
return err;
}
/** @brief ESP32 OTA 已接收字节数。 */
s_esp32_received = 0;
/** @brief ESP32 自身 OTA 写入进行中标志。 */
s_esp32_ota_active = true;
DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
return ESP_OK;
}
/**
* @brief 将接收到的数据块写入 ESP32 OTA 分区。
* @param data
* @param len
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
{
if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
@@ -349,7 +398,11 @@ static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
return err;
}
// HTTP POST /ota/update - 完成 ESP32 OTA 并重启
/**
* @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
* @param req
* @return
*/
static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
{
(void)req;
@@ -364,6 +417,7 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
esp_err_t err = esp_ota_end(s_esp32_ota_handle);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err));
/** @brief ESP32 自身 OTA 写入进行中标志。 */
s_esp32_ota_active = false;
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
@@ -372,11 +426,13 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
err = esp_ota_set_boot_partition(s_esp32_update_partition);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
/** @brief ESP32 自身 OTA 写入进行中标志。 */
s_esp32_ota_active = false;
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
}
/** @brief ESP32 自身 OTA 写入进行中标志。 */
s_esp32_ota_active = false;
DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
@@ -389,7 +445,11 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /ota/update - 查询 ESP32 OTA 状态
/**
* @brief HTTP GET /ota/update - 查询 ESP32 OTA 状态
* @param req
* @return
*/
static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -408,7 +468,13 @@ static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
}
// 从 HTTP 流中读取完整的 JSON 头(以 } 结尾)
// 返回 JSON 字符串长度,失败返回 0
/**
* @brief 返回 JSON 字符串长度,失败返回 0
* @param req
* @param buf
* @param buf_size
* @return
*/
static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
{
int total = 0;
@@ -429,7 +495,11 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
// ----------------------------------------
// 统一 /ota/upload 处理器(自动识别目标)
// ----------------------------------------
/**
* @brief HTTP 文件上传处理器,接收固件并写入 Flash。
* @param req
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t ota_upload_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -592,6 +662,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
ota_meta_t meta;
meta_load(&meta);
if (meta.stored_size >= meta.total_size) {
/** @brief GD32 固件存储完成标志。 */
s_gd32_storage_done = true;
DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发...");
wifi_ota_signal_relay();
@@ -603,7 +674,11 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP POST /ota/relay 处理器
/**
* @brief HTTP POST /ota/relay 处理器
* @param req
* @return
*/
static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
{
(void)req;
@@ -619,7 +694,11 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /ota/status 处理器
/**
* @brief HTTP GET /ota/status 处理器
* @param req
* @return
*/
static esp_err_t ota_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -647,7 +726,11 @@ static esp_err_t ota_status_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /health 处理器
/**
* @brief HTTP GET /health 处理器
* @param req
* @return
*/
static esp_err_t health_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -669,7 +752,11 @@ static esp_err_t health_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /logs - 拉取并清空日志缓冲区
/**
* @brief HTTP GET /logs - 拉取并清空日志缓冲区
* @param req
* @return
*/
static esp_err_t logs_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -691,7 +778,11 @@ static esp_err_t logs_handler(httpd_req_t *req)
// HTTP POST /can/send 处理器
// 上位机发送 CAN 帧,经 ESP32 转发到 GD32
// 请求体: {"ch": 0, "id": 123456, "data": "0102030405060708", "dlc": 8}
// data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
/**
* @brief data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
* @param req
* @return
*/
static esp_err_t can_send_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -768,7 +859,11 @@ static esp_err_t can_send_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /can/status 处理器
/**
* @brief HTTP GET /can/status 处理器
* @param req
* @return
*/
static esp_err_t can_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -782,7 +877,11 @@ static esp_err_t can_status_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /wifi/status 处理器
/**
* @brief HTTP GET /wifi/status 处理器
* @param req
* @return
*/
static esp_err_t wifi_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -814,7 +913,11 @@ static esp_err_t wifi_status_handler(httpd_req_t *req)
return ESP_OK;
}
// HTTP GET /system/status 处理器
/**
* @brief HTTP GET /system/status 处理器
* @param req
* @return
*/
static esp_err_t system_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -864,7 +967,10 @@ static esp_err_t system_status_handler(httpd_req_t *req)
// ----------------------------------------
// 启动 HTTP OTA 服务器
// ----------------------------------------
/**
* @brief 启动 HTTP OTA 服务器,注册各接口路由。
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t http_ota_server_start(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
@@ -1055,7 +1161,9 @@ static esp_err_t http_ota_server_start(void)
// ----------------------------------------
// 公开 API
// ----------------------------------------
/**
* @brief 通知 OTA 中继开始向 GD32 转发固件。
*/
void wifi_ota_signal_relay(void)
{
if (s_relay_triggered) {
@@ -1063,6 +1171,7 @@ void wifi_ota_signal_relay(void)
return;
}
/** @brief OTA 中继是否已触发标志。 */
s_relay_triggered = true;
if (g_ota_dispatch_queue) {
@@ -1072,11 +1181,20 @@ void wifi_ota_signal_relay(void)
}
}
/**
* @brief 获取当前 WiFi 工作模式(AP 或 STA)。
* @return 当前 WiFi 模式枚举值。
*/
ota_wifi_mode_t wifi_get_mode(void)
{
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
return s_wifi_mode;
}
/**
* @brief 获取当前 WiFi 信号强度。
* @return 信号强度(dBm),未连接时返回无效值。
*/
int8_t wifi_get_rssi(void)
{
if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
@@ -1089,8 +1207,13 @@ int8_t wifi_get_rssi(void)
return -127;
}
/**
* @brief 判断 STA 是否已连接到上级 WiFi。
* @return true 已连接,false 未连接。
*/
bool wifi_is_connected(void)
{
/** @brief STA 模式连接状态。 */
return s_sta_connected;
}
@@ -1098,12 +1221,14 @@ bool wifi_is_connected(void)
// ★ WiFi 早期初始化(在 app_main 调度器启动前调用)
// 包括:NVS → netif → event handler → WiFi driver → 配置 → connect
// 返回:true = STA 模式已启动(正在连接),false = 需要切 AP
// ----------------------------------------
/**
* @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
* @return true STA 初始化中,false 需回退 AP。
*/
bool wifi_early_init(void)
{
DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
// NVS(WiFi 校准数据存储依赖 NVS)
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS 分区损坏或版本不匹配,擦除后重试
@@ -1117,8 +1242,9 @@ bool wifi_early_init(void)
DEBUG_INFO("WIFI", "NVS 初始化成功");
}
// 创建信号量
/** @brief STA 连接成功信号量。 */
s_sta_connected_sem = xSemaphoreCreateBinary();
/** @brief STA 获取 IP 信号量。 */
s_sta_got_ip_sem = xSemaphoreCreateBinary();
if (!s_sta_connected_sem || !s_sta_got_ip_sem) {
DEBUG_ERR("WIFI", "创建信号量失败");
@@ -1129,7 +1255,6 @@ bool wifi_early_init(void)
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册");
// 创建 STA netif(必须在 esp_wifi_init 之前)
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (sta_netif == NULL) {
sta_netif = esp_netif_create_default_wifi_sta();
@@ -1142,7 +1267,6 @@ bool wifi_early_init(void)
DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建");
}
// WiFi driver 初始化(必须在 netif 创建之后)
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
DEBUG_INFO("WIFI", "WiFi driver 初始化成功");
@@ -1169,7 +1293,10 @@ bool wifi_early_init(void)
// WiFi OTA FreeRTOS 任务
// WiFi 初始化已在 app_main 中通过 wifi_early_init() 完成
// 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控
// ----------------------------------------
/**
* @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void wifi_ota_task(void *pvParameters)
{
(void)pvParameters;
@@ -1185,13 +1312,13 @@ void wifi_ota_task(void *pvParameters)
} else {
DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
esp_wifi_stop(); // 先停 STA,再切 AP
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
s_wifi_mode = OTA_WIFI_MODE_AP;
wifi_init_ap();
}
}
// AP 模式:wifi_early_init 已完成,无需额外操作
// 启动 HTTP 服务器
esp_err_t err = http_ota_server_start();
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
@@ -1217,18 +1344,12 @@ void wifi_ota_task(void *pvParameters)
if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
}
// 心跳 + WiFi 状态监控
// 每秒一次循环:每 3 秒发心跳包,每 10 秒检查 RSSI
// WiFi 状态监控
// 每秒一次循环:每 10 秒检查 RSSI
uint32_t tick = 0;
while (1) {
tick++;
// 轻量心跳:每 3 秒发一次,上位机据此判断 ESP32 是否在线
if (tick % 3 == 0 && s_sta_connected) {
const char hb[] = "{\"t\":\"hb\"}\n";
debug_log_send((const uint8_t *)hb, sizeof(hb) - 1);
}
// 每 10 秒检查一次 RSSI
if (tick % 10 == 0) {
int8_t rssi = wifi_get_rssi();
+24 -8
View File
@@ -13,24 +13,40 @@ typedef enum {
OTA_WIFI_MODE_STA // STA 模式(连接路由器)
} ota_wifi_mode_t;
// WiFi OTA 任务入口
/**
* @brief WiFi OTA 任务入口,负责 HTTP 服务、上传处理和 OTA 触发流程。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void wifi_ota_task(void *pvParameters);
// ★ WiFi 早期初始化(app_main 调度前调用)
// 包括 NVS、WiFi driver、netif、event handler、配置、连接尝试
// 返回 true = STA 模式启动中,false = 需要在 task 中切换 AP
// 在 app_main 早期执行 WiFi 初始化并尝试以 STA 模式联网。
/**
* @brief 返回 true 表示 STA 初始化已开始,false 表示后续应回退到 AP 模式。
* @return true STA 初始化中,false 需回退 AP。
*/
bool wifi_early_init(void);
// 信号通知 OTA 中继开始(存储完成后调用)
/**
* @brief 在固件存储完成后通知 OTA 中继开始向 GD32 转发。
*/
void wifi_ota_signal_relay(void);
// 获取当前 WiFi 模式
/**
* @brief 获取当前 WiFi 工作模式(AP 或 STA)。
* @return 当前 WiFi 模式枚举值。
*/
ota_wifi_mode_t wifi_get_mode(void);
// 获取 WiFi RSSI
/**
* @brief 获取当前 WiFi RSSI,未连接时通常返回无效/默认值。
* @return 信号强度(dBm),未连接时返回无效值。
*/
int8_t wifi_get_rssi(void);
// STA 是否已连接
/**
* @brief 判断当前 STA 模式下是否已经连接到上级 WiFi。
* @return true 已连接,false 未连接。
*/
bool wifi_is_connected(void);
#endif // WIFI_OTA_H
+50 -4
View File
@@ -13,8 +13,12 @@
#include "protocol.h"
#include "monitor/debug_log.h"
/** @brief 协议层初始化标志。 */
static bool s_protocol_initialized = false;
/**
* @brief 初始化协议层,准备 CRC 和 AES 模块。
*/
void protocol_init(void)
{
if (s_protocol_initialized) return;
@@ -22,11 +26,17 @@ void protocol_init(void)
DEBUG_INFO("PROT", "=== 初始化协议层 ===");
DEBUG_INFO("PROT", "协议层功能: CRC16 Modbus + 硬件 AES-128-CBC");
/** @brief 协议层初始化标志。 */
s_protocol_initialized = true;
DEBUG_INFO("PROT", "协议层就绪");
}
// CRC16 Modbus(LE,低字节在前)
/**
* @brief CRC16 Modbus(LE,低字节在前)
* @param data 待校验的数据指针。
* @param len 数据长度(字节)。
* @return CRC16 校验值。
*/
uint16_t crc16_modbus(const uint8_t *data, size_t len)
{
uint16_t crc = 0xFFFF;
@@ -43,7 +53,14 @@ uint16_t crc16_modbus(const uint8_t *data, size_t len)
return (crc >> 8) | (crc << 8);
}
// 构建 SPI 帧: SOF + CMD + LEN(2LE) + PAYLOAD + CRC16(2LE)
/**
* @brief 构建 SPI 帧: SOF + CMD + LEN(2LE) + PAYLOAD + CRC16(2LE)
* @param cmd 命令码。
* @param payload 负载数据指针,可为 NULL。
* @param len 负载长度(字节)。
* @param out_frame 输出帧缓冲区。
* @return 帧总长度,失败返回 -1。
*/
int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame)
{
if (len > FRAME_MAX_PAYLOAD) {
@@ -253,7 +270,12 @@ void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
memcpy(nonce_out, state, 16);
}
// 会话密钥初始化
/**
* @brief 会话密钥初始化
* @param sess 待初始化的会话上下文。
* @param k_session 128 位会话密钥。
* @return 0 成功,-1 失败。
*/
int session_key_init(session_ctx_t *sess, const uint8_t *k_session)
{
if (!sess || !k_session) return -1;
@@ -266,6 +288,10 @@ int session_key_init(session_ctx_t *sess, const uint8_t *k_session)
return 0;
}
/**
* @brief 清除会话上下文中的密钥和状态。
* @param sess 待清除的会话上下文。
*/
void session_key_clear(session_ctx_t *sess)
{
if (sess) {
@@ -275,6 +301,14 @@ void session_key_clear(session_ctx_t *sess)
}
}
/**
* @brief 使用会话上下文加密数据。
* @param sess 会话上下文。
* @param in 输入明文。
* @param len 数据长度。
* @param out 输出密文缓冲区。
* @return 密文长度,失败返回 -1。
*/
int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
{
if (!sess || !in || !out) return -1;
@@ -291,6 +325,14 @@ int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t
return ret;
}
/**
* @brief 使用会话上下文解密数据。
* @param sess 会话上下文。
* @param in 输入密文。
* @param len 数据长度。
* @param out 输出明文缓冲区。
* @return 明文长度,失败返回 -1。
*/
int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
{
if (!sess || !in || !out) return -1;
@@ -307,7 +349,11 @@ int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t
return ret;
}
// 使用 ESP-IDF 硬件 RNG 生成随机数
/**
* @brief 使用 ESP-IDF 硬件 RNG 生成随机数
* @param nonce 输出缓冲区。
* @param len 随机数字节数。
*/
void generate_random_nonce(uint8_t *nonce, size_t len)
{
esp_fill_random(nonce, len);
+86 -15
View File
@@ -46,48 +46,119 @@ inline constexpr size_t CHIP_UID_LEN = 12;
typedef struct {
uint8_t key[16]; // 会话密钥 K_session
uint8_t iv[16]; // CBC 模式 IV
int used;
int used; // 会话是否已初始化标志
} session_ctx_t;
// 初始化协议层
/**
* @brief 初始化协议层资源,建立 CRC/AES 等基础能力的可用状态。
*/
void protocol_init(void);
// CRC16 Modbus 计算(低字节在前,LE)
/**
* @brief 计算一段数据的 CRC16-Modbus 校验值。
* @param data 待校验的数据指针。
* @param len 数据长度(字节)。
* @return CRC16 校验值。
*/
uint16_t crc16_modbus(const uint8_t *data, size_t len);
// 构建帧: 返回总帧长度
// 输出格式: SOF + CMD + LEN(2) + PAYLOAD + CRC16(2)
/**
* @brief 按协议格式构建一帧数据,返回生成后的总帧长度。
* @param cmd 命令码。
* @param payload 负载数据指针,可为 NULL。
* @param len 负载长度(字节)。
* @param out_frame 输出帧缓冲区。
* @return 帧总长度,失败返回 -1。
*/
int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame);
// 解析帧: 返回 payload 起始偏移,失败返回 -1
// 检查 SOF,验证 CRC16,提取 CMD 和 LEN
/**
* @brief 解析一帧协议数据,提取命令字、负载和长度,失败返回 -1。
* @param frame 待解析的帧数据。
* @param frame_len 帧数据总长度。
* @param out_cmd 输出参数,解析出的命令码。
* @param out_payload 输出缓冲区,存放解析出的负载。
* @param out_len 输出参数,负载实际长度。
* @return 0 成功,-1 失败。
*/
int frame_parse(const uint8_t *frame, size_t frame_len,
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len);
// T2.2: AES-128 CBC 加密
// 使用预设主密钥或会话密钥
// 注意: 函数名带 protocol_ 前缀以避免与 ESP-IDF wpa_supplicant 组件冲突
/**
* @brief 使用 AES-128-CBC 对明文进行加密,返回密文长度,失败返回负值。
* @param plaintext 明文数据。
* @param len 明文长度(字节)。
* @param key 128 位 AES 密钥。
* @param iv 128 位 CBC 初始向量。
* @param ciphertext_out 输出缓冲区,存放密文。
* @return 密文长度,失败返回 -1。
*/
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
const uint8_t *key, const uint8_t *iv,
uint8_t *ciphertext_out);
/**
* @brief 使用 AES-128-CBC 对密文进行解密,返回去除填充后的明文长度。
* @param ciphertext 密文数据。
* @param len 密文长度(字节,须为 16 的倍数)。
* @param key 128 位 AES 密钥。
* @param iv 128 位 CBC 初始向量。
* @param plaintext_out 输出缓冲区,存放解密后的明文。
* @return 明文长度,失败返回 -1。
*/
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
const uint8_t *key, const uint8_t *iv,
uint8_t *plaintext_out);
// T2.1: 搅伴算法 - 生成随机数
// 输入: 时间戳 + 内存地址 + rand_seed
// 输出: 16 字节 nonce_server
/**
* @brief 根据时间戳、内存地址和随机种子生成 16 字节随机数。
* @param timestamp 时间戳种子。
* @param mem_addr 内存地址种子。
* @param rand_seed 随机数种子。
* @param nonce_out 输出缓冲区,存放生成的 16 字节随机数。
*/
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
uint32_t rand_seed, uint8_t *nonce_out);
// T2.4: 会话密钥管理
/**
* @brief 用给定会话密钥初始化会话上下文。
* @param sess 待初始化的会话上下文。
* @param k_session 128 位会话密钥。
* @return 0 成功,-1 失败。
*/
int session_key_init(session_ctx_t *sess, const uint8_t *k_session);
/**
* @brief 清除会话上下文中的密钥与状态信息。
* @param sess 待清除的会话上下文。
*/
void session_key_clear(session_ctx_t *sess);
/**
* @brief 使用当前会话上下文对输入数据执行加密。
* @param sess 会话上下文。
* @param in 输入明文。
* @param len 数据长度。
* @param out 输出密文缓冲区。
* @return 密文长度,失败返回 -1。
*/
int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
/**
* @brief 使用当前会话上下文对输入数据执行解密。
* @param sess 会话上下文。
* @param in 输入密文。
* @param len 数据长度。
* @param out 输出明文缓冲区。
* @return 明文长度,失败返回 -1。
*/
int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
// 使用 ESP-IDF 硬件 RNG 生成随机数
/**
* @brief 使用 ESP-IDF 硬件 RNG 生成指定长度的随机数。
* @param nonce 输出缓冲区。
* @param len 随机数字节数。
*/
void generate_random_nonce(uint8_t *nonce, size_t len);
#endif // PROTOCOL_H
+22 -1
View File
@@ -15,9 +15,13 @@
#include "spi_master.h"
#include "monitor/debug_log.h"
/** @brief SPI2 从设备句柄。 */
spi_device_handle_t g_spi_device = NULL;
// SPI2 总线初始化
/**
* @brief SPI2 总线初始化
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_master_init(void)
{
DEBUG_INFO("SPI", "=== 初始化 SPI2 (VSPI) ===");
@@ -100,12 +104,27 @@ esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
}
// 仅发送
/**
* @brief 调用同步传输接口发送数据,不要求接收返回内容。
* @param cmd SPI 命令码。
* @param payload 发送数据指针。
* @param len 发送数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len)
{
return spi_sync_transfer(cmd, payload, len, NULL, 0, 3000);
}
// 仅接收
/**
* @brief 发送哑数据产生时钟,从从设备同步读取指定长度数据。
* @param cmd SPI 命令码。
* @param recv_buf 接收缓冲区。
* @param len 接收长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
{
uint8_t dummy[64] = {0};
@@ -182,6 +201,7 @@ static int can_frame_send(can_channel_t channel, uint32_t ext_id,
return 0;
}
// 对外暴露的 GD32 CAN 发送接口,会在必要时修正默认 DLC。
int gd32_can_send(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
@@ -193,6 +213,7 @@ int gd32_can_send(can_channel_t channel, uint32_t ext_id,
return can_frame_send(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
}
// 对外暴露的无响应 GD32 CAN 发送接口。
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc)
{
+49 -22
View File
@@ -38,47 +38,74 @@ typedef enum {
typedef struct {
can_channel_t channel; // CAN 通道 (0~2)
uint32_t ext_id; // 扩展帧 ID (29bit, Big-Endian 存储)
uint8_t data[CAN_MAX_DLC];
uint8_t data[CAN_MAX_DLC]; // CAN 数据负载 (最大 64 字节)
uint8_t dlc; // 数据长度 (1~64)
} can_frame_t;
// ============================================================
// 发送 CANFD 帧
// channel: CAN 通道 (0, 1, 2)
// ext_id: 扩展帧 ID (29bit)
// data: 数据内容 (1~64 字节)
// dlc: 数据长度 (自动识别,可传 0 由 data 长度决定)
// recv_buf: 接收缓冲区 (可传 NULL)
// recv_max: 期望接收最大字节数
// timeout_ms: 超时时间 (ms)
// 返回: >= 0 成功(实际接收字节数), < 0 失败
// ============================================================
/**
* @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。
* @param channel CAN 通道(0~2)。
* @param ext_id 29 位扩展帧 ID。
* @param data 数据负载指针。
* @param dlc 数据长度,0 则自动推断。
* @param recv_buf 接收缓冲区,可为 NULL。
* @param recv_max 期望最大接收字节数。
* @param timeout_ms 超时时间(毫秒)。
* @return >= 0 接收字节数,< 0 失败。
*/
int gd32_can_send(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms);
// 快速发送接口(不带响应接收)
// 用于广播/通知类 CAN 帧
/**
* @brief 发送一帧无需响应的 CANFD 数据,适合广播或通知场景。
* @param channel CAN 通道(0~2)。
* @param ext_id 29 位扩展帧 ID。
* @param data 数据负载指针。
* @param dlc 数据长度。
* @return >= 0 成功,< 0 失败。
*/
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc);
// ============================================================
// 初始化 SPI2 (VSPI) 总线和设备
/**
* @brief 初始化 SPI2 总线与目标从设备。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_master_init(void);
// 同步 SPI 传输(阻塞式)
// 发送 cmd + payload,接收响应到 recv_buf
// 返回: ESP_OK 成功, ESP_ERR_TIMEOUT 超时
/**
* @brief 执行一次同步 SPI 传输,发送命令和负载并可选接收返回数据。
* @param cmd SPI 命令码。
* @param payload 发送数据指针。
* @param payload_len 发送数据长度。
* @param recv_buf 接收缓冲区,可为 NULL。
* @param recv_len 期望接收长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
size_t payload_len, uint8_t *recv_buf,
size_t recv_len, uint32_t timeout_ms);
// 仅发送(不期望接收)
/**
* @brief 仅发送 SPI 数据,不关心返回内容。
* @param cmd SPI 命令码。
* @param payload 发送数据指针。
* @param len 发送数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len);
// 仅接收(发送哑数据)
/**
* @brief 仅接收 SPI 数据,底层会发送哑数据产生时钟。
* @param cmd SPI 命令码。
* @param recv_buf 接收缓冲区。
* @param len 接收长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms);
#endif // SPI_MASTER_H