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
+17 -2
View File
@@ -113,9 +113,24 @@ curl -X POST http://192.168.31.1/can/send \
| ota_relay | configMAX_PRIORITIES - 2 | 8192 | OTA 中继 |
| wifi_ota | configMAX_PRIORITIES - 3 | 8192 | WiFi OTA |
## 调试日志
## 调试日志与上位机通信
UDP 输出到上位机,端口 6666。上位机 WebUI 自动监听并显示。
当前与上位机相关的通信分成两类:
1. **业务通信:HTTP**
- WebUI 通过 HTTP 接口与 ESP32 通信
- 典型接口:`/system/status`、`/logs`、`/can/send`、`/ota/*`
- 其中日志面板实际通过 **轮询 `GET /logs`** 获取日志
2. **调试镜像:UART0 + UDP**
- 所有 `DEBUG_*` 日志会同时写到 UART0、UDP、以及内部环形缓冲区
- UDP 目标端口为 `6666`,适合独立调试接收器使用
- 但 **当前 tools WebUI 不直接监听 UDP 6666**
因此,当前 WebUI 场景下:
- `/system/status` 用于在线状态/健康状态展示
- `/logs` 用于日志展示
- 不再需要额外的 `{"t":"hb"}` 假日志心跳包
## 编译
@@ -1,233 +0,0 @@
/*
* health_monitor.cpp - 系统健康监控
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控 + 电压监控
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "driver/temperature_sensor.h"
#include "health_monitor.h"
#include "debug_log.h"
health_state_t g_health_state = SYS_NORMAL;
static SemaphoreHandle_t g_health_mutex = NULL;
static temperature_sensor_handle_t s_temp_handle = NULL;
// ----------------------------------------
// 健康状态管理
// ----------------------------------------
void health_monitor_init(void)
{
g_health_mutex = xSemaphoreCreateMutex();
if (g_health_mutex == NULL) {
DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
}
}
health_state_t health_get_current(void)
{
health_state_t state;
if (g_health_mutex) {
xSemaphoreTake(g_health_mutex, portMAX_DELAY);
}
state = g_health_state;
if (g_health_mutex) {
xSemaphoreGive(g_health_mutex);
}
return state;
}
// ----------------------------------------
// 温度监控(定时器回调)
// ----------------------------------------
static void temp_monitor_callback(void *arg)
{
float temp = 0.0f;
esp_err_t err = temperature_sensor_get_celsius(s_temp_handle, &temp);
if (err != ESP_OK) {
DEBUG_WARN("TEMP", "读取温度失败: %s", esp_err_to_name(err));
return;
}
DEBUG_DBG("TEMP", "芯片温度: %.1f°C", temp);
if (temp >= TEMP_CRITICAL) {
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) {
g_health_state = SYS_WARNING;
DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
}
}
}
// ----------------------------------------
// 电压监控(启动时调用一次)
// ----------------------------------------
static void voltage_monitor_callback(void *arg)
{
float voltage = 3.30f; // ESP32-C3 无内置电压传感器,使用估算
DEBUG_DBG("VOLT", "VDD 电压: %.2fV (估算)", voltage);
if (voltage < 2.3f) {
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) {
g_health_state = SYS_WARNING;
DEBUG_WARN("VOLT", "警告: 电压 %.2fV < 2.5V", voltage);
DEBUG_WARN("VOLT", "健康状态变更为: SYS_WARNING");
}
} else {
DEBUG_INFO("VOLT", "电压正常: %.2fV", voltage);
}
}
// ----------------------------------------
// 温度电压监控任务入口
// ----------------------------------------
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) {
DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
} else {
ESP_ERROR_CHECK(temperature_sensor_enable(s_temp_handle));
DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)");
}
// 定时器,每5秒读取一次温度
esp_timer_handle_t temp_timer;
esp_timer_create_args_t timer_args = {};
timer_args.callback = &temp_monitor_callback;
timer_args.arg = nullptr;
timer_args.name = "temp_monitor";
timer_args.dispatch_method = ESP_TIMER_TASK;
timer_args.skip_unhandled_events = false;
esp_timer_create(&timer_args, &temp_timer);
esp_timer_start_periodic(temp_timer, 5 * 1000000); // 5秒
DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 5 秒)");
// 启动时检查一次电压
voltage_monitor_callback(NULL);
// 每5秒打印一次健康状态
uint32_t loop_count = 0;
while (1) {
loop_count++;
uint32_t free_heap = esp_get_free_heap_size();
const char *state_str;
switch (g_health_state) {
case SYS_NORMAL: state_str = "正常"; break;
case SYS_WARNING: state_str = "警告"; break;
case SYS_CRITICAL: state_str = "危险"; break;
case SYS_SAFE_SHUTDOWN: state_str = "安全关机"; break;
default: state_str = "未知"; break;
}
DEBUG_INFO("MON", "--- 健康状态 #%lu ---", (unsigned long)loop_count);
DEBUG_INFO("MON", "健康状态: %s (%d)", state_str, g_health_state);
DEBUG_INFO("MON", "空闲堆: %lu 字节", (unsigned long)free_heap);
// 堆内存警告
if (free_heap < HEAP_WARNING_MIN) {
DEBUG_WARN("MON", "空闲堆内存不足: %lu < %d", (unsigned long)free_heap, HEAP_WARNING_MIN);
}
DEBUG_INFO("MON", "===================");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
// ----------------------------------------
// 健康事件日志
// ----------------------------------------
void health_log_event(const char *event, int value)
{
DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
}
// ----------------------------------------
// OTA 传输前的健康检查
// ----------------------------------------
health_status_t health_check_before_ota(void)
{
health_status_t status = HEALTH_MON_OK;
if (g_health_state >= SYS_SAFE_SHUTDOWN) {
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输");
return HEALTH_MON_SHUTDOWN;
} else if (g_health_state >= SYS_CRITICAL) {
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输");
return HEALTH_MON_CRITICAL;
} else if (g_health_state >= SYS_WARNING) {
status = HEALTH_MON_WARNING;
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
}
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
if (ap_info.rssi < WIFI_RSSI_CRITICAL) {
g_health_state = SYS_CRITICAL;
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (危险阈值)",
ap_info.rssi, WIFI_RSSI_CRITICAL);
return HEALTH_MON_CRITICAL;
} else if (ap_info.rssi < WIFI_RSSI_NORMAL) {
if (status < HEALTH_MON_WARNING) {
status = HEALTH_MON_WARNING;
}
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (警告阈值)",
ap_info.rssi, WIFI_RSSI_NORMAL);
}
}
if (status == HEALTH_MON_OK) {
DEBUG_DBG("HEALTH", "OTA 前检查: 正常");
}
return status;
}
health_status_t health_update_from_wifi_rssi(int8_t rssi)
{
if (rssi < WIFI_RSSI_CRITICAL) {
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) {
g_health_state = SYS_WARNING;
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
health_log_event("WIFI警告", rssi);
}
return HEALTH_MON_WARNING;
}
if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
g_health_state = SYS_NORMAL;
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
health_log_event("WIFI恢复", rssi);
}
return HEALTH_MON_OK;
}
@@ -1,37 +0,0 @@
/*
* health_monitor.h - 系统健康监控头文件
*/
#pragma once
#include <stdint.h>
#include "app_config.h"
// T9.3: 健康状态等级
typedef enum {
SYS_NORMAL = 0, // 正常
SYS_WARNING = 1, // 警告
SYS_CRITICAL = 2, // 危险
SYS_SAFE_SHUTDOWN = 3 // 安全关机
} health_state_t;
typedef enum {
HEALTH_MON_OK = 0,
HEALTH_MON_WARNING = 1,
HEALTH_MON_CRITICAL = 2,
HEALTH_MON_SHUTDOWN = 3
} health_status_t;
// 外部共享状态(互斥锁保护)
extern health_state_t g_health_state;
// WiFi RSSI 阈值(来自 app_config.h)
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
// 函数声明
void health_monitor_init(void);
void health_monitor_task(void *pvParameters);
health_status_t health_check_before_ota(void);
void health_log_event(const char *event, int value);
health_state_t health_get_current(void);
health_status_t health_update_from_wifi_rssi(int8_t rssi);
@@ -1,972 +0,0 @@
/*
* wifi_ota.c - WiFi OTA 服务器实现
* T4.1 - T4.5
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_ota_ops.h"
#include "nvs_flash.h"
#include "lwip/netdb.h"
#include "esp_netif.h"
#include "esp_netif_types.h"
#include "esp_event.h"
#include "esp_mac.h"
#include "app_config.h"
#include "wifi_ota.h"
#include "ota_relay.h"
#include "health_monitor.h"
#include "monitor/debug_log.h"
#include "spi_master.h"
#include "cJSON.h"
static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
static bool s_relay_triggered = false;
static bool s_sta_connected = false;
static int s_sta_retry_count = 0;
static SemaphoreHandle_t s_sta_connected_sem = NULL;
static SemaphoreHandle_t s_sta_got_ip_sem = NULL;
// ESP32 OTA 状态
static bool s_esp32_ota_active = false;
static esp_ota_handle_t s_esp32_ota_handle = 0;
static const esp_partition_t *s_esp32_update_partition = NULL;
static size_t s_esp32_received = 0;
// GD32 OTA 存储状态
static bool s_gd32_storage_done = false;
static httpd_handle_t s_http_server = NULL;
static void wifi_set_static_ip(void);
// ----------------------------------------
// WiFi/IP 事件回调(事件驱动连接)
// ----------------------------------------
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT) {
switch (event_id) {
case WIFI_EVENT_STA_START:
DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
esp_wifi_connect();
break;
case WIFI_EVENT_STA_CONNECTED:
DEBUG_INFO("WIFI", "STA 已关联到 AP");
s_sta_connected = true;
s_sta_retry_count = 0;
if (s_sta_connected_sem) {
xSemaphoreGive(s_sta_connected_sem);
}
break;
case WIFI_EVENT_STA_DISCONNECTED: {
wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count);
s_sta_connected = false;
if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
s_sta_retry_count++;
DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
esp_wifi_connect();
} else {
DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
}
break;
}
case WIFI_EVENT_AP_STACONNECTED: {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 连入, AID=%d", MAC2STR(event->mac), event->aid);
break;
}
case WIFI_EVENT_AP_STADISCONNECTED: {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 离开, AID=%d, reason=%d",
MAC2STR(event->mac), event->aid, event->reason);
break;
}
default:
break;
}
} else if (event_base == IP_EVENT) {
switch (event_id) {
case IP_EVENT_STA_GOT_IP: {
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
if (s_sta_got_ip_sem) {
xSemaphoreGive(s_sta_got_ip_sem);
}
break;
}
default:
break;
}
}
}
// ----------------------------------------
// WiFi AP 模式初始化
// ----------------------------------------
static esp_err_t wifi_init_ap(void)
{
DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
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");
}
wifi_config_t wifi_config = {};
memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID) + 1);
wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD) + 1);
wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
wifi_config.ap.channel = WIFI_AP_CHANNEL;
DEBUG_INFO("WIFI", "AP SSID: %s, Channel: %d", WIFI_AP_SSID, WIFI_AP_CHANNEL);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1");
DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1");
DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT);
return ESP_OK;
}
// ----------------------------------------
// WiFi STA 模式初始化
// ----------------------------------------
static esp_err_t wifi_init_sta(void)
{
DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
s_wifi_mode = OTA_WIFI_MODE_STA;
s_sta_retry_count = 0;
// ★ 设置国家代码:中国,允许信道 1-13
// ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
wifi_country_t country = {};
memcpy(country.cc, "CN", 3);
country.schan = 1;
country.nchan = 13;
country.policy = WIFI_COUNTRY_POLICY_MANUAL;
wifi_config_t wifi_config = {};
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
wifi_config.sta.pmf_cfg.capable = true;
wifi_config.sta.pmf_cfg.required = false;
wifi_config.sta.listen_interval = 3;
memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, strlen(WIFI_STA_SSID) + 1);
memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, strlen(WIFI_STA_PASSWORD) + 1);
DEBUG_INFO("WIFI", "STA SSID: %s (全信道扫描, CN 国家代码)", WIFI_STA_SSID);
ESP_ERROR_CHECK(esp_wifi_set_country(&country));
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());
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
DEBUG_INFO("WIFI", "WiFi STA 启动, SSID: %s, 事件驱动连接...", WIFI_STA_SSID);
return ESP_OK;
}
// 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
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;
const char *wait_name = WIFI_STA_USE_STATIC_IP ? "STA_CONNECTED" : "STA_GOT_IP";
if (wait_sem == NULL) {
DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
return ESP_ERR_INVALID_STATE;
}
TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
if (xSemaphoreTake(wait_sem, timeout_ticks) == pdTRUE) {
esp_netif_ip_info_t ip_info;
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
if (ip_str) {
snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
}
DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
}
s_sta_connected = true;
return ESP_OK;
}
DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
return ESP_ERR_TIMEOUT;
}
// 设置 STA 静态 IP
static void wifi_set_static_ip(void)
{
if (WIFI_STA_USE_STATIC_IP) {
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (sta_netif) {
DEBUG_INFO("WIFI", "STA 使用静态 IP 模式,停止 DHCP Client");
ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
esp_netif_ip_info_t info;
memset(&info, 0, sizeof(info));
inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
// 设置 DNS
esp_netif_dns_info_t dns_info;
memset(&dns_info, 0, sizeof(dns_info));
inet_pton(AF_INET, WIFI_STA_STATIC_DNS, &dns_info.ip.u_addr.ip4.addr);
ESP_ERROR_CHECK(esp_netif_set_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns_info));
DEBUG_INFO("WIFI", "DNS 已设置: %s", WIFI_STA_STATIC_DNS);
}
} else {
DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
}
}
// ----------------------------------------
// ESP32 OTA 内部函数
// ----------------------------------------
static esp_err_t esp32_ota_begin(size_t total_size)
{
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);
s_esp32_update_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (s_esp32_update_partition == NULL ||
s_esp32_update_partition->address == boot_partition->address) {
s_esp32_update_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (s_esp32_update_partition == NULL) {
DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
return ESP_FAIL;
}
DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx",
s_esp32_update_partition->label,
s_esp32_update_partition->size,
s_esp32_update_partition->address);
if (total_size > s_esp32_update_partition->size) {
DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
total_size, s_esp32_update_partition->size);
return ESP_FAIL;
}
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));
s_esp32_update_partition = NULL;
return err;
}
s_esp32_received = 0;
s_esp32_ota_active = true;
DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
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) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len);
if (err == ESP_OK) {
s_esp32_received += len;
}
return err;
}
// HTTP POST /ota/update - 完成 ESP32 OTA 并重启
static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
{
(void)req;
if (!s_esp32_ota_active) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
return ESP_FAIL;
}
DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", s_esp32_received);
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));
s_esp32_ota_active = false;
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
}
err = esp_ota_set_boot_partition(s_esp32_update_partition);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
s_esp32_ota_active = false;
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
}
s_esp32_ota_active = false;
DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
return ESP_OK;
}
// HTTP GET /ota/update - 查询 ESP32 OTA 状态
static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
{
char resp[128];
if (s_esp32_ota_active) {
snprintf(resp, sizeof(resp),
"{\"active\": true, \"received\": %u, \"partition\": \"%s\"}",
(unsigned int)s_esp32_received,
s_esp32_update_partition ? s_esp32_update_partition->label : "null");
} else {
snprintf(resp, sizeof(resp), "{\"active\": false}");
}
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// ----------------------------------------
// 统一 /ota/upload 处理器(自动识别目标)
// ----------------------------------------
static esp_err_t ota_upload_handler(httpd_req_t *req)
{
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
// 先读取一小段判断是 JSON 还是 binary
char header[256];
int header_read = httpd_req_recv(req, header, sizeof(header) - 1);
if (header_read <= 0) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
return ESP_FAIL;
}
header[header_read] = '\0';
bool is_esp32_ota = false;
uint32_t total_size = 0;
// 尝试解析 JSON
if (header_read > 0 && header[0] == '{') {
cJSON *root = cJSON_Parse(header);
if (root) {
cJSON *type_item = cJSON_GetObjectItem(root, "type");
if (type_item && cJSON_IsString(type_item) &&
strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
is_esp32_ota = true;
DEBUG_INFO("WIFI", "检测到 ESP32 OTA");
}
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
if (size_item) {
total_size = (uint32_t)cJSON_GetNumberValue(size_item);
}
cJSON_Delete(root);
}
}
if (is_esp32_ota) {
// --- ESP32 OTA ---
if (total_size == 0) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
return ESP_FAIL;
}
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");
return ESP_FAIL;
}
// 先处理已读的 header
err = esp32_ota_write((const uint8_t *)header, header_read);
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "OTA write 失败");
return ESP_FAIL;
}
// 继续读剩余数据
size_t total_received = header_read;
while (total_received < req->content_len) {
size_t to_read = req->content_len - total_received;
if (to_read > 4096) to_read = 4096;
int ret = httpd_req_recv(req, header, to_read);
if (ret <= 0) break;
esp_err_t err = esp32_ota_write((const uint8_t *)header, ret);
if (err != ESP_OK) break;
total_received += ret;
if (total_size > 0 && s_esp32_received % (total_size / 10 + 1) == 0) {
DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%u%%)",
(unsigned int)s_esp32_received, total_size,
(unsigned int)((s_esp32_received * 100) / total_size));
}
}
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节", (unsigned int)s_esp32_received);
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// --- GD32 OTA(原逻辑)---
if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
DEBUG_ERR("WIFI", "固件太大: %d > %d", req->content_len, OTA_MAX_FIRMWARE_SIZE);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
return ESP_FAIL;
}
uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
int received = 0;
// 先处理已读的 header
if (header_read >= 6) {
uint16_t seq = header[0] | (header[1] << 8);
uint32_t fw_size = header[2] | (header[3] << 8) | (header[4] << 16) | (header[5] << 24);
if (seq == 0) {
ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
.chunk_count = (fw_size + 4095) / 4096, .checksum = {0} };
meta_save(&meta);
DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
}
}
// 继续读剩余数据
while (received + header_read < req->content_len) {
size_t to_read = req->content_len - received - header_read;
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
int ret = httpd_req_recv(req, (char *)buf, to_read);
if (ret <= 0) break;
uint32_t flash_offset = ((received + header_read) / OTA_BLOCK_SIZE) * OTA_BLOCK_SIZE;
esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
if (err == ESP_OK) {
meta_update(flash_offset + ret, (received + header_read) / OTA_BLOCK_SIZE);
}
received += ret;
uint32_t progress = ((received + header_read) * 100) / req->content_len;
if (progress % 10 == 0) {
DEBUG_INFO("WIFI", "GD32 上传进度: %d / %d (%u%%)",
received + header_read, req->content_len, progress);
}
}
free(buf);
DEBUG_INFO("WIFI", "GD32 固件存储完成: %d 字节", received + header_read);
ota_meta_t meta;
meta_load(&meta);
if (meta.stored_size >= meta.total_size) {
s_gd32_storage_done = true;
DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发...");
wifi_ota_signal_relay();
}
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// HTTP POST /ota/relay 处理器
static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
{
(void)req;
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
wifi_ota_signal_relay();
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// HTTP GET /ota/status 处理器
static esp_err_t ota_status_handler(httpd_req_t *req)
{
DEBUG_INFO("WIFI", "=== HTTP: GET /ota/status ===");
ota_meta_t meta;
meta_load(&meta);
char resp[256];
int len = snprintf(resp, sizeof(resp),
"{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
"\"storage_done\": %s, \"relay_state\": %d}",
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
(unsigned long)meta.last_seq,
s_gd32_storage_done ? "true" : "false",
g_relay_state);
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s",
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
s_gd32_storage_done ? "true" : "false");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
return ESP_OK;
}
// HTTP GET /health 处理器
static esp_err_t health_handler(httpd_req_t *req)
{
DEBUG_INFO("WIFI", "=== HTTP: GET /health ===");
health_state_t state = health_get_current();
uint32_t free_heap = esp_get_free_heap_size();
char resp[128];
int len = snprintf(resp, sizeof(resp),
"{\"health\": %d, \"free_heap\": %lu}",
state, (unsigned long)free_heap);
DEBUG_INFO("WIFI", "健康状态: %d, 空闲堆: %lu", state, (unsigned long)free_heap);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
return ESP_OK;
}
// HTTP POST /can/send 处理器
// 上位机发送 CAN 帧,经 ESP32 转发到 GD32
// 请求体: {"ch": 0, "id": 123456, "data": "0102030405060708", "dlc": 8}
// data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
static esp_err_t can_send_handler(httpd_req_t *req)
{
char buf[256] = {0};
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
return ESP_FAIL;
}
buf[ret] = '\0';
cJSON *root = cJSON_Parse(buf);
if (!root) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
return ESP_FAIL;
}
cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
cJSON *id_item = cJSON_GetObjectItem(root, "id");
cJSON *data_item = cJSON_GetObjectItem(root, "data");
cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
if (!ch_item || !id_item) {
cJSON_Delete(root);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
return ESP_FAIL;
}
can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
uint8_t tx_data[CAN_MAX_DLC] = {0};
uint8_t tx_dlc = 0;
if (data_item && cJSON_IsString(data_item)) {
const char *hex_str = cJSON_GetStringValue(data_item);
size_t hex_len = strlen(hex_str);
for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
tx_dlc++;
}
}
if (tx_dlc == 0 && dlc > 0) {
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
}
cJSON_Delete(root);
// 发送 CAN 帧到 GD32
uint8_t resp_data[CAN_MAX_DLC] = {0};
int resp_len = gd32_can_send(ch, ext_id, tx_data, tx_dlc,
resp_data, sizeof(resp_data), 1000);
char resp[512];
if (resp_len >= 0) {
// 将响应 data 转为 hex 字符串
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
for (int i = 0; i < resp_len; i++) {
sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
}
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
} else {
snprintf(resp, sizeof(resp),
"{\"status\": \"error\", \"code\": %d}", resp_len);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// HTTP GET /can/status 处理器
static esp_err_t can_status_handler(httpd_req_t *req)
{
char resp[128];
snprintf(resp, sizeof(resp),
"{\"channels\": 3, \"max_dlc\": %d, \"spi_init\": %s}",
CAN_MAX_DLC,
g_spi_device != NULL ? "true" : "false");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// HTTP GET /wifi/status 处理器
static esp_err_t wifi_status_handler(httpd_req_t *req)
{
char resp[256];
char ip_str[16] = "0.0.0.0";
int8_t rssi = -127;
if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
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) {
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
}
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
rssi = ap_info.rssi;
}
}
int len = snprintf(resp, sizeof(resp),
"{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
ip_str, rssi,
(s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
return ESP_OK;
}
// ----------------------------------------
// 启动 HTTP OTA 服务器
// ----------------------------------------
static esp_err_t http_ota_server_start(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = HTTP_SERVER_PORT;
config.stack_size = HTTP_SERVER_STACK_SIZE;
config.max_uri_handlers = 16; // 默认只有8个槽位,我们有9个端点,不够
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
esp_err_t err = httpd_start(&s_http_server, &config);
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
return err;
}
httpd_uri_t upload_uri = {
.uri = "/ota/upload",
.method = HTTP_POST,
.handler = ota_upload_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &upload_uri);
httpd_uri_t relay_uri = {
.uri = "/ota/relay",
.method = HTTP_POST,
.handler = ota_relay_trigger_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &relay_uri);
httpd_uri_t status_uri = {
.uri = "/ota/status",
.method = HTTP_GET,
.handler = ota_status_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &status_uri);
httpd_uri_t esp32_update_uri = {
.uri = "/ota/update",
.method = HTTP_POST,
.handler = esp32_ota_update_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &esp32_update_uri);
httpd_uri_t esp32_status_uri = {
.uri = "/ota/esp32_status",
.method = HTTP_GET,
.handler = esp32_ota_status_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &esp32_status_uri);
httpd_uri_t health_uri = {
.uri = "/health",
.method = HTTP_GET,
.handler = health_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &health_uri);
httpd_uri_t wifi_status_uri = {
.uri = "/wifi/status",
.method = HTTP_GET,
.handler = wifi_status_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &wifi_status_uri);
// CAN 帧转发
httpd_uri_t can_send_uri = {
.uri = "/can/send",
.method = HTTP_POST,
.handler = can_send_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &can_send_uri);
httpd_uri_t can_status_uri = {
.uri = "/can/status",
.method = HTTP_GET,
.handler = can_status_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(s_http_server, &can_status_uri);
DEBUG_INFO("WIFI", "HTTP 服务器已启动");
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /can/send, /can/status");
return ESP_OK;
}
// ----------------------------------------
// 公开 API
// ----------------------------------------
void wifi_ota_signal_relay(void)
{
if (s_relay_triggered) {
DEBUG_WARN("WIFI", "转发已经触发过");
return;
}
s_relay_triggered = true;
if (g_ota_dispatch_queue) {
uint32_t msg = 1;
xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
}
}
ota_wifi_mode_t wifi_get_mode(void)
{
return s_wifi_mode;
}
int8_t wifi_get_rssi(void)
{
if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
return -127;
}
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
return ap_info.rssi;
}
return -127;
}
bool wifi_is_connected(void)
{
return s_sta_connected;
}
// ----------------------------------------
// WiFi OTA FreeRTOS 任务
// ----------------------------------------
void wifi_ota_task(void *pvParameters)
{
(void)pvParameters;
DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
// 初始化 NVS
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
} else {
DEBUG_INFO("WIFI", "NVS 初始化成功");
}
// 初始化网络接口(可安全重复调用)
esp_netif_init();
DEBUG_INFO("WIFI", "网络接口初始化成功");
// 创建默认事件循环(必须在 esp_wifi_init 之前)
esp_err_t ev_ret = esp_event_loop_create_default();
if (ev_ret == ESP_OK) {
DEBUG_INFO("WIFI", "默认事件循环已创建");
} else if (ev_ret == ESP_ERR_INVALID_STATE) {
DEBUG_INFO("WIFI", "默认事件循环已存在,跳过");
} else {
DEBUG_ERR("WIFI", "创建事件循环失败: %s", esp_err_to_name(ev_ret));
}
// ★ 创建信号量(用于事件驱动等待)
s_sta_connected_sem = xSemaphoreCreateBinary();
s_sta_got_ip_sem = xSemaphoreCreateBinary();
if (!s_sta_connected_sem || !s_sta_got_ip_sem) {
DEBUG_ERR("WIFI", "创建信号量失败");
}
// ★ 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册");
// ★ 创建 STA netif(必须在 esp_wifi_init 之前!)
// esp_netif_create_default_wifi_sta() 注册默认 STA 事件处理
// 如果在 esp_wifi_init() 之后调用,部分内部 handler 不会正确绑定
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();
if (sta_netif == NULL) {
DEBUG_ERR("WIFI", "创建 STA netif 失败");
} else {
DEBUG_INFO("WIFI", "STA netif 已创建");
}
} else {
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 初始化成功");
// STA 优先,失败自动切 AP(事件驱动连接 + 自动重连)
// 注意:CONFIG_WIFI_MODE 是 constexpr,不能用 #if(预处理器不认识 C++ 变量)
if (CONFIG_WIFI_MODE == 1) {
DEBUG_INFO("WIFI", "工作模式: STA (优先)");
err = wifi_init_sta();
if (err == ESP_OK) {
char ip_str[16] = {0};
// 等待连接成功
err = wifi_wait_sta_connected(ip_str, sizeof(ip_str));
if (err == ESP_OK) {
DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
} else {
DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
esp_wifi_stop();
s_wifi_mode = OTA_WIFI_MODE_AP;
wifi_init_ap();
}
}
} else {
DEBUG_INFO("WIFI", "工作模式: AP");
wifi_init_ap();
}
// 启动 HTTP 服务器
err = http_ota_server_start();
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
}
DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
// 定期检查 WiFi 状态
while (1) {
int8_t rssi = wifi_get_rssi();
if (rssi != -127) {
health_update_from_wifi_rssi(rssi);
if (rssi < WIFI_RSSI_CRITICAL) {
DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL);
} else if (rssi < WIFI_RSSI_WARNING) {
DEBUG_WARN("WIFI", "WiFi RSSI 较弱: %d dBm", rssi);
}
}
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
+74
View File
@@ -0,0 +1,74 @@
# ESP32_COMM 通信梳理(2026-05-05)
## 结论
当前“ESP32 ↔ 上位机(tools WebUI)”的主通信方式是 **HTTP**,不是 UDP。
### WebUI 实际在用的接口
- `GET /system/status`:每 1 秒轮询一次,用于在线状态、WiFi、heap、温度、电压、OTA 状态展示
- `GET /logs`:每 1 秒轮询一次,用于拉取并清空日志环形缓冲区
- `POST /can/send`:发送 CAN 请求到 GD32
- `POST /ota/upload` / `POST /ota/relay` / `POST /ota/update`:OTA 相关
## 日志链路
所有 `DEBUG_*` 日志目前会并行写入三处:
1. **UART0**
- 本地串口调试
2. **UDP 单播** → `DEBUG_LOG_IP:DEBUG_LOG_PORT`
- 可供独立 UDP 调试接收端使用
3. **内部环形缓冲区**
- 被 `GET /logs` 读取
- 这是当前 WebUI 日志面板真正使用的通道
## 为什么 `{"t":"hb"}` 是多余的
之前在 `wifi_ota_task()` 里,每 3 秒会发送一次:
```json
{"t":"hb"}
```
它是通过 `debug_log_send()` 发出的,所以会同时进入:
- UART
- UDP
- `/logs` 对应的环形缓冲区
这会导致 WebUI 把它显示成一条普通日志:
- level: `I`
- tag: `ESP32`
- message: `{"t":"hb"}`
但 WebUI 本来已经每秒轮询 `/system/status`,完全可以判断设备在线,不需要再靠这种“伪日志心跳”。
## 已处理
已删除 `wifi_ota_task()` 中每 3 秒发送一次的 `{"t":"hb"}`。
## 现在是否还有重复/不必要通信
### 对 WebUI 来说
有两条并行链路,但用途不同,不算重复:
- `/system/status`:结构化状态
- `/logs`:文本日志
这两条都保留是合理的。
### 对底层日志输出来说
日志同时发 UART + UDP + ring buffer,属于“一份日志三路镜像”。
这对调试友好,但从“最简设计”角度看,**UDP 在当前 WebUI 场景里不是必需的**。
也就是说:
- **必须保留:** ring buffer + `/logs`
- **建议保留:** UART0
- **可选保留:** UDP 6666
如果后续你想继续精简,可以做“方案2”:把 UDP 也做成可关闭,甚至默认关闭。
+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