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:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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__)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user