refactor(debug): 重构调试日志环形缓冲区管理

- 将日志环形缓冲区从静态数组改为动态堆分配
- 实现启动时大容量缓冲区和运行时小容量缓冲区的切换机制
- 添加日志环满时优先保留错误和警告信息的清理策略
- 优化内存使用,在日志为空时避免不必要的内存分配
- 移除调试日志中的冗余字段显示

feat(api): 更新CAN接口为批量发送模式

- 将 `/can/send` 接口替换为 `/can/send_batch`,支持1到N帧的批量发送
- 更新API文档中的接口描述和使用示例
- 修改请求参数格式为 `{"ch":0,"frames":[{...}]}` 返回格式为 `{"status":"ok","results":[...]}`
- 单帧发送也需要通过send_batch接口实现

feat(monitor): 新增GD32 ADC调试反馈处理功能

- 添加gd32_adc_feedback组件用于解析0xC4类型的SPI反馈
- 实现ADC数据解析包括温度、电压等参数的计算和转换
- 在can_feedback_task中集成ADC反馈分发逻辑
- 添加线程安全的数据获取和年龄检查接口

refactor(config): 优化任务栈大小配置并调整OTA块大小

- 重新评估各任务栈使用情况,优化HTTP、WiFi、SPI等工作任务的栈大小
- 将HTTP服务器栈从8KB调整至7KB,WiFi OTA任务栈从4KB调整至3KB
- OTA块大小从2KB调整回4KB以提升传输吞吐性能
- 重新整理配置文件结构,移动WiFi模式配置位置
This commit is contained in:
小龙虾
2026-07-20 03:10:42 +08:00
parent b807645a20
commit 00b716c8db
66 changed files with 4803 additions and 1074 deletions
+3 -3
View File
@@ -14,10 +14,10 @@ idf_component_register(
"server/ble_ota_host.cpp" # BLE OTA GATT/GAP/广播/Host
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存/栈水位主动巡检)
"monitor/cpu_stats.cpp" # 1Hz FreeRTOS CPU 占用采样任务
"monitor/can_feedback_task.cpp" # CAN 反馈被动监听(0xC1 takeEvent)
"monitor/can_feedback_task.cpp" # CAN 反馈被动监听(0xC1 takeEvent)+ 分发 0xC4
"monitor/gd32_adc_feedback.cpp" # GD32 ADC 调试反馈缓存(0xC4)
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
"monitor/task_stack_check.cpp" # 任务栈水位指标收集(health_mon 巡检 + /system/status 共享)
"monitor/stack_overflow_hook.cpp" # FreeRTOS 栈溢出钩子(canary 触发时 UART 硬写告警)
"monitor/task_stack.cpp" # 任务栈水位收集 + 溢出钩子
INCLUDE_DIRS
"."
"core"
+23 -22
View File
@@ -10,6 +10,12 @@
#include "freertos/FreeRTOS.h"
#include "version.h"
// ============================================================
// 功能开关(ESP32 本机;GD32 侧开关随 HELLO/PING 身份块上报,无需镜像)
// ============================================================
// 0 = AP 热点,1 = STA 连路由器
inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
// ============================================================
// 固件版本(由 version.h 自动生成,此处提供 C++ constexpr 访问)
// ============================================================
@@ -17,13 +23,6 @@ inline constexpr const char *FW_VERSION = FW_VERSION_STRING; ///< 固件版
inline constexpr const char *FW_BUILD_TIME = BUILD_TIME; ///< 编译时间字符串(自动生成)
inline constexpr const char *FW_GIT_HASH = GIT_HASH; ///< Git 提交哈希(自动生成)
// ============================================================
// WiFi 工作模式选择
// ============================================================
// 0 = AP 模式(设备当热点)
// 1 = STA 模式(连接路由器)
inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
// ============================================================
// WiFi STA 模式配置(连接路由器)
// ============================================================
@@ -47,19 +46,21 @@ inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服
// ============================================================
// 任务栈大小(字节)— 2026-07 按板端 task_stack 水位收敛
// 规则:peak_used 实测 + 至少 ~1.5–2KB 余量;httpd 路径最重单独放宽。
// 实测(空闲/常规):http peak~5KB, wifi_ota~1.4KB, ota_relay~1KB,
// health~1.2KB, cpu_stats~2KB, ble*~1KB, reconnect~1KB。
// 规则:S ≈ peak_used + 1.5~2KB;偏松的收、偏紧的不砍。
// 板测峰值量级(常规/OTA):
// http~5.2KB, spi~1.9KB, wifi_ota~1.2KB, ble_ota~0.9KB,
// ble_worker 1004B, health/cpu~1.8KB, ota_relay 轻载~1.3KB。
// 2026-07 余量优化:收 http/spi/wifi/ble* 偏松项,health/cpu/relay 保持。
// ============================================================
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< httpd:原 10KB,peak~5KB → 8KB(余量~3KB)
inline constexpr uint32_t TASK_STACK_WIFI_OTA = 4096; ///< wifi_ota 主循环:原 8KB,peak~1.4KB
inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(轻载 peak~1KB;转发时看 /system/status.task_stack)
inline constexpr uint32_t TASK_STACK_SPI_WORKER = 5120; ///< spi_worker:5KB(帧缓冲热路径;已纳入 task_stack 巡检)
inline constexpr uint32_t TASK_STACK_BLE_OTA = 3072; ///< ble_ota 主任务:peak~1KB
inline constexpr uint32_t TASK_STACK_BLE_WORKER = 3072; ///< ble_ota_worker
inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.2KB
inline constexpr uint32_t TASK_STACK_CPU_STATS = 3072; ///< cpu_stats:peak~2KB
inline constexpr uint32_t TASK_STACK_WIFI_RECONNECT = 2560; ///< wifi_reconnect:peak~1KB
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 7168; ///< httpd:peak~5.2KB + ~2KB → 7KB(原 8/10KB)
inline constexpr uint32_t TASK_STACK_WIFI_OTA = 3072; ///< wifi_ota:peak~1.2KB + ~1.8KB → 3KB(原 4/8KB)
inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(轻载 peak~1.3KB;转发突发不砍)
inline constexpr uint32_t TASK_STACK_SPI_WORKER = 4096; ///< spi_worker:peak~1.9KB + ~2KB → 4KB(原 5KB)
inline constexpr uint32_t TASK_STACK_BLE_OTA = 2560; ///< ble_ota:peak~0.9KB + ~1.5KB → 2.5KB(原 3KB)
inline constexpr uint32_t TASK_STACK_BLE_WORKER = 2560; ///< ble_ota_worker:peak 1004B + ~1.5KB → 2.5KB(原 3KB)
inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.8KB,余量~1.2KB,不砍
inline constexpr uint32_t TASK_STACK_CPU_STATS = 3072; ///< cpu_stats:peak~1.8KB,余量~1.2KB,不砍
inline constexpr uint32_t TASK_STACK_WIFI_RECONNECT = 2560; ///< wifi_reconnect:轻载,2.5KB 保持
inline constexpr uint32_t TASK_STACK_CAN_FB = 3072; ///< can_fb:被动 takeEvent(0xC1)
inline constexpr uint32_t TASK_STACK_BLE_FINISH = 4096; ///< 按需:SHA finalize(保持)
inline constexpr uint32_t TASK_STACK_BLE_GD32_WAIT = 3072; ///< 按需:GD32 中继等待
@@ -68,9 +69,8 @@ inline constexpr uint32_t TASK_STACK_BLE_RESTART = 2048; ///< 按需:延时
// ============================================================
// OTA 配置
// ============================================================
inline constexpr size_t OTA_BLOCK_SIZE = 2048; ///< 上传接收块:2KB 降峰值 BSS/堆(s_ota_upload_recv 静态占用)
// OTA_BLOCK_SIZE 演进:8KB(原)→2KB(压堆)→4KB(试吞吐)→2KB(2026-07 回退省 DRAM)。
// 4KB 时 s_ota_upload_recv BSS +2KB,开机 free/largest 更紧;回退 2KB 优先保内存。
inline constexpr size_t OTA_BLOCK_SIZE = 4096; ///< 上传接收块:4KB(吞吐优先;s_ota_upload_recv 静态 +2KB vs 2KB 块)
// OTA_BLOCK_SIZE 演进:8KB→2KB(压堆)→4KB(试吞吐)→2KB(2026-07 回退)→4KB(2026-07-20 再提吞吐)。
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB = 221184B = 0x35F90,固件大小上限
inline constexpr size_t GD32_OTA_PACKET_SIZE = 48; // GD32 OTA 实际转发包载荷
@@ -88,6 +88,7 @@ inline constexpr size_t HEAP_WARNING_MIN = 10240; ///< 堆内
// ============================================================
// 调试配置(UDP 日志)
// GD32 ADC/CAN 等开关:看 HELLO_ACK/PING 身份块 feat 位(GD32 config 顶部)
// ============================================================
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; ///< UDP 日志目标 IP
inline constexpr uint16_t DEBUG_LOG_PORT = 6666; ///< UDP 日志目标端口
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 51
#define FW_VERSION_STRING "0.3.51"
#define FW_VERSION_PATCH 52
#define FW_VERSION_STRING "0.3.52"
// 编译时间
#define BUILD_TIME "2026-07-18 00:40:04"
#define BUILD_TIME "2026-07-20 02:50:38"
// Git 信息
#define GIT_HASH "1b10f89"
#define GIT_HASH "b807645"
+23 -4
View File
@@ -21,6 +21,8 @@
#include "transport/spi_master.h"
#include "monitor/debug_log.h"
#include "monitor/can_feedback_task.h"
#include "monitor/gd32_adc_feedback.h"
#include "app_config.h"
#include "esp_system.h"
#include <string.h>
@@ -30,6 +32,7 @@ namespace {
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
constexpr uint8_t kSpiAdcFeedbackType = 0xC4U; ///< GD32 ADC 调试反馈
constexpr size_t kRingPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致,避免截断
constexpr const char *kTag = "CAN_FB";
@@ -43,6 +46,11 @@ static can_feedback_result_t s_work [CAN_CHANNEL_MAX];
static uint8_t s_workIdx[CAN_CHANNEL_MAX];
static uint32_t s_lastLoggedError[CAN_CHANNEL_MAX];
/**
* @brief 将某通道完整反馈发布到 s_latest(线程安全)。
* @param ch 通道 0..2
* @param fb 解析后的快照
*/
static void publish(uint8_t ch, const can_feedback_result_t &fb)
{
if (s_mutex == nullptr) return;
@@ -53,6 +61,9 @@ static void publish(uint8_t ch, const can_feedback_result_t &fb)
}
}
/**
* @brief can_fb 任务主循环:takeEvent 分发 0xC1 CAN / 0xC4 ADC。
*/
static void taskLoop(void *)
{
DEBUG_INFO(kTag, "CAN 反馈被动监听任务启动 (GD32 主动推送, 0xC1)");
@@ -85,6 +96,12 @@ static void taskLoop(void *)
continue;
}
/* 0xC4 ADC 调试反馈:GD32 关开关则不推;ESP32 始终可解析(无本地开关) */
if (ring_type == kSpiAdcFeedbackType) {
(void)onGd32AdcFeedbackEvent(ring_payload, ring_len);
continue;
}
if (ring_type != kSpiCanFeedbackType || ring_len < 28U) {
continue;
}
@@ -103,13 +120,11 @@ static void taskLoop(void *)
if (s_work[ch].stats.error_count != 0U
&& s_work[ch].stats.error_count != s_lastLoggedError[ch]) {
DEBUG_WARN(kTag,
"CH%u seq=%lu err=%lu TEC=%u REC=%u state=0x%02X",
"CH%u err=%lu TEC=%u REC=%u",
static_cast<unsigned>(ch),
static_cast<unsigned long>(s_work[ch].stats.snapshot_seq),
static_cast<unsigned long>(s_work[ch].stats.error_count),
static_cast<unsigned>(s_work[ch].stats.tec),
static_cast<unsigned>(s_work[ch].stats.rec),
static_cast<unsigned>(s_work[ch].stats.bus_state));
static_cast<unsigned>(s_work[ch].stats.rec));
s_lastLoggedError[ch] = s_work[ch].stats.error_count;
}
}
@@ -156,6 +171,10 @@ bool startCanFeedbackTask()
return true;
}
/**
* @brief 暂停/恢复 CAN(及同任务分发的 ADC)事件监听。
* @param pause true=OTA 期间暂停;false=恢复
*/
void pauseCanFeedbackTask(bool pause)
{
if (s_paused == pause) return; // 幂等:状态未变时不重复日志
+1 -1
View File
@@ -95,7 +95,7 @@ void CpuStats::sampleOnce()
(now_tick - m_lastTruncWarnTick) >= cpuStatsWarnIntervalTicks()) {
m_lastTruncWarnTick = now_tick;
DEBUG_WARN("CPU",
"任务数 %lu > 快照上限 %u,本拍仅统计前 %u 个任务",
"任务数%lu>上限%u,只计前%u",
(unsigned long)live,
(unsigned)CPU_TASK_SNAPSHOT_MAX,
(unsigned)CPU_TASK_SNAPSHOT_MAX);
+139 -19
View File
@@ -18,6 +18,7 @@
*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
@@ -69,17 +70,13 @@ bool DebugLog::isKeepAliveTag(const char *tag)
char *DebugLog::allocEmptyJson(int *out_len)
{
char *empty = (char *)malloc(3);
if (!empty) {
return nullptr;
}
empty[0] = '[';
empty[1] = ']';
empty[2] = '\0';
/* 返回 nullptr 让 caller 走"[]"字面量分支,避免空环时每秒 malloc(3)/free(3)。
* ringDrainJson 的所有"返空"路径都调本函数,caller logs_handler 见 nullptr
* 后直接发 "[]" 字面量,零分配。 */
if (out_len) {
*out_len = 2;
*out_len = 0;
}
return empty;
return nullptr;
}
/**
@@ -148,8 +145,104 @@ int DebugLog::jsonEscapeAppend(char *buf, int pos, int capacity, const char *src
}
// ============================================================
// 环形缓冲(Locked 变体假定已持 m_logRingMutex)
// 环形缓冲(堆上;Locked 变体假定已持 m_logRingMutex)
// ============================================================
bool DebugLog::allocRing(int slots)
{
if (slots <= 0) {
return false;
}
char *rows = static_cast<char *>(calloc((size_t)slots, (size_t)LOG_RING_SIZE));
int *lens = static_cast<int *>(calloc((size_t)slots, sizeof(int)));
if (!rows || !lens) {
free(rows);
free(lens);
return false;
}
free(m_logRing);
free(m_logRingLens);
m_logRing = rows;
m_logRingLens = lens;
m_logRingSlots = slots;
m_logRingHead = 0;
m_logRingCount = 0;
return true;
}
void DebugLog::shrinkRingAfterBoot()
{
if (m_logRingShrunk || !m_logRingMutex) {
return;
}
if (m_logRingSlots > 0 && m_logRingSlots <= LOG_RING_SLOTS_STEADY) {
m_logRingShrunk = true;
return;
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
const int old_slots = m_logRingSlots;
const int old_count = m_logRingCount;
const int old_head = m_logRingHead;
char *old_ring = m_logRing;
int *old_lens = m_logRingLens;
const int new_slots = LOG_RING_SLOTS_STEADY;
char *rows = static_cast<char *>(calloc((size_t)new_slots, (size_t)LOG_RING_SIZE));
int *lens = static_cast<int *>(calloc((size_t)new_slots, sizeof(int)));
if (!rows || !lens) {
free(rows);
free(lens);
xSemaphoreGive(m_logRingMutex);
ESP_LOGW(TAG, "log ring shrink failed, keep boot ring (%d slots)", old_slots);
return;
}
/* 保留时间序上最新的 min(count, new_slots) 条 */
int keep = old_count;
if (keep > new_slots) {
keep = new_slots;
}
int new_head = 0;
if (old_ring && old_lens && keep > 0 && old_slots > 0) {
for (int i = 0; i < keep; ++i) {
/* 从最旧保留起点:跳过被挤掉的旧条 */
const int src_i = old_count - keep + i;
const int src = (old_head - old_count + src_i + old_slots * 4) % old_slots;
int len = old_lens[src];
if (len < 0) {
len = 0;
}
if (len >= LOG_RING_SIZE) {
len = LOG_RING_SIZE - 1;
}
char *dst = rows + (size_t)new_head * (size_t)LOG_RING_SIZE;
if (len > 0) {
memcpy(dst, old_ring + (size_t)src * (size_t)LOG_RING_SIZE, (size_t)len);
}
dst[len] = '\0';
lens[new_head] = len;
new_head = (new_head + 1) % new_slots;
}
}
m_logRing = rows;
m_logRingLens = lens;
m_logRingSlots = new_slots;
m_logRingCount = keep;
m_logRingHead = (keep >= new_slots) ? new_head : keep;
m_logRingShrunk = true;
xSemaphoreGive(m_logRingMutex);
free(old_ring);
free(old_lens);
ESP_LOGI(TAG, "log ring shrunk: %d → %d slots (free ~%d B)",
old_slots, new_slots,
(old_slots - new_slots) * (LOG_RING_SIZE + (int)sizeof(int)));
}
int DebugLog::ringCount()
{
if (!m_logRingMutex) {
@@ -166,11 +259,11 @@ bool DebugLog::ringPopOldestLocked(char *out, size_t out_size, int *out_len)
if (out_len) {
*out_len = 0;
}
if (!out || out_size == 0 || m_logRingCount <= 0) {
if (!out || out_size == 0 || m_logRingCount <= 0 || !m_logRing || m_logRingSlots <= 0) {
return false;
}
const int idx = (m_logRingHead - m_logRingCount + LOG_RING_SLOTS) % LOG_RING_SLOTS;
const int idx = (m_logRingHead - m_logRingCount + m_logRingSlots) % m_logRingSlots;
int len = m_logRingLens[idx];
if (len < 0) {
len = 0;
@@ -182,7 +275,7 @@ bool DebugLog::ringPopOldestLocked(char *out, size_t out_size, int *out_len)
}
if (copy_len > 0) {
memcpy(out, m_logRing[idx], (size_t)copy_len);
memcpy(out, ringSlot(idx), (size_t)copy_len);
}
out[copy_len] = '\0';
if (out_len) {
@@ -215,6 +308,9 @@ int DebugLog::ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const
if (total_raw_len) {
*total_raw_len = 0;
}
if (!m_logRing || m_logRingSlots <= 0) {
return 0;
}
int n = m_logRingCount;
if (n > max_n) {
n = max_n;
@@ -225,7 +321,7 @@ int DebugLog::ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const
int sum = 0;
for (int i = 0; i < n; ++i) {
const int idx = (m_logRingHead - m_logRingCount + i + LOG_RING_SLOTS) % LOG_RING_SLOTS;
const int idx = (m_logRingHead - m_logRingCount + i + m_logRingSlots) % m_logRingSlots;
int len = m_logRingLens[idx];
if (len < 0) {
len = 0;
@@ -243,7 +339,7 @@ int DebugLog::ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const
void DebugLog::ringPushLocked(const char *data, int len)
{
if (!data || len <= 0) {
if (!data || len <= 0 || !m_logRing || !m_logRingLens || m_logRingSlots <= 0) {
return;
}
@@ -252,12 +348,27 @@ void DebugLog::ringPushLocked(const char *data, int len)
copy_len = LOG_RING_SIZE - 1;
}
memcpy(m_logRing[m_logRingHead], data, (size_t)copy_len);
m_logRing[m_logRingHead][copy_len] = '\0';
/* 满环时优先覆盖 INFO/DBG,保护 ERR/WARN。 */
if (m_logRingCount >= m_logRingSlots) {
int victim_idx = m_logRingHead;
for (int i = 0; i < m_logRingSlots; ++i) {
const int idx = (m_logRingHead + i) % m_logRingSlots;
const char *s = ringSlot(idx);
if (s[0] == '[' && (s[1] == 'I' || s[1] == 'D')) {
victim_idx = idx;
break;
}
}
m_logRingHead = victim_idx;
}
char *slot = ringSlot(m_logRingHead);
memcpy(slot, data, (size_t)copy_len);
slot[copy_len] = '\0';
m_logRingLens[m_logRingHead] = copy_len;
m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS;
if (m_logRingCount < LOG_RING_SLOTS) {
m_logRingHead = (m_logRingHead + 1) % m_logRingSlots;
if (m_logRingCount < m_logRingSlots) {
m_logRingCount++;
}
}
@@ -475,6 +586,15 @@ esp_err_t DebugLog::init()
return ESP_ERR_NO_MEM;
}
if (!allocRing(LOG_RING_SLOTS_BOOT)) {
ESP_LOGE(TAG, "boot log ring alloc 失败 (%d slots),/logs 不可用", LOG_RING_SLOTS_BOOT);
/* UART/UDP 仍可用 */
} else {
ESP_LOGI(TAG, "boot log ring %d×%d (~%d B heap)",
LOG_RING_SLOTS_BOOT, LOG_RING_SIZE,
LOG_RING_SLOTS_BOOT * (LOG_RING_SIZE + (int)sizeof(int)));
}
memset(&m_debugAddr, 0, sizeof(m_debugAddr));
m_debugAddr.sin_family = AF_INET;
m_debugAddr.sin_port = htons(DEBUG_LOG_PORT);
+25 -8
View File
@@ -51,9 +51,15 @@ class DebugLog {
public:
static DebugLog& instance();
/** @brief 初始化日志系统(UART + ring;UDP socket 延迟创建)。 */
/** @brief 初始化日志系统(UART + 开机大环;UDP socket 延迟创建)。 */
esp_err_t init();
/**
* @brief 启动 burst 结束后把环从开机容量缩到稳态小环,释放大块堆。
* 幂等;失败则保留大环。宜在 WiFi/HTTP 就绪后、日志变少时调用。
*/
void shrinkRingAfterBoot();
/** @brief 初始化 UART0 调试串口。 */
esp_err_t uartInit();
@@ -96,17 +102,28 @@ private:
bool m_uartAvailable = false;
debug_log_level_t m_logLevel = DEBUG_LOG_INFO;
/* 环形日志:OTA 中继期需保留关键 INFO,64×160≈10KB BSS。 */
static constexpr int LOG_RING_SLOTS = 64;
static constexpr int LOG_RING_SIZE = 160;
/** 单次 /logs drain 上限(与 SLOTS 更接近,减 burst 滞后) */
static constexpr int LOG_DRAIN_MAX = 48;
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE] = {};
int m_logRingLens[LOG_RING_SLOTS] = {};
/* 环形日志(堆上分配,非 .data):
* 开机 LOG_RING_SLOTS_BOOT×128 容纳启动刷屏;
* shrinkRingAfterBoot() 后改为 LOG_RING_SLOTS_STEADY×128,释放大环。
* 业务 DEBUG_* 目标:整行([L][TAG]+正文+\\n)≤128B;槽 128 含 \\0 → 有效≤127。 */
static constexpr int LOG_RING_SLOTS_BOOT = 64; ///< 开机 burst:~8.4KB 堆
static constexpr int LOG_RING_SLOTS_STEADY = 16; ///< 稳态:~2.1KB 堆(对齐 drain)
static constexpr int LOG_RING_SIZE = 128; ///< 单条槽(含 \\0);有效≤127(含换行)
static constexpr int LOG_DRAIN_MAX = 16; ///< 单次 /logs 最多条数
char *m_logRing = nullptr; ///< [slots * LOG_RING_SIZE] 扁平行
int *m_logRingLens = nullptr; ///< [slots]
int m_logRingSlots = 0; ///< 当前容量
int m_logRingHead = 0;
int m_logRingCount = 0;
bool m_logRingShrunk = false;
SemaphoreHandle_t m_logRingMutex = nullptr;
char *ringSlot(int idx) { return m_logRing + (size_t)idx * (size_t)LOG_RING_SIZE; }
const char *ringSlot(int idx) const {
return m_logRing + (size_t)idx * (size_t)LOG_RING_SIZE;
}
bool allocRing(int slots);
/* 堆门控(防 OTA 时 UDP/格式化抢 LWIP 堆):
* ERR/WARN 永不因堆门控丢弃
* free < kDropInfoFree → 非 keep-alive 丢 INFO/DBG;keep-alive 保留 INFO
+150
View File
@@ -0,0 +1,150 @@
/*
* gd32_adc_feedback.cpp - GD32 ADC 调试反馈解析与缓存
*/
#include "monitor/gd32_adc_feedback.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <string.h>
namespace {
constexpr uint8_t kPayloadVer = 1U;
constexpr size_t kPayloadLen = 20U;
static SemaphoreHandle_t s_mux = nullptr;
static gd32_adc_feedback_t s_latest = {};
static bool s_have = false;
/** @brief 读小端 u16。 */
static uint16_t rdU16Le(const uint8_t *p)
{
return static_cast<uint16_t>(p[0] | (static_cast<uint16_t>(p[1]) << 8));
}
/** @brief 读小端 u32。 */
static uint32_t rdU32Le(const uint8_t *p)
{
return static_cast<uint32_t>(p[0])
| (static_cast<uint32_t>(p[1]) << 8)
| (static_cast<uint32_t>(p[2]) << 16)
| (static_cast<uint32_t>(p[3]) << 24);
}
/** @brief 懒创建快照互斥量。 */
static void ensureMux()
{
if (s_mux == nullptr) {
s_mux = xSemaphoreCreateMutex();
}
}
} // namespace
/**
* @brief 处理一帧 0xC4 ADC 调试反馈并更新最新快照。
* @param payload 固定 20B 载荷
* @param len 实际长度
* @return true 解析并写入成功
*/
bool onGd32AdcFeedbackEvent(const uint8_t *payload, size_t len)
{
if (payload == nullptr || len < kPayloadLen) {
return false;
}
if (payload[0] != kPayloadVer) {
return false;
}
const uint8_t flags = payload[1];
const int16_t temp_x100 = static_cast<int16_t>(rdU16Le(&payload[2]));
const uint16_t vdda_mV = rdU16Le(&payload[4]);
const uint16_t vin_mV = rdU16Le(&payload[6]);
const uint16_t temp_raw = rdU16Le(&payload[8]);
const uint16_t vref_raw = rdU16Le(&payload[10]);
const uint32_t seq = rdU32Le(&payload[12]);
const uint32_t uptime = rdU32Le(&payload[16]);
gd32_adc_feedback_t fb = {};
fb.valid = (flags & 0x01U) != 0U;
fb.temperature_c = static_cast<float>(temp_x100) / 100.0f;
fb.vdda_v = static_cast<float>(vdda_mV) / 1000.0f;
fb.vin_adc_v = static_cast<float>(vin_mV) / 1000.0f;
/* 原理图:VCC — R18 47k — VIN_ADC — R13 4.7k — GND
* VCC = V_adc * (R18+R13)/R13 = V_adc * 11 */
fb.vcc_v = fb.vin_adc_v * ((47.0f + 4.7f) / 4.7f);
fb.temp_raw = temp_raw;
fb.vref_raw = vref_raw;
fb.seq = seq;
fb.uptime_sec = uptime;
fb.recv_ms = esp_timer_get_time() / 1000;
ensureMux();
if (s_mux == nullptr) {
return false;
}
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) {
return false;
}
s_latest = fb;
s_have = true;
xSemaphoreGive(s_mux);
return true;
}
/**
* @brief 线程安全拷贝最近一次 ADC 反馈。
* @param[out] out 输出缓冲
* @return true 已有至少一帧有效数据
*/
bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out)
{
if (out == nullptr) {
return false;
}
ensureMux();
if (s_mux == nullptr) {
return false;
}
bool ok = false;
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) == pdTRUE) {
if (s_have) {
*out = s_latest;
ok = true;
}
xSemaphoreGive(s_mux);
}
return ok;
}
/**
* @brief 距上次收到 0xC4 的毫秒数。
* @return 年龄 ms;尚无数据返回 UINT32_MAX
*/
uint32_t gd32AdcFeedbackAgeMs(void)
{
ensureMux();
if (s_mux == nullptr) {
return UINT32_MAX;
}
int64_t recv = 0;
bool have = false;
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) == pdTRUE) {
have = s_have;
recv = s_latest.recv_ms;
xSemaphoreGive(s_mux);
}
if (!have || recv <= 0) {
return UINT32_MAX;
}
const int64_t now = esp_timer_get_time() / 1000;
if (now < recv) {
return 0U;
}
const int64_t age = now - recv;
if (age > static_cast<int64_t>(UINT32_MAX)) {
return UINT32_MAX;
}
return static_cast<uint32_t>(age);
}
+53
View File
@@ -0,0 +1,53 @@
/*
* gd32_adc_feedback.h - 被动接收 GD32 ADC 调试反馈(SPI TYPE 0xC4)
*
* 与 CAN 0xC1 同模型:GD32 1Hz 主动按铃推送,ESP32 takeEvent 侧解析入库。
* 是否推送由 GD32 CONFIG_SPI_ADC_FEEDBACK_ENABLE 决定,经 HELLO/PING 身份块
* (V2_FEAT_ADC_FEEDBACK)上报;ESP32 无本地开关,有帧即解析。
*/
#ifndef GD32_ADC_FEEDBACK_H
#define GD32_ADC_FEEDBACK_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @brief 最近一次 GD32 ADC 调试快照(工程量已换算)。 */
typedef struct {
bool valid; ///< GD32 sample.valid
float temperature_c; ///< ℃
float vdda_v; ///< V(MCU 模拟供电,约 3.3)
float vin_adc_v; ///< V(分压后 ADC 引脚 PA6)
float vcc_v; ///< V(输入母线 VCC,由 vin 分压反推)
uint16_t temp_raw;
uint16_t vref_raw;
uint32_t seq;
uint32_t uptime_sec; ///< GD32 侧 uptime
int64_t recv_ms; ///< ESP32 收到时 esp_timer ms
} gd32_adc_feedback_t;
/**
* @brief 处理一帧 0xC4 EVENT payload(由 can_fb 任务 takeEvent 分发)。
* @return true 表示已接受并更新快照。
*/
bool onGd32AdcFeedbackEvent(const uint8_t *payload, size_t len);
/**
* @brief 拷贝最新快照。
* @param[out] out 输出缓冲
* @return true 表示至少收到过一帧
*/
bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out);
/** @brief 距离上次收到的毫秒数;无数据返回 UINT32_MAX。 */
uint32_t gd32AdcFeedbackAgeMs(void);
#ifdef __cplusplus
}
#endif
#endif /* GD32_ADC_FEEDBACK_H */
+399 -10
View File
@@ -8,6 +8,8 @@
*/
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
@@ -20,9 +22,107 @@
#include "health_monitor.h"
#include "debug_log.h"
#include "task_stack_check.h"
#include "stack_overflow_hook.h"
#include "task_stack.h"
#include "ota/ota_coordinator.h"
#include "ble_ota.h"
#include "wifi_ota.h"
#include "spi_master.h"
#include "esp_event.h"
#include "esp_netif_types.h"
// ============================================================
// heap hook:捕获"破新低时的最后一次 alloc"(最后一击,未必是主因)
// ============================================================
// CONFIG_HEAP_USE_HOOKS=y 时,每次 malloc 成功后进入本弱符号。
//
// ★ 为何不能记任务名(用户看到"每次都是 WiFi"的根因):
// 1) WiFi IRAM 路径禁止在 hook 里调 FreeRTOS/pcTaskGetName(会搞挂 STA)。
// 2) 即便能取任务名:HTTP/UDP **发送前** lwip 仍会为 pbuf/TX 再 malloc,
// 芯片 minimum free 往往在这条"最后一击"上被压低,任务名就会是 wifi/tiT/httpd,
// 看起来像"WiFi 吃堆",实际只是发送路径的末包分配。
// 因此 hook **永不写任务名**;只记 size + caller 地址(addr2line)。
// 前端诊断应标明「最后一击 / 可能网络发送」,勿把连接列的 WiFi 当成触发任务。
//
// ★ 记录策略:health_mon 周期把真实 free 同步进 g_est_free;
// hook 内仅做 g_est_free -= size(C3 单核,无锁可接受漂移)。
// 当 est 创新低时更新 hook 快照。free hook 不知 size,靠周期 resync 纠偏。
namespace {
volatile uint32_t g_hook_min_est = 0xFFFFFFFFu; ///< hook 见过的最低 est free
volatile uint32_t g_hook_alloc_size = 0; ///< 触发 est 新低的那次 alloc 大小
volatile uint32_t g_hook_caller_addr = 0; ///< 调用者返回地址
volatile uint32_t g_hook_free_after = 0; ///< 那次后的 est free(非精确实测)
volatile uint32_t g_hook_fired = 0;
volatile uint32_t g_hook_armed = 0;
volatile uint32_t g_est_free = 0; ///< 近似空闲,任务上下文 resync
volatile uint32_t g_ip_event_count = 0;
/** 最近若干次「值得关注」的 alloc(size≥阈值),供破新低时对照。 */
struct RecentAllocSnap {
uint32_t size;
uint32_t caller;
};
static constexpr uint32_t kHookInterestingMin = 48; ///< 忽略极小碎片分配
static constexpr size_t kRecentAllocCap = 4;
RecentAllocSnap g_recent_allocs[kRecentAllocCap] = {};
volatile uint32_t g_recent_alloc_w = 0;
volatile uint32_t g_recent_alloc_n = 0;
void hookResyncEstFree(uint32_t real_free)
{
g_est_free = real_free;
}
} // namespace
// ESP-IDF heap alloc hook:仅 DRAM 标量 + 内联 return address,禁止 FreeRTOS/flash 长函数。
extern "C"
void esp_heap_trace_alloc_hook(void *ptr, size_t size, uint32_t caps)
{
(void)caps;
if (!ptr || size == 0 || g_hook_armed == 0) {
return;
}
const uint32_t sz = (uint32_t)size;
/* 近似空闲:只减不增(free 路径不在此修正),health_mon 周期 resync。 */
uint32_t est = g_est_free;
if (est >= sz) {
est -= sz;
} else {
est = 0;
}
g_est_free = est;
/* 环形记下近期「有意义」分配,破新低时可读到发送路径小包之前的大块。 */
if (sz >= kHookInterestingMin) {
const uint32_t w = g_recent_alloc_w % (uint32_t)kRecentAllocCap;
g_recent_allocs[w].size = sz;
g_recent_allocs[w].caller = (uint32_t)(uintptr_t)__builtin_return_address(0);
g_recent_alloc_w = w + 1U;
if (g_recent_alloc_n < (uint32_t)kRecentAllocCap) {
g_recent_alloc_n = g_recent_alloc_n + 1U;
}
}
/* est 创新低 → 记「最后一击」alloc(常为网络 pbuf,任务若可读会是 wifi) */
if (est < g_hook_min_est) {
g_hook_min_est = est;
g_hook_alloc_size = sz;
g_hook_caller_addr = (uint32_t)(uintptr_t)__builtin_return_address(0);
g_hook_free_after = est;
g_hook_fired = 1;
/* 不采任务名(WiFi IRAM 路径禁止 FreeRTOS 调用) */
}
}
// IP 事件 handler:STA 获得 IP(含 DHCP 续约)时计数 +1
void ipEventHandler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
(void)arg; (void)event_base; (void)event_id; (void)event_data;
if (event_base == IP_EVENT && (event_id == IP_EVENT_STA_GOT_IP || event_id == IP_EVENT_ETH_GOT_IP)) {
uint32_t cur = g_ip_event_count;
g_ip_event_count = cur + 1;
}
}
// ============================================================
// 栈水位主动巡检
@@ -60,6 +160,18 @@ void HealthMonitor::init()
if (m_mutex == NULL) {
DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
}
// 注册 IP 事件 handler,统计 STA 获得 IP(含 DHCP 续约)次数。
// 配合 heap_low_log 快照的 ip_event_count 字段,可验证"低谷是不是 DHCP 续约引起"。
// 失败仅告警,不致命(计数器保持 0,快照里 ip_event_count 永远 0)。
esp_err_t ev = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ipEventHandler, nullptr);
if (ev != ESP_OK) {
DEBUG_WARN("HEALTH", "IP 事件 handler 注册失败: %s", esp_err_to_name(ev));
}
// 武装 heap alloc hook:init() 在 app_main 之后调用。armed=0 时 hook 直接 return。
// hook 本身禁止任何 FreeRTOS/flash 调用(见 esp_heap_trace_alloc_hook 注释)。
g_hook_armed = 1;
}
// ============================================================
@@ -159,7 +271,7 @@ void HealthMonitor::tempMonitorCallback(void *arg)
if (temp >= TEMP_CRITICAL) {
self.setState(SYS_SAFE_SHUTDOWN);
DEBUG_ERR("TEMP", "!!! 危险 !!! 温度 %.1f°C >= %d°C,触发安全关机", temp, TEMP_CRITICAL);
DEBUG_ERR("TEMP", "过热 %.1fC>=%dC 安全关机", temp, TEMP_CRITICAL);
DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN");
} else if (temp >= TEMP_WARNING) {
health_state_t cur = self.getState();
@@ -213,12 +325,19 @@ void HealthMonitor::taskEntry(void *pvParameters)
// 在 health_mon 完好栈上用 DEBUG_ERR 经 UDP/UART 发详细告警。
// 标志只在置位后告警一次,不自动清除(canary 触发即不可逆,需重启恢复)。
if (g_stackOverflowDetected) {
DEBUG_ERR("MON", "!!! 栈溢出已被内核 canary 检测到,请检查任务栈大小 !!!");
DEBUG_ERR("MON", "栈溢出(canary),检查任务栈");
// 告警一次后清标志,避免 5 秒刷屏;后续若再次触发会重新置位
g_stackOverflowDetected = false;
}
uint32_t free_heap = esp_get_free_heap_size();
/* 同步 hook 近似空闲:纠正 free 路径未记账造成的向下漂移。
* 必须在 maybeRecord 之前,否则 est 长期偏低会误触发 hook 快照。 */
hookResyncEstFree(free_heap);
// 堆低水位快照:仅在"最低空闲"创新低时记录一条上下文(5s 检测足够)
self.maybeRecordHeapLowWater();
if (free_heap < HEAP_WARNING_MIN) {
/* 堆告警节流:OTA 期间阈值更低、间隔更长 */
static TickType_t s_last_heap_warn = 0;
@@ -231,7 +350,7 @@ void HealthMonitor::taskEntry(void *pvParameters)
(now - s_last_heap_warn) >= pdMS_TO_TICKS(interval_ms)) {
s_last_heap_warn = now;
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
DEBUG_WARN("MON", "堆内存偏紧%s: free=%lu min=%lu largest_blk=%lu",
DEBUG_WARN("MON", "堆紧%s free=%lu min=%lu big=%lu",
ota_busy ? "(OTA中)" : "",
(unsigned long)free_heap,
(unsigned long)esp_get_minimum_free_heap_size(),
@@ -255,11 +374,10 @@ void HealthMonitor::taskEntry(void *pvParameters)
size_t warn_bytes = (size_t)(metrics[i].stack_size_bytes * STACK_WARN_RATIO);
if (hw < (UBaseType_t)warn_bytes) {
if (!s_stackWarned[i]) {
DEBUG_ERR("MON", "!!! 栈水位过低 !!! 任务=%s 剩余=%luB 阈值=%luB(%.0f%%) 栈大小=%luB",
DEBUG_ERR("MON", "栈低 task=%s left=%lu thr=%lu sz=%lu",
metrics[i].name,
(unsigned long)hw,
(unsigned long)warn_bytes,
STACK_WARN_RATIO * 100.0f,
(unsigned long)metrics[i].stack_size_bytes);
s_stackWarned[i] = true;
}
@@ -313,14 +431,14 @@ health_status_t HealthMonitor::checkBeforeOta()
if (getLastWifiRssi(&last_rssi, &age_ms)) {
if (age_ms <= OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS) {
if (last_rssi < OTA_WIFI_RSSI_CRITICAL) {
DEBUG_WARN("HEALTH", "OTA 前检查: 最近 RSSI %d < %d (样本年龄 %lu ms)",
DEBUG_WARN("HEALTH", "OTA前 RSSI %d<%d age=%lums",
last_rssi, OTA_WIFI_RSSI_CRITICAL, (unsigned long)age_ms);
return HEALTH_MON_CRITICAL;
} else if (last_rssi < WIFI_RSSI_NORMAL) {
if (status < HEALTH_MON_WARNING) {
status = HEALTH_MON_WARNING;
}
DEBUG_WARN("HEALTH", "OTA 前检查: 最近 RSSI %d < %d (样本年龄 %lu ms)",
DEBUG_WARN("HEALTH", "OTA前 RSSI %d<%d age=%lums",
last_rssi, WIFI_RSSI_NORMAL, (unsigned long)age_ms);
}
}
@@ -409,7 +527,7 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
} else { // next == SYS_NORMAL,从 WARNING/CRITICAL 恢复
setState(SYS_NORMAL);
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm (从 %d 降级)", rssi, WIFI_RSSI_NORMAL, (int)cur);
DEBUG_INFO("HEALTH", "RSSI恢复 %d>=%d (lv%d)", rssi, WIFI_RSSI_NORMAL, (int)cur);
}
}
@@ -418,3 +536,274 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
return HEALTH_MON_OK;
}
// ============================================================
// 堆低水位快照("最低空闲"创新低时记录上下文)
// ============================================================
namespace {
/** @brief snprintf 追加到 buf 的安全封装:截断但不越界,返回新 offset。 */
int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...)
{
if (offset < 0 || (size_t)offset >= cap) return offset;
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, ap);
va_end(ap);
if (n < 0) return offset;
if ((size_t)n >= cap - (size_t)offset) {
// 被截断:vsnprintf 返回的是"本应写入"的长度,按截断后修正 offset
return (int)cap - 1;
}
return offset + n;
}
} // namespace
/**
* @brief 检测"内部堆最低空闲"是否创新低,是则覆盖写入唯一快照槽。
*
* 调用方:health_mon 5s 巡检任务。仅在"新低"时写一条,避免每 5s 刷一条无意义记录。
* 上下文字段:OTA 系统状态、通道、RSSI、温度、栈剩余最少的任务名+剩余字节。
* 所有外部读取(OtaCoordinator::getSnapshot / collectTaskStackMetrics / rssi / temp)
* 都先在锁外取,避免持锁期间嵌套调用其它模块的锁;持锁后只做字段拷贝。
*/
void HealthMonitor::maybeRecordHeapLowWater()
{
// 与 http_handlers.cpp 同口径:内部 DRAM 数据堆
static constexpr uint32_t CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
const uint32_t cur_min = (uint32_t)heap_caps_get_minimum_free_size(CAPS);
// 不创新低则直接返回(不持锁)
if (cur_min >= m_heapLowWaterLastMin) {
return;
}
// ── 锁外收集上下文 ──
const uint32_t free_heap = (uint32_t)esp_get_free_heap_size();
const uint32_t largest_block = (uint32_t)heap_caps_get_largest_free_block(CAPS);
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
const uint8_t sys_state = (uint8_t)snap.sys_state;
const uint8_t ota_channel = (uint8_t)snap.channel;
const uint32_t ota_stored = (uint32_t)snap.meta.stored_size;
const uint32_t ota_total = (uint32_t)snap.meta.total_size;
int8_t rssi = -127;
{
int8_t r;
uint32_t age_ms;
if (getLastWifiRssi(&r, &age_ms)) {
rssi = r;
}
}
const float temperature = getLastTemperature();
/* current_task = 采样者,不是触发者。固定写 health_mon,避免误解为“谁 alloc”。 */
char current_task[16] = "health_mon";
// 连接状态
const uint8_t ble_connected = BleOta::instance().isConnected() ? 1 : 0;
const uint8_t wifi_connected = WifiOta::instance().getStaConnected() ? 1 : 0;
// 嫌疑资源水位:定位"哪个子系统吃堆"
const uint16_t ble_queue_depth = (uint16_t)ble_ota_get_data_queue_depth();
const uint16_t ble_reorder = (uint16_t)ble_ota_get_reorder_cached_count();
const uint16_t spi_pending = (uint16_t)SpiMaster::instance().getPendingJobCount();
// 内存全景:default 含 PSRAM,与 internal 对比可判断"是内部碎片 vs 系统级紧"
const uint32_t default_free = (uint32_t)heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
uint32_t alloc_count = 0;
{
multi_heap_info_t info = {};
heap_caps_get_info(&info, CAPS);
alloc_count = (uint32_t)info.allocated_blocks;
}
// 收集所有受监控任务栈水位,按"剩余升序"取前 8 个
HeapLowTaskStack stacks[8];
uint8_t stack_count = 0;
{
TaskStackMetric metrics[STACK_TASK_MAX];
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
// 先全部采集
struct Item { char name[16]; uint32_t rem; };
Item items[STACK_TASK_MAX];
size_t item_n = 0;
for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) {
if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) {
continue;
}
UBaseType_t hw = uxTaskGetStackHighWaterMark(metrics[i].handle);
Item &it = items[item_n++];
it.rem = (uint32_t)hw * sizeof(StackType_t);
strncpy(it.name, metrics[i].name ? metrics[i].name : "?", sizeof(it.name) - 1);
it.name[sizeof(it.name) - 1] = '\0';
}
// 冒泡排序按 rem 升序(item_n ≤ 9,开销小)
for (size_t i = 0; i + 1 < item_n; ++i) {
for (size_t j = i + 1; j < item_n; ++j) {
if (items[j].rem < items[i].rem) {
Item t = items[i]; items[i] = items[j]; items[j] = t;
}
}
}
// 取前 8
for (size_t i = 0; i < item_n && stack_count < 8; ++i) {
strncpy(stacks[stack_count].name, items[i].name, sizeof(stacks[stack_count].name) - 1);
stacks[stack_count].name[sizeof(stacks[stack_count].name) - 1] = '\0';
stacks[stack_count].remaining = items[i].rem;
++stack_count;
}
}
// ── 持锁覆盖写唯一槽 ──
LockGuard lock(m_mutex);
HeapLowWaterRecord &rec = m_heapLowWater;
rec = {};
rec.ts_ms = (uint64_t)(esp_timer_get_time() / 1000);
rec.free_heap = free_heap;
rec.min_free_heap = cur_min;
rec.largest_free_block = largest_block;
rec.sys_state = sys_state;
rec.ota_channel = ota_channel;
rec.rssi = rssi;
rec.temperature = temperature;
rec.task_stack_count = stack_count;
for (uint8_t i = 0; i < stack_count && i < 8; ++i) {
rec.task_stacks[i] = stacks[i];
}
strncpy(rec.current_task, current_task, sizeof(rec.current_task) - 1);
rec.current_task[sizeof(rec.current_task) - 1] = '\0';
rec.ble_connected = ble_connected;
rec.wifi_connected = wifi_connected;
rec.ota_stored_size = ota_stored;
rec.ota_total_size = ota_total;
rec.ble_data_queue_depth = ble_queue_depth;
rec.ble_reorder_cached = ble_reorder;
rec.spi_pending_jobs = spi_pending;
rec.reserved = 0;
rec.default_free_heap = default_free;
rec.alloc_count = alloc_count;
// heap hook:「最后一击」alloc(无任务名;见文件头注释)
{
uint32_t hook_size = g_hook_alloc_size;
uint32_t hook_caller = g_hook_caller_addr;
uint32_t hook_after = g_hook_free_after;
uint32_t hook_fired = g_hook_fired;
/* 窗口内更大块(若存在):发送前小 pbuf 压低水位时,主因更可能是此前 bulk */
uint32_t big_size = 0;
uint32_t big_caller = 0;
const uint32_t n = g_recent_alloc_n;
for (uint32_t i = 0; i < n && i < (uint32_t)kRecentAllocCap; ++i) {
if (g_recent_allocs[i].size > big_size) {
big_size = g_recent_allocs[i].size;
big_caller = g_recent_allocs[i].caller;
}
}
rec.hook_alloc_size = hook_size;
rec.hook_caller_addr = hook_caller;
rec.hook_free_after = hook_after;
rec.hook_fired = hook_fired;
/* hook_task 存类别:net? / net?+big / bulk / tiny(非 FreeRTOS 任务名) */
rec.hook_task[0] = '\0';
if (hook_fired) {
const char *cls = "tiny";
if (hook_size >= 2048U) {
cls = "bulk";
} else if (hook_size >= kHookInterestingMin) {
if (big_size > hook_size && big_size >= 2048U) {
cls = "net?+big";
rec.hook_caller_addr = big_caller;
} else {
cls = "net?";
}
}
strncpy(rec.hook_task, cls, sizeof(rec.hook_task) - 1);
rec.hook_task[sizeof(rec.hook_task) - 1] = '\0';
}
g_hook_min_est = g_est_free;
g_hook_fired = 0;
}
rec.ip_event_count = g_ip_event_count;
m_heapLowWaterValid = true;
m_heapLowWaterLastMin = cur_min;
}
/**
* @brief 输出设备唯一快照槽:{"count":0|1,"records":[...]}。
* cap 不够装下整条时退回 count=0,避免半截 JSON。
*/
int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
{
LockGuard lock(m_mutex);
if (!m_heapLowWaterValid) {
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
const HeapLowWaterRecord &r = m_heapLowWater;
int pos = appendJsonfSafe(buf, cap, offset,
"{\"count\":1,\"records\":[{"
"\"ts_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu,"
"\"largest_free_block\":%lu,\"sys_state\":%u,\"ota_channel\":%u,"
"\"rssi\":%d,\"temperature\":%.1f,"
"\"current_task\":\"%s\",\"ble_connected\":%u,\"wifi_connected\":%u,"
"\"ota_stored_size\":%lu,\"ota_total_size\":%lu,"
"\"ble_data_queue_depth\":%u,\"ble_reorder_cached\":%u,"
"\"spi_pending_jobs\":%u,\"default_free_heap\":%lu,\"alloc_count\":%lu,"
"\"hook_task\":\"%s\",\"hook_alloc_size\":%lu,\"hook_caller_addr\":%lu,"
"\"hook_free_after\":%lu,\"hook_fired\":%lu,\"ip_event_count\":%lu,"
"\"task_stacks\":[",
(unsigned long long)r.ts_ms,
(unsigned long)r.free_heap,
(unsigned long)r.min_free_heap,
(unsigned long)r.largest_free_block,
(unsigned)r.sys_state,
(unsigned)r.ota_channel,
(int)r.rssi,
(double)r.temperature,
r.current_task,
(unsigned)r.ble_connected,
(unsigned)r.wifi_connected,
(unsigned long)r.ota_stored_size,
(unsigned long)r.ota_total_size,
(unsigned)r.ble_data_queue_depth,
(unsigned)r.ble_reorder_cached,
(unsigned)r.spi_pending_jobs,
(unsigned long)r.default_free_heap,
(unsigned long)r.alloc_count,
r.hook_task,
(unsigned long)r.hook_alloc_size,
(unsigned long)r.hook_caller_addr,
(unsigned long)r.hook_free_after,
(unsigned long)r.hook_fired,
(unsigned long)r.ip_event_count);
if (pos == offset) {
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
for (uint8_t k = 0; k < r.task_stack_count && k < 8; ++k) {
int after = appendJsonfSafe(buf, cap, pos,
"%s{\"name\":\"%s\",\"remaining\":%lu}",
k == 0 ? "" : ",",
r.task_stacks[k].name,
(unsigned long)r.task_stacks[k].remaining);
if (after == pos) {
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
pos = after;
}
int closed = appendJsonfSafe(buf, cap, pos, "]}]}");
if (closed == pos) {
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
return closed;
}
+70
View File
@@ -32,6 +32,62 @@ typedef enum {
// WiFi RSSI 阈值(来自 app_config.h)
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING ///< WiFi 信号正常阈值(别名)
// ============================================================
// 堆低水位快照("最低空闲"创新低时记录上下文,供 /system/heap_low_log 查询)
// ============================================================
/** @brief 单条任务栈水位(用于低水位快照内嵌的任务栈表)。 */
struct HeapLowTaskStack {
char name[16]; ///< 任务名(截断)
uint32_t remaining; ///< 剩余栈字节(uxTaskGetStackHighWaterMark × sizeof(StackType_t))
};
/** @brief 单条"堆低水位"快照记录。仅当 internal_min_free 出现新低时写入。 */
struct HeapLowWaterRecord {
uint64_t ts_ms; ///< 启动后毫秒(esp_timer_get_time / 1000)
uint32_t free_heap; ///< 当时的总空闲堆(esp_get_free_heap_size)
uint32_t min_free_heap; ///< 当时记录到的内部堆最低水位
uint32_t largest_free_block; ///< 最大连续空闲块
uint8_t sys_state; ///< OTA 系统状态码(ota_system_state_t)
uint8_t ota_channel; ///< OTA 通道(0=WIFI, 1=BLE)
int8_t rssi; ///< 最近 WiFi RSSI(dBm,-127=未知)
float temperature; ///< 最近芯片温度(°C)
/* 多任务栈水位表:按"剩余升序"取前 8 个,定位"是谁的栈突然涨了"。
* task_stack_count=0 表示未采集到任务信息。 */
HeapLowTaskStack task_stacks[8];
uint8_t task_stack_count;
/* 触发低水位的"当前任务":取当前 TaskHandle 的名字(taskENTER_CRITICAL 后取),
* 用于推断是谁在分配堆。空串表示未知。 */
char current_task[16];
/* 连接/OTA 进度上下文:区分"无连接 vs 中继期 vs 收尾期" */
uint8_t ble_connected; ///< 0/1
uint8_t wifi_connected; ///< 0/1
uint32_t ota_stored_size; ///< OTA 已存储字节
uint32_t ota_total_size; ///< OTA 总字节
/* 嫌疑资源水位:定位"OTA/IDLE 期低谷由哪个子系统吃堆"。
* 非 OTA 期这些应为 0;非 0 时即对应子系统持有堆缓冲。 */
uint16_t ble_data_queue_depth; ///< BLE OTA 数据队列当前条数
uint16_t ble_reorder_cached; ///< BLE reorder 已缓存乱序包数
uint16_t spi_pending_jobs; ///< SPI master 待处理 job 数
uint16_t reserved; ///< 对齐保留
/* 内存全景:区分"内部碎片"vs"系统级紧"。
* default_free 含 PSRAM;internal_free = free_heap(含 SPIRAM 时与 default 区分)。
* alloc_count = 当前活跃 alloc 数,看"是不是小块大量未释放"。 */
uint32_t default_free_heap; ///< MALLOC_CAP_DEFAULT free(含 PSRAM)
uint32_t alloc_count; ///< 内部堆当前 allocated_blocks(heap_caps_get_info)
/* heap hook:「破新低时的最后一击 alloc」。
* 不记任务名(WiFi IRAM 禁止;且发送路径末包常把任务刷成 wifi,误导主因)。
* hook_task 现为类别串:net? / bulk / tiny(非 FreeRTOS 任务名)。
* hook_caller_addr 用 addr2line 解析。 */
char hook_task[16]; ///< 分配类别 net?/bulk/tiny,不是任务名
uint32_t hook_alloc_size; ///< 最后一击 alloc 字节数
uint32_t hook_caller_addr; ///< 调用者返回地址(addr2line)
uint32_t hook_free_after; ///< 该次后的近似 free(est,非精确实测)
uint32_t hook_fired; ///< 0=本窗口 hook 未命中
/* DHCP/IP 事件计数:每次 STA GOT_IP(含续约)+1。
* 对比两次低谷记录的计数差,确认低谷是不是 DHCP 续约引起。 */
uint32_t ip_event_count; ///< 累计 IP 获得/续约次数
};
/**
* @brief 系统健康监控类(单例)
*
@@ -73,6 +129,15 @@ public:
/** @brief 获取健康监控任务句柄。 */
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
// ── 堆低水位快照 ──
/** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。
* 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。
* 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。 */
void maybeRecordHeapLowWater();
/** @brief 将唯一快照序列化为 JSON:{"count":0|1,"records":[...]}。线程安全。 */
int dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const;
/**
* @brief 由 WiFi 事件回调推送连接状态(避免 HealthMonitor → WifiOta 依赖)。
* WiFi 连接/断开时由 wifi_ota 事件处理器主动调用,
@@ -106,6 +171,11 @@ private:
// m_wifiConnected 由 m_mutex 保护(setWifiConnected 写 / checkBeforeOta 读)。
bool m_wifiConnected = false; ///< WiFi 连接状态(由 WiFi 事件推送)
// 堆低水位:设备仅 1 槽(覆盖写)。m_mutex 保护。
HeapLowWaterRecord m_heapLowWater = {};
bool m_heapLowWaterValid = false; ///< 是否有一条有效快照
uint32_t m_heapLowWaterLastMin = 0xFFFFFFFFu; ///< 上次记下的最低值
static void tempMonitorCallback(void *arg);
// RAII 锁守卫(与 SpiMaster::LockGuard 风格一致)
-41
View File
@@ -1,41 +0,0 @@
/*
* stack_overflow_hook.cpp - FreeRTOS 栈溢出钩子
*
* CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY 开启后,内核在任务切换时检查
* 栈 canary,若被踩则回调 vApplicationStackOverflowHook。
*
* 钩子在被踩栈的任务上下文中执行,此时该任务栈已损坏 —— 禁止任何依赖栈 /
* mutex / 网络栈的调用(printf 格式化、UDP sendto、xSemaphoreTake 等都可能
* 二次崩溃)。唯一安全做法:用 uart_write_bytes 往 UART0 硬写预写好的常量
* 字符串(物理外设直写,不需要栈空间),并置一个 volatile 标志,由
* HealthMonitor 巡检循环(运行在完好的 health_mon 栈上)后续用 DEBUG_ERR
* 经 UDP/UART 发详细告警。
*
* UART0 直写不受 OTA 门控抑制,是 OTA 末尾断网阶段唯一可靠的物理通道。
*/
#include <cstring>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_system.h"
// 供 HealthMonitor 巡检检测:canary 触发后置位,由巡检发详细告警。
volatile bool g_stackOverflowDetected = false;
extern "C" void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
(void)xTask;
g_stackOverflowDetected = true;
// 栈已损坏,只能做不依赖栈的最小操作。UART0 硬写常量字符串。
static const char prefix[] = "\r\n!!! STACK OVERFLOW in task: ";
uart_write_bytes(UART_NUM_0, prefix, sizeof(prefix) - 1);
// 任务名最多 configMAX_TASK_NAME_LEN,strlen 对小已知 buffer 安全
uart_write_bytes(UART_NUM_0, pcTaskName, strnlen(pcTaskName, configMAX_TASK_NAME_LEN));
static const char suffix[] = " !!!\r\n";
uart_write_bytes(UART_NUM_0, suffix, sizeof(suffix) - 1);
/* 立刻 abort:OTA 上传 100% 后若 httpd 栈溢出却不 abort,
* 前端会一直停在 100% 像“卡住”,且 TCB 已损坏无法恢复。 */
esp_system_abort("stack overflow");
}
-18
View File
@@ -1,18 +0,0 @@
/*
* stack_overflow_hook.h - FreeRTOS 栈溢出钩子标志声明
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* canary 触发时由 vApplicationStackOverflowHook 置位;
* HealthMonitor 巡检循环检测此标志,置位后用 DEBUG_ERR 发详细告警。 */
extern volatile bool g_stackOverflowDetected;
#ifdef __cplusplus
}
#endif
+82
View File
@@ -0,0 +1,82 @@
/*
* task_stack.cpp - 任务栈水位收集 + FreeRTOS 栈溢出钩子
*
* 水位表:health_mon 巡检 + /system/status 共用。
* 溢出钩子:CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY 下内核回调;
* 被踩栈上下文禁止 mutex/网络/格式化,仅 UART0 硬写 + abort。
*/
#include "task_stack.h"
#include <cstring>
#include "app_config.h"
#include "health_monitor.h"
#include "cpu_stats.h"
#include "ota_relay.h"
#include "wifi_ota.h"
#include "ble_ota.h"
#include "spi_master.h"
#include "driver/uart.h"
#include "esp_system.h"
// ============================================================
// 栈溢出钩子
// ============================================================
volatile bool g_stackOverflowDetected = false;
extern "C" void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
(void)xTask;
g_stackOverflowDetected = true;
static const char prefix[] = "\r\n!!! STACK OVERFLOW in task: ";
uart_write_bytes(UART_NUM_0, prefix, sizeof(prefix) - 1);
uart_write_bytes(UART_NUM_0, pcTaskName, strnlen(pcTaskName, configMAX_TASK_NAME_LEN));
static const char suffix[] = " !!!\r\n";
uart_write_bytes(UART_NUM_0, suffix, sizeof(suffix) - 1);
/* OTA 末尾若 httpd 栈溢出却不 abort,前端会一直停在 100%。 */
esp_system_abort("stack overflow");
}
// ============================================================
// 任务栈水位表
// ============================================================
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
{
if (!out || max == 0) {
return 0;
}
size_t n = 0;
// httpd 无公开句柄;未 httpd_start 时 xTaskGetHandle 为 NULL
if (n < max) {
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
}
if (n < max) {
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), TASK_STACK_HEALTH_MON };
}
if (n < max) {
out[n++] = { "cpu_stats", CpuStats::instance().getTaskHandle(), TASK_STACK_CPU_STATS };
}
if (n < max) {
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), TASK_STACK_OTA_RELAY };
}
if (n < max) {
out[n++] = { "spi_worker", SpiMaster::instance().getWorkerTaskHandle(), TASK_STACK_SPI_WORKER };
}
if (n < max) {
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), TASK_STACK_WIFI_OTA };
}
if (n < max) {
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, TASK_STACK_WIFI_RECONNECT };
}
if (n < max) {
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), TASK_STACK_BLE_OTA };
}
if (n < max) {
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), TASK_STACK_BLE_WORKER };
}
return n;
}
+40
View File
@@ -0,0 +1,40 @@
/*
* task_stack.h - 任务栈:水位收集 + 溢出钩子
*
* 1) collectTaskStackMetrics:供 health_mon 巡检与 /system/status 共用任务表
* 2) vApplicationStackOverflowHook / g_stackOverflowDetected:canary 溢出
*/
#pragma once
#include <stddef.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifdef __cplusplus
extern "C" {
#endif
/** canary 触发时由 vApplicationStackOverflowHook 置位(随后 abort)。 */
extern volatile bool g_stackOverflowDetected;
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
/** @brief 单个任务的栈指标描述。 */
struct TaskStackMetric {
const char* name; ///< 任务名(展示用)
TaskHandle_t handle; ///< 句柄(可能为 NULL:任务尚未创建)
size_t stack_size_bytes; ///< 创建时分配的栈大小(字节)
};
/**
* @brief 收集受监控任务的栈指标。
* @return 实际写入条数(<= max)。handle==NULL 的条目由调用方跳过。
*/
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max);
#endif /* __cplusplus */
-55
View File
@@ -1,55 +0,0 @@
/*
* task_stack_check.cpp - 任务栈水位指标收集(共享)
*
* 实现 collectTaskStackMetrics():复用 http_handlers 旧版 task_metrics[] 任务表,
* 供 health_monitor 主动巡检与 /system/status 被动报告共用。
*/
#include "task_stack_check.h"
#include "app_config.h"
#include "health_monitor.h"
#include "cpu_stats.h"
#include "ota_relay.h"
#include "wifi_ota.h"
#include "ble_ota.h"
#include "spi_master.h"
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
{
if (!out || max == 0) return 0;
size_t n = 0;
// httpd 由 esp_http_server 创建,无公开句柄;按任务名取。
// httpd 任务在 httpd_start() 后才存在,未启动时 xTaskGetHandle 返回 NULL。
if (n < max) {
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
}
if (n < max) {
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), TASK_STACK_HEALTH_MON };
}
if (n < max) {
out[n++] = { "cpu_stats", CpuStats::instance().getTaskHandle(), TASK_STACK_CPU_STATS };
}
/* 转发热路径:ota_relay(业务)+ spi_worker(帧缓冲,此前监控缺口) */
if (n < max) {
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), TASK_STACK_OTA_RELAY };
}
if (n < max) {
out[n++] = { "spi_worker", SpiMaster::instance().getWorkerTaskHandle(), TASK_STACK_SPI_WORKER };
}
if (n < max) {
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), TASK_STACK_WIFI_OTA };
}
if (n < max) {
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, TASK_STACK_WIFI_RECONNECT };
}
if (n < max) {
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), TASK_STACK_BLE_OTA };
}
if (n < max) {
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), TASK_STACK_BLE_WORKER };
}
return n;
}
-32
View File
@@ -1,32 +0,0 @@
/*
* task_stack_check.h - 任务栈水位指标收集(共享)
*
* 将受监控任务的任务名/句柄/栈大小统一收集,供:
* - health_monitor 巡检主动告警(低水位时 DEBUG_ERR,突破 OTA 门控)
* - http_handlers /system/status 被动报告(前端每秒轮询展示)
* 两处复用同一份任务表,避免散落维护。
*
* 表内含转发关键:ota_relay、spi_worker(帧缓冲热路径)。
*/
#pragma once
#include <stddef.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/** @brief 单个任务的栈指标描述。 */
struct TaskStackMetric {
const char* name; ///< 任务名(展示用)
TaskHandle_t handle; ///< FreeRTOS 任务句柄(可能为 NULL:任务尚未创建)
size_t stack_size_bytes; ///< 创建该任务时分配的栈大小(字节)
};
/**
* @brief 收集所有受监控任务的栈指标。
* @param out 输出数组。
* @param max out 数组容量。
* @return 实际写入条数(<= max)。
*
* 注意:部分任务句柄在任务创建前为 NULL,调用方应跳过 handle==NULL 的条目。
*/
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max);
+10 -9
View File
@@ -187,7 +187,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
esp_err_t err = esp_ota_begin(m_partition, OTA_WITH_SEQUENTIAL_WRITES, &m_handle);
const int64_t t1 = esp_timer_get_time();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_begin(SEQUENTIAL) 失败: %s (耗时 %lld ms)",
DEBUG_ERR(TAG, "ota_begin fail:%s %lldms",
esp_err_to_name(err), (long long)((t1 - t0) / 1000));
return beginCleanupAndReturn(err);
}
@@ -201,7 +201,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
m_ended = false;
memset(m_sha256, 0, sizeof(m_sha256));
DEBUG_DBG(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase-on-write)",
DEBUG_DBG(TAG, "ESP32 OTA begin part=%s size=%u (erase-on-write)",
m_partition->label, (unsigned)total_size);
return ESP_OK;
}
@@ -224,9 +224,8 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
if (err == ESP_OK) {
m_received += len;
} else {
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u offset=%u free=%lu)",
esp_err_to_name(err), (unsigned)len, (unsigned)m_received,
(unsigned long)esp_get_free_heap_size());
DEBUG_ERR(TAG, "ota_write %s n=%u off=%u",
esp_err_to_name(err), (unsigned)len, (unsigned)m_received);
}
return err;
}
@@ -257,7 +256,7 @@ esp_err_t Esp32OtaCore::computeShaFromPartition()
if (to_read > sizeof(s_sha_read_buf)) to_read = sizeof(s_sha_read_buf);
esp_err_t rd_err = esp_partition_read(m_partition, offset, s_sha_read_buf, to_read);
if (rd_err != ESP_OK) {
DEBUG_ERR(TAG, "SHA 计算: 分区读失败 @offset %u: %s",
DEBUG_ERR(TAG, "SHA读失败 off=%u %s",
(unsigned)offset, esp_err_to_name(rd_err));
mbedtls_sha256_free(&s_sha_ctx);
return rd_err;
@@ -327,7 +326,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
DEBUG_DBG(TAG, "ESP32 OTA 校验 SHA-256...");
esp_err_t sha_err = computeShaFromPartition();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
DEBUG_ERR(TAG, "SHA失败:%s 中止", esp_err_to_name(sha_err));
return finalizeCleanupAndReturn(sha_err);
}
@@ -353,8 +352,10 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
if (sha_match) {
DEBUG_DBG(TAG, "SHA-256 校验通过");
} else {
DEBUG_ERR(TAG, "SHA-256 不匹配: frontend=%s device=%s",
frontend_sha_hex, device_hex);
/* 与正常刷写失败一致:先报异常,再分 2 行打两个 SHA-256 */
DEBUG_ERR(TAG, "SHA-256 不匹配");
DEBUG_ERR(TAG, "frontend=%s", frontend_sha_hex);
DEBUG_ERR(TAG, "device=%s", device_hex);
return finalizeCleanupAndReturn(ESP_ERR_INVALID_VERSION);
}
} else if (out_device_sha_hex) {
+14 -16
View File
@@ -148,12 +148,12 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
// RELAYING 是中继阶段,不可中断,也拒绝。
if (h && m_sysState == OTA_SYS_GD32_STORING) {
if (m_otaChannel != channel) {
DEBUG_WARN(TAG, "GD32 存储中(通道=%d),拒绝跨通道请求(通道=%d)",
DEBUG_WARN(TAG, "GD32存中 ch=%d 拒跨通道 ch=%d",
(int)m_otaChannel, (int)channel);
unlock(h);
return ESP_ERR_INVALID_STATE;
}
DEBUG_WARN(TAG, "GD32 存储仍在进行中(同通道),中止旧存储并重新开始");
DEBUG_WARN(TAG, "GD32同通道重传:中止旧存储");
doAbortLocked(); // 清 meta + IDLE,与 abort 路径一致
// epoch 将在下方设置 GD32_STORING 后递增
}
@@ -182,7 +182,7 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
m_sysState = OTA_SYS_GD32_STORING;
m_gd32StorageDone = false;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
DEBUG_DBG(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING (epoch=%u, channel=%d)",
DEBUG_DBG(TAG, "GD32存开始 %" PRIu32 "B →STORING ep=%u ch=%d",
total_size, m_otaEpoch, (int)channel);
}
unlock(h);
@@ -242,20 +242,18 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
void OtaCoordinator::completeGd32Storage()
{
bool h = lock();
bool resume_ble = false;
if (h && m_sysState == OTA_SYS_GD32_STORING) {
m_sysState = OTA_SYS_GD32_STORED;
m_gd32StorageDone = true;
m_busySinceTick = 0; // ★ STORED 是非忙状态
resume_ble = (m_otaChannel == OTA_CHANNEL_WIFI);
DEBUG_DBG(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
/* WiFi 路径不在此处恢复 BLE:
* 旧逻辑 HTTP 一收完就 resume,55KB 极快时日志紧贴 raw upload start,
* 像「一刷新就共存恢复」。挂起到中继结束/中止(notifyRelayComplete、abort)。 */
} else if (h) {
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
}
unlock(h);
if (resume_ble) {
wifiOtaResumeBleIfNeeded();
}
}
esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
@@ -281,7 +279,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
if (m_sysState != OTA_SYS_GD32_STORED &&
m_sysState != OTA_SYS_IDLE &&
m_sysState != OTA_SYS_GD32_COMPLETE) {
DEBUG_WARN(TAG, "拒绝触发中继: 当前状态 %s (%d) 不在白名单",
DEBUG_WARN(TAG, "拒中继 state=%s(%d)",
stateToString(m_sysState), (int)m_sysState);
unlock(h);
return ESP_ERR_INVALID_STATE;
@@ -419,12 +417,12 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
if (m_otaChannel != channel) {
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
DEBUG_WARN(TAG, "ESP32收中 ch=%d 拒跨通道 ch=%d",
(int)m_otaChannel, (int)channel);
unlock(h);
return ESP_ERR_INVALID_STATE;
}
DEBUG_WARN(TAG, "ESP32 OTA 仍在接收中(同通道),中止旧会话并重新开始");
DEBUG_WARN(TAG, "ESP32同通道重传:中止旧会话");
doAbortLocked(); // abort core + IDLE + 恢复 CAN 反馈
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
}
@@ -434,7 +432,7 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
// core 仍 active ⇒ 真正在收尾,拒绝(不可 forceCleanup,防并发写分区)。
if (h && m_sysState == OTA_SYS_ESP32_FINALIZING) {
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32_FINALIZING 残留(core 已空闲),强制复位后允许新会话");
DEBUG_WARN(TAG, "FINALIZING残留(core空闲),强制复位");
doAbortLocked();
} else {
DEBUG_WARN(TAG, "系统忙 (ESP32_FINALIZING, core 活跃),拒绝 ESP32 OTA");
@@ -447,7 +445,7 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING ||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
DEBUG_WARN(TAG, "忙 state=%d(%s) 拒ESP32 OTA",
(int)m_sysState, stateToString(m_sysState));
unlock(h);
return ESP_ERR_INVALID_STATE;
@@ -488,7 +486,7 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
// 避免连续 isIdle() + getSystemState() 各加锁一次(共 2 次锁)。
ota_system_state_t cur = getSystemState();
if (!isIdleState(cur) && cur != OTA_SYS_ERROR) {
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
DEBUG_WARN(TAG, "ESP32拒且非空闲 state=%d,不强制清",
(int)cur);
if (channel == OTA_CHANNEL_WIFI) {
wifiOtaResumeBleIfNeeded();
@@ -497,9 +495,9 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
}
if (cur == OTA_SYS_ERROR) {
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝但协调器处于 ERROR,清理底层残留句柄后重试");
DEBUG_WARN(TAG, "ESP32拒但ERROR,清残留后重试");
} else {
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝但系统空闲,判定为锁残留,强制清理状态机后重试");
DEBUG_WARN(TAG, "ESP32拒但空闲,判锁残留,强制清后重试");
}
Esp32OtaCore::instance().forceCleanup();
err = beginEsp32Ota(total_size, channel);
+45 -37
View File
@@ -596,18 +596,23 @@ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_
if (DebugLog::instance().getLogLevel() < DEBUG_LOG_DBG) {
return;
}
char hex[193] = {0};
size_t n = (len > 32U) ? 32U : len;
char hx[17] = {0};
size_t n = (len > 8U) ? 8U : len;
if (payload) {
for (size_t i = 0; i < n; ++i) {
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]);
snprintf(&hx[i * 2], sizeof(hx) - i * 2, "%02X", payload[i]);
}
} else {
n = 0;
}
DEBUG_DBG("OTA", "%s resp: type=0x%02X len=%u payload[%u]=%s%s",
stage, resp_type, (unsigned)len, (unsigned)n,
n > 0 ? hex : "<empty>", len > n ? "..." : "");
/* stage 名截断,避免整行超 128B */
char stg[12];
snprintf(stg, sizeof(stg), "%.11s", stage ? stage : "?");
DEBUG_DBG("OTA", "%s rsp t=%02X n=%u",
stg, resp_type, (unsigned)len);
if (n > 0) {
DEBUG_DBG("OTA", " hx=%s%s", hx, len > n ? ".." : "");
}
}
/**
@@ -858,10 +863,9 @@ esp_err_t OtaRelay::requestGd32AppEnterBootloader()
DEBUG_WARN("OTA", "ENTER_BOOT 发送失败: %s", esp_err_to_name(boot_err));
}
/* 3) 硬门控:HELLO role=boot(给 APP 擦页+复位+BOOT 起机更长时间) */
/* 3) 硬门控:HELLO role=boot(成功时 SPI waitForBootloaderReady 已打就绪日志) */
esp_err_t wait_err = SpiMaster::instance().waitForBootloaderReady(12000);
if (wait_err == ESP_OK) {
DEBUG_INFO("OTA", "Bootloader 已就绪 (HELLO role=boot)");
return ESP_OK;
}
@@ -882,7 +886,7 @@ esp_err_t OtaRelay::requestGd32AppEnterBootloader()
DEBUG_ERR("OTA", "Bootloader HELLO 就绪失败(重试后): %s", esp_err_to_name(wait_err));
return wait_err;
}
DEBUG_INFO("OTA", "Bootloader 已就绪 (HELLO role=boot, 二次)");
/* 成功日志由 SPI waitForBootloaderReady 统一输出 */
return ESP_OK;
}
@@ -936,8 +940,8 @@ ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts)
}
}
DEBUG_WARN("OTA", "HANDSHAKE_REQ 未就绪,重试 %" PRIu32 "/%" PRIu32 ": err=%d resp_type=0x%02X resp_len=%u",
attempt + 1U, attempts, last_err, last_resp_type, (unsigned)last_resp_len);
DEBUG_WARN("OTA", "HANDSHAKE重试 %" PRIu32 "/%" PRIu32 " err=%d t=0x%02X",
attempt + 1U, attempts, last_err, last_resp_type);
vTaskDelay(pdMS_TO_TICKS(kHandshakeRetryGapMs));
}
@@ -964,7 +968,7 @@ ota_result_t OtaRelay::sendAndWaitAckShape(const char *stage, uint8_t cmd,
err = pollUntilExpectedAckShape(stage, &resp_type, resp_payload, resp_max, resp_len,
poll_timeout_ms, wait_rounds, max_extra_polls, matcher);
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !matcher(resp_payload, *resp_len)) {
DEBUG_ERR("OTA", "%s 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu",
DEBUG_ERR("OTA", "%s fail err=%d t=0x%02X n=%zu",
stage, err, resp_type, *resp_len);
return OTA_RESULT_ERR_TIMEOUT;
}
@@ -1019,14 +1023,7 @@ ota_result_t OtaRelay::doChallengeAndAuth()
OtaRelay::isConfirmPlainAck);
if (res != OTA_RESULT_OK) return res;
{
char hex[73] = {0};
size_t n = (resp_len > 36U) ? 36U : resp_len;
for (size_t i = 0; i < n; ++i) {
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", resp_payload[i]);
}
DEBUG_INFO("OTA", "CONFIRM 验证通过, resp_len=%zu, payload=%s", resp_len, hex);
}
DEBUG_INFO("OTA", "CONFIRM ok, resp_len=%zu", resp_len);
Protocol::sessionKeyInit(&m_sessionCtx, m_kSession);
return OTA_RESULT_OK;
@@ -1110,9 +1107,8 @@ ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, uint32_t flash_offset, c
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
seq, (unsigned)chunk_size, if_end,
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
DEBUG_DBG("OTA", "包%" PRIu32 " n=%u end=%08" PRIX32,
seq, (unsigned)chunk_size, if_end);
/* 不在此写 m_transferCurrentSeq:预组下一包时不应污染“当前发送序号” */
if (out_chunk_size) {
@@ -1188,8 +1184,8 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
TickType_t last_hb = xTaskGetTickCount();
while (xTaskGetTickCount() < poll_deadline) {
if ((xTaskGetTickCount() - last_hb) >= pdMS_TO_TICKS(1000)) {
DEBUG_INFO("OTA", "包 %" PRIu32 " 等 ACK… send=%" PRIu32 " type=0x%02X wait=%d credit=%u",
seq, send_i, resp_type, (int)saw_wait,
DEBUG_INFO("OTA", "包%" PRIu32 "等ACK send=%" PRIu32 " t=0x%02X cr=%u",
seq, send_i, resp_type,
(unsigned)SpiMaster::instance().remoteCredit());
last_hb = xTaskGetTickCount();
}
@@ -1244,11 +1240,11 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
}
}
DEBUG_ERR("OTA", "包 %" PRIu32 " 失败 (elapsed=%" PRIu32 " ms credit=%u type=0x%02X saw_wait=%d)",
DEBUG_ERR("OTA", "包%" PRIu32 "失败 el=%" PRIu32 "ms t=0x%02X cr=%u",
seq,
(uint32_t)((xTaskGetTickCount() - pkt_start) * portTICK_PERIOD_MS),
(unsigned)SpiMaster::instance().remoteCredit(),
resp_type, (int)saw_wait);
resp_type,
(unsigned)SpiMaster::instance().remoteCredit());
*out_result = OTA_RESULT_ERR_TIMEOUT;
return true;
}
@@ -1369,10 +1365,9 @@ bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
gd32Version = ver;
if (out_uptime) *out_uptime = uptime;
// 启动探测等场景用 DBG;OTA 成功路径由 doWaitForAppAlive 统一 INFO 总结
DEBUG_DBG("OTA", "GD32 APP 存活: uptime=%" PRIu32 "s APP=v%d.%d.%d.%d BOOT=v%d.%d.%d.%d",
DEBUG_DBG("OTA", "GD32 APP up=%" PRIu32 "s APP=v%d.%d.%d.%d",
uptime,
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build,
ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build);
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build);
return true;
}
if (out_uptime) *out_uptime = 0;
@@ -1432,7 +1427,7 @@ ota_result_t OtaRelay::doSendFinishOta()
continue;
}
DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%" PRIu32 "/10",
DEBUG_WARN("OTA", "FINISH_OTA异常 t=0x%02X n=%zu r=%" PRIu32,
resp_type, resp_len, retry + 1);
vTaskDelay(pdMS_TO_TICKS(10));
}
@@ -1482,10 +1477,8 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
return OTA_RESULT_ERR_GD32;
}
gd32_version_t ver = getSnapshot().version;
DEBUG_INFO("OTA", "GD32 OTA 完成: APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build,
ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build);
/* 版本已在 HELLO/PING 身份(SPI ID APP/BOOT)打印,此处不重复 */
DEBUG_INFO("OTA", "GD32 OTA完成");
return OTA_RESULT_OK;
}
@@ -1539,11 +1532,26 @@ void OtaRelay::taskEntry(void *pvParameters)
if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK &&
SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
session_ok = true;
/* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */
if (SpiMaster::instance().gd32IdKnown()) {
StateLockGuard lock;
gd32Version = SpiMaster::instance().gd32VersionCached();
gd32AppAlive = true;
alive = true;
DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d",
gd32Version.app_major, gd32Version.app_minor,
gd32Version.app_patch, gd32Version.app_build);
DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d",
gd32Version.boot_major, gd32Version.boot_minor,
gd32Version.boot_patch, gd32Version.boot_build);
break;
}
} else {
DEBUG_INFO("OTA", "等待 GD32 APP... (%" PRIu32 "/20)", attempt + 1U);
continue;
}
}
/* 旧固件无 HELLO 身份:回退 APP PING */
if (probeAndCacheAppAlive(2000)) {
alive = true;
break;
@@ -1576,7 +1584,7 @@ void OtaRelay::taskEntry(void *pvParameters)
// 成功日志已在 doWaitForAppAlive 输出,此处不再重复
vTaskDelay(pdMS_TO_TICKS(3000));
} else if (result == OTA_RESULT_ERR_TIMEOUT) {
DEBUG_ERR("OTA", "OTA 超时中止 (TIMEOUT=%d,含全局预算/停刷/单包)", (int)result);
DEBUG_ERR("OTA", "OTA超时中止 TIMEOUT=%d", (int)result);
} else if (result == OTA_RESULT_ERR_USER_ABORT) {
DEBUG_ERR("OTA", "OTA 用户中止 (USER_ABORT=%d)", (int)result);
} else {
+15 -10
View File
@@ -15,6 +15,11 @@ void drainOtaDataQueue()
}
}
uint32_t ble_ota_get_data_queue_depth()
{
return s_otaDataQueue ? (uint32_t)uxQueueMessagesWaiting(s_otaDataQueue) : 0;
}
BleOta& BleOta::instance()
{
static BleOta inst;
@@ -145,7 +150,7 @@ void ble_ota_boost_wifi_ps()
s_wifi_ps_boosted = true;
// 通过协调器通知 WiFi 层
OtaCoordinator::instance().notifyBleCoexStart();
DEBUG_DBG(TAG, "BLE OTA 数据传输进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
DEBUG_DBG(TAG, "BLE OTA中:暂停RSSI,保持PS_NONE");
}
/**
@@ -157,7 +162,7 @@ void ble_ota_restore_wifi_ps()
s_wifi_ps_boosted = false;
// 通过协调器通知 WiFi 层
OtaCoordinator::instance().notifyBleCoexEnd();
DEBUG_DBG(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
DEBUG_DBG(TAG, "BLE OTA数据结束:恢复RSSI轮询");
}
// ============================================================
@@ -286,8 +291,8 @@ esp_err_t BleOta::resumeStack()
if (!m_stackRunning) {
/* 仅当曾 hard-deinit 过才需要完整 serverStart */
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
(unsigned long)esp_get_free_heap_size());
DEBUG_DBG(TAG, "WiFi共存:BLE栈未运行,serverStart free=%lu",
(unsigned long)esp_get_free_heap_size());
esp_err_t err = serverStart();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "resumeStack/serverStart 失败: %s", esp_err_to_name(err));
@@ -297,9 +302,9 @@ esp_err_t BleOta::resumeStack()
return ESP_OK;
}
/* 软挂起恢复:重新开广播即可 */
DEBUG_INFO(TAG, "WiFi OTA 结束:恢复 BLE 广播 free=%lu",
(unsigned long)esp_get_free_heap_size());
/* 软挂起恢复:重新开广播即可(非 BLE OTA 会话,仅射频共存;默认 INFO 不刷) */
DEBUG_DBG(TAG, "WiFi共存:恢复BLE广播 free=%lu",
(unsigned long)esp_get_free_heap_size());
m_bleReady = true;
ble_ota_start_advertising();
return ESP_OK;
@@ -375,17 +380,17 @@ void BleOta::taskEntry(void *pvParameters)
12, // 略提高:尽快排空队列,减少 withoutResponse 连发丢包
NULL);
if (wk_ok != pdPASS) {
DEBUG_ERR(TAG, "OTA 数据工作线程创建失败,BLE OTA 数据传输不可用");
DEBUG_ERR(TAG, "OTA工作线程创建失败,BLE不可用");
vQueueDelete(s_otaDataQueue);
s_otaDataQueue = nullptr;
}
} else {
DEBUG_ERR(TAG, "OTA 数据队列创建失败,BLE OTA 数据传输不可用");
DEBUG_ERR(TAG, "OTA队列创建失败,BLE不可用");
}
esp_err_t err = BleOta::instance().serverStart();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用",
DEBUG_ERR(TAG, "BLE serverStart失败:%s",
esp_err_to_name(err));
}
+8
View File
@@ -81,4 +81,12 @@ public:
bool m_suspendedForWifiOta = false; ///< 因 WiFi OTA 主动挂起
};
// ── 堆低水位快照辅助 getter(不在 BleOta 类内,file-static 资源访问) ──
/** @brief BLE OTA 数据队列当前深度(每个 item 持有 malloc 的 data 缓冲)。
* 供 /system/heap_low_log 采样。非 OTA 期返回 0。 */
uint32_t ble_ota_get_data_queue_depth();
/** @brief BLE reorder buffer 已缓存的乱序包数(每个 slot 持有 malloc 的 payload)。
* 供 /system/heap_low_log 采样。非 ESP32 OTA 期返回 0。 */
uint32_t ble_ota_get_reorder_cached_count();
#endif // BLE_OTA_H
+43 -40
View File
@@ -62,15 +62,14 @@ uint16_t ble_ota_log_conn_params(uint16_t conn_handle, const char *phase)
struct ble_gap_conn_desc desc = {};
int rc = ble_gap_conn_find(conn_handle, &desc);
if (rc != 0) {
DEBUG_WARN(TAG, "%s读取 BLE 连接参数失败: rc=%d", phase, rc);
DEBUG_WARN(TAG, "%sBLE param读失败 rc=%d", phase, rc);
return 0;
}
DEBUG_DBG(TAG, "%sBLE 连接参数: interval=%.2fms latency=%u supervision=%ums",
DEBUG_DBG(TAG, "%sitv=%.2f lat=%u",
phase,
desc.conn_itvl * 1.25f,
(unsigned)desc.conn_latency,
(unsigned)desc.supervision_timeout * 10U);
(unsigned)desc.conn_latency);
return desc.conn_itvl;
}
@@ -97,12 +96,10 @@ int ble_ota_request_conn_params(uint16_t conn_handle, bool windows_compatible)
params.supervision_timeout = 200;
int rc = ble_gap_update_params(conn_handle, &params);
DEBUG_DBG(TAG, "请求 BLE 连接参数(%s): interval=%.1f..%.1fms ce=%u..%u rc=%d",
windows_compatible ? "Windows兼容" : "高速",
DEBUG_DBG(TAG, "BLEp(%s) %.1f-%.1f rc=%d",
windows_compatible ? "Win" : "Hi",
params.itvl_min * 1.25f,
params.itvl_max * 1.25f,
(unsigned)params.min_ce_len,
(unsigned)params.max_ce_len,
rc);
return rc;
}
@@ -129,11 +126,8 @@ void esp32_reorder_alloc()
s_fallback_ooo_count = 0;
if (!s_reorder.slots || !s_reorder.lens) {
DEBUG_WARN(TAG, "Reorder buffer 分配失败 (slots=%p lens=%p),回退到丢弃策略。"
"heap: free=%u min=%u",
s_reorder.slots, s_reorder.lens,
(unsigned)esp_get_free_heap_size(),
(unsigned)esp_get_minimum_free_heap_size());
DEBUG_WARN(TAG, "Reorder分配失败,回退丢弃 free=%u",
(unsigned)esp_get_free_heap_size());
// 释放可能部分成功的分配
free(s_reorder.slots); s_reorder.slots = nullptr;
free(s_reorder.lens); s_reorder.lens = nullptr;
@@ -143,7 +137,7 @@ void esp32_reorder_alloc()
}
s_reorder.active = true;
s_reorder.fallback = false;
DEBUG_DBG(TAG, "Reorder buffer 已启用: window=%u payloadCap=%u heap=%u",
DEBUG_DBG(TAG, "Reorder ok win=%u cap=%u heap=%u",
(unsigned)s_reorder.windowSize, (unsigned)s_reorder.payloadCap,
(unsigned)esp_get_free_heap_size());
}
@@ -178,6 +172,16 @@ void esp32_reorder_free()
s_fallback_ooo_count = 0;
}
/**
* @brief 取当前 reorder buffer 已缓存的乱序包数(每个 slot 持有 malloc 的 payload)。
* 供堆低水位快照采样,定位"OTA 期低谷是不是 BLE reorder 引起"。
* 非 OTA 期 active=false,返回 0。
*/
uint32_t ble_ota_get_reorder_cached_count()
{
return s_reorder.active ? s_reorder.cachedCount : 0;
}
/**
* @brief flush expected 对应的连续缓存包到 flash,并释放对应 slot。
* 由 esp32_write_ordered 在每次 expected 推进后调用。
@@ -249,14 +253,14 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
if (shouldAck) {
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
DEBUG_WARN(TAG, "ESP32 乱序包跳过(回退): seq=%u (expected=%u),节流 ACK bytes=%u (ooo=%u)",
DEBUG_WARN(TAG, "乱序 s=%u e=%u ACK=%u ooo=%u",
seq, (unsigned)s_reorder.expected, (unsigned)curReceived,
(unsigned)s_fallback_ooo_count);
sendWindowAck(cmd, s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
curReceived);
s_fallback_ooo_count = 0;
} else {
DEBUG_DBG(TAG, "ESP32 乱序包跳过(回退): seq=%u (expected=%u),抑制 ACK (ooo=%u)",
DEBUG_DBG(TAG, "ESP32乱序 s=%u e=%u 抑ACK ooo=%u",
seq, (unsigned)s_reorder.expected,
(unsigned)s_fallback_ooo_count);
}
@@ -289,7 +293,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
s_reorder.expected++;
uint32_t flushed = esp32_reorder_flush();
if (flushed > 0) {
DEBUG_INFO(TAG, "Reorder 缺口已补齐: seq=%u,连续 flush=%u 包,expected→%u",
DEBUG_INFO(TAG, "Reorder补齐 seq=%u flush=%u exp→%u",
seq, (unsigned)flushed, (unsigned)s_reorder.expected);
}
return ESP_OK;
@@ -300,7 +304,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
if (idx < s_reorder.windowSize) {
if (s_reorder.slots[idx]) {
// 该 slot 已被占用(重复的乱序包),跳过
DEBUG_DBG(TAG, "Reorder slot 已占: seq=%u idx=%u,跳过重复包",
DEBUG_DBG(TAG, "Reorder占 s=%u i=%u 跳过",
seq, (unsigned)idx);
return ESP_OK;
}
@@ -309,8 +313,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
uint8_t *copy = (uint8_t *)malloc(data_len);
if (!copy) {
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
DEBUG_ERR(TAG, "Reorder slot malloc 失败: seq=%u expected=%u bytes=%u,"
"ACK 报告缺口并 abort",
DEBUG_ERR(TAG, "Reorder OOM s=%u e=%u n=%u ACK+abort",
seq, (unsigned)s_reorder.expected, (unsigned)curReceived);
sendWindowAck(cmd,
s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
@@ -322,18 +325,18 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
s_reorder.lens[idx] = data_len;
s_reorder.cachedCount++;
if (s_reorder.cachedCount == 1U) {
DEBUG_INFO(TAG, "Reorder 检测到缺口: expected=%u,开始缓存 seq=%u",
DEBUG_INFO(TAG, "Reorder缺口 exp=%u cache seq=%u",
(unsigned)s_reorder.expected, seq);
}
DEBUG_DBG(TAG, "Reorder 缓存乱序包: seq=%u idx=%u (expected=%u)",
DEBUG_DBG(TAG, "Reorder cache s=%u i=%u exp=%u",
seq, (unsigned)idx, (unsigned)s_reorder.expected);
return ESP_OK;
}
// seq 超出窗口范围(防御性,理论不会发生)
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
DEBUG_WARN(TAG, "ESP32 包超出 reorder 窗口: seq=%u (expected=%u base=%u win=%u),ACK bytes=%u",
seq, (unsigned)s_reorder.expected, (unsigned)s_reorder.windowBase,
DEBUG_WARN(TAG, "超窗 s=%u e=%u w=%u ACK=%u",
seq, (unsigned)s_reorder.expected,
(unsigned)s_reorder.windowSize, (unsigned)curReceived);
sendWindowAck(cmd, s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
curReceived);
@@ -448,7 +451,7 @@ void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
esp_err_t err = BleOta::instance().notifyResponse(ack, sizeof(ack));
if (err != ESP_OK) {
DEBUG_WARN(TAG, "发送 ACK 失败: cmd=0x%02X seq=%u err=%s",
DEBUG_WARN(TAG, "ACK失败 cmd=0x%02X seq=%u %s",
cmd, seq, esp_err_to_name(err));
} else {
DEBUG_DBG(TAG, "已发送 ACK: cmd=0x%02X seq=%u bytes=%lu",
@@ -530,7 +533,7 @@ void ble_gd32_relay_wait_task(void *pvParameters)
ota_result_t result = (ota_result_t)((int32_t)result_msg);
if (result == OTA_RESULT_OK) {
DEBUG_INFO(TAG, "GD32 BLE OTA 中继完成,通知前端 (stage=0x11, session=%u)", session_id);
DEBUG_INFO(TAG, "GD32中继完成 stage=0x11 sess=%u", session_id);
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_DONE,
packBleFinishValue(session_id, 0));
} else {
@@ -596,7 +599,7 @@ int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
memcpy(heap_buf, buf, (size_t)recv_len);
BleOtaDataBatch item = { heap_buf, (size_t)recv_len };
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
DEBUG_WARN(TAG, "DATA_IN 工作队列满,丢弃 %d 字节(等待客户端重传)", recv_len);
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
free(heap_buf);
}
} else {
@@ -626,12 +629,12 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
if (Esp32OtaCore::instance().isActive()) {
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
if (curCh != OTA_CHANNEL_BLE) {
DEBUG_WARN(TAG, "ESP32 OTA 被其他通道占用(ch=%d),BLE 拒绝抢占",
DEBUG_WARN(TAG, "ESP32被ch=%d占用,BLE拒抢",
(int)curCh);
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return true;
}
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,保留 reorder 状态");
DEBUG_INFO(TAG, "ESP32已激活 seq=0重传,保留reorder");
} else {
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
esp_err_t err = ble_esp32_ota_begin(total_size);
@@ -696,7 +699,7 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
isLastPacket ||
firstPacketReady)) {
if (isLastPacket || transferComplete) {
DEBUG_DBG(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
DEBUG_DBG(TAG, "ESP32尾包 s=%u n=%zu rx=%u tot=%" PRIu32,
seq, data_len, (unsigned int)afterReceived, total_size);
} else if (firstPacketReady) {
DEBUG_DBG(TAG, "ESP32 begin 就绪 ACK: seq=0 received=%u total=%" PRIu32,
@@ -709,7 +712,7 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
}
if (transferComplete) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
DEBUG_INFO(TAG, "ESP32收完 %u/%" PRIu32 "B chunks=%u",
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
BleOta::instance().m_esp32LastCmd = cmd;
@@ -743,12 +746,12 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
// (否则会 erase 分区、清 meta,毁掉已写入进度)。
if (s_ble_gd32_meta_active && s_ble_gd32_meta.total_size == total_size) {
if (s_ble_gd32_meta.stored_size > 0) {
DEBUG_INFO(TAG, "GD32 会话已激活,seq=0 视为重传,跳过写入 stored=%" PRIu32,
DEBUG_INFO(TAG, "GD32已激活 seq=0重传 skip stored=%" PRIu32,
s_ble_gd32_meta.stored_size);
sendWindowAck(cmd, 0, s_ble_gd32_meta.stored_size);
return false;
}
DEBUG_INFO(TAG, "GD32 会话已激活且未落盘,seq=0 重传首块(不重新 prepare)");
DEBUG_INFO(TAG, "GD32未落盘 seq=0重传首块(不prepare)");
// fall through:stored_size==0 时重写 offset 0
} else {
// 与 WiFi 共用 prepareGd32Storage:validate + begin + erase + clearMeta + saveMeta
@@ -810,12 +813,12 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
(s_gd32_ooo_count >= BLE_OTA_ACK_WINDOW) ||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
if (shouldAck) {
DEBUG_WARN(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),节流 ACK bytes=%" PRIu32 " (ooo=%u)",
DEBUG_WARN(TAG, "GD32乱序 s=%u last=%" PRIu32 " ACK=%" PRIu32 " ooo=%u",
seq, meta.last_seq, meta.stored_size, (unsigned)s_gd32_ooo_count);
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
s_gd32_ooo_count = 0;
} else {
DEBUG_DBG(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),抑制 ACK (ooo=%u)",
DEBUG_DBG(TAG, "GD32乱序 s=%u last=%" PRIu32 " 抑ACK ooo=%u",
seq, meta.last_seq, (unsigned)s_gd32_ooo_count);
}
return false; // 外层推进 offset,继续下一包
@@ -881,14 +884,14 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
// --- 检查 GD32 固件是否接收完毕 ---
if (transfer_complete) {
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
DEBUG_INFO(TAG, "GD32收完 %" PRIu32 "/%" PRIu32 "B chunks=%u",
meta.stored_size, meta.total_size, seq + 1);
s_ble_gd32_meta_active = false;
ble_ota_restore_wifi_ps();
// 与 WiFi 共用 finishGd32StorageAfterReceive:complete + SHA
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
DEBUG_ERR(TAG, "GD32 SHA失败:%s", esp_err_to_name(sha_err));
}
}
return false;
@@ -937,8 +940,8 @@ void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
// 当前包总长度: header(7) + payload(data_len)
size_t packet_len = 7 + data_len;
DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu offset=%zu",
cmd, seq, total_size, data_len, offset);
DEBUG_DBG(TAG, "DATA_IN c=0x%02X s=%u tot=%" PRIu32 " n=%zu",
cmd, seq, total_size, data_len);
// 分发到对应通道的包处理器
bool abort_batch = false;
@@ -985,7 +988,7 @@ void ble_ota_data_worker_task(void *pvParameters)
// 若 BLE 已断开或 WiFi OTA 挂起,丢弃队列中的残留数据
if (!BleOta::instance().isConnected() ||
BleOta::instance().isSuspendedForWifiOta()) {
DEBUG_DBG(TAG, "BLE 不可用/挂起,丢弃 %zu 字节队列数据", item.len);
DEBUG_DBG(TAG, "BLE不可用 丢%zuB队列", item.len);
free(item.data);
continue;
}
+21 -21
View File
@@ -38,7 +38,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
}
if (len < 35) {
DEBUG_WARN(TAG, "CTRL 0xF7 数据长度不足 35(前端 SHA-256 必传),拒绝触发 GD32 中继");
DEBUG_WARN(TAG, "CTRL 0xF7 len<35 缺SHA,拒GD32中继");
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
packBleFinishValue(session_id, 1));
return 0;
@@ -60,22 +60,21 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha);
shaBytesToHexUpper(device_sha, device_sha_hex);
sendSha256Notify(device_sha, session_id);
DEBUG_DBG(TAG, "GD32 前端 SHA-256: %s, session=%u", frontend_sha_hex, session_id);
DEBUG_DBG(TAG, "GD32 缓存 SHA-256: %s", device_sha_hex);
if (err == ESP_ERR_INVALID_CRC) {
DEBUG_ERR(TAG, "GD32 缓存 SHA-256 与前端文件不匹配,拒绝触发 SPI 中继");
DEBUG_ERR(TAG, "GD32 SHA-256 不匹配");
DEBUG_ERR(TAG, "frontend=%s", frontend_sha_hex);
DEBUG_ERR(TAG, "device=%s", device_sha_hex);
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
packBleFinishValue(session_id, 0));
return 0;
} else if (err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 SHA-256 计算/读取失败: %s", esp_err_to_name(err));
DEBUG_ERR(TAG, "GD32 SHA 失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, 3));
return 0;
}
DEBUG_INFO(TAG, "GD32 固件就绪且 SHA-256 匹配 (%" PRIu32 " / %" PRIu32 " 字节),通过协调器触发中继",
DEBUG_INFO(TAG, "GD32就绪 SHA匹配 %" PRIu32 "/%" PRIu32 ",触发中继",
meta.stored_size, meta.total_size);
if (OtaRelay::getResultQueue()) {
uint32_t stale = 0;
@@ -93,7 +92,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_START,
packBleFinishValue(session_id, 0));
DEBUG_INFO(TAG, "已通知前端: GD32 中继已开始 (stage=0x10, session=%u)", session_id);
DEBUG_INFO(TAG, "GD32中继开始 stage=0x10 sess=%u", session_id);
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)malloc(sizeof(BleGd32RelayWaitCtx));
if (!ctx) {
@@ -142,7 +141,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
// 强制要求 32B 前端 SHA-256:与 WiFi 流程对齐,设备端读回分区计算并比对,
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH。
if (len < 33) {
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33(前端 SHA-256 必传),拒绝触发");
DEBUG_WARN(TAG, "CTRL 0xF8 len<33 缺SHA,拒绝");
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)ESP_ERR_INVALID_ARG);
return 0;
}
@@ -159,7 +158,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
}
finish_ctx->sha_hex[64] = '\0';
finish_ctx->session_id = session_id;
DEBUG_DBG(TAG, "前端 SHA-256: %s, session=%u", finish_ctx->sha_hex, session_id);
DEBUG_DBG(TAG, "feSHA=%s sess=%u", finish_ctx->sha_hex, session_id);
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
@@ -179,7 +178,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
}
} else if (buf[0] == BLE_OTA_CTRL_RESTART_ESP32) {
// 重启 ESP32:GATT 回调中仅推送 FINISH,延时 + 重启交给独立 task,避免卡 NimBLE host
DEBUG_INFO(TAG, "收到 0xFA 重启请求,创建 restart task(500ms 后 esp_restart())");
DEBUG_INFO(TAG, "0xFA重启,建restart task(500ms)");
if (BleOta::instance().m_esp32RebootPending) {
DEBUG_WARN(TAG, "0xFA 重启已在进行中,忽略重复 CTRL");
return 0;
@@ -207,11 +206,11 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
}
ota_system_state_t cur_state = OtaCoordinator::instance().getSystemState();
DEBUG_WARN(TAG, "收到 0xFB 中止请求: state=%s session=%u",
DEBUG_WARN(TAG, "0xFB abort state=%s sess=%u",
OtaCoordinator::stateToString(cur_state), session_id);
if (cur_state == OTA_SYS_GD32_RELAYING) {
DEBUG_WARN(TAG, "GD32 SPI 中继进行中,无法中止(请等待完成或断开 BLE)");
DEBUG_WARN(TAG, "GD32 SPI中继中,无法中止,请等待");
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, 2));
return 0;
@@ -466,7 +465,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
{
ota_system_state_t cur = OtaCoordinator::instance().getSystemState();
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
DEBUG_WARN(TAG, "BLE断但%s进行中,保留状态",
OtaCoordinator::stateToString(cur));
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
cur == OTA_SYS_ESP32_FINALIZING ||
@@ -479,7 +478,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
ch = snap.channel;
}
if (ch == OTA_CHANNEL_BLE) {
DEBUG_WARN(TAG, "BLE 断开,中止 BLE 通道 OTA (state=%s)",
DEBUG_WARN(TAG, "BLE断,中止OTA state=%s",
OtaCoordinator::stateToString(cur));
ble_ota_session_reset();
} else {
@@ -502,7 +501,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
return 0;
case BLE_GAP_EVENT_SUBSCRIBE:
DEBUG_INFO(TAG, "CCC 描述符更新: conn=%d attr=%d notify=%d indicate=%d",
DEBUG_INFO(TAG, "CCC upd conn=%d attr=%d n=%d i=%d",
event->subscribe.conn_handle,
event->subscribe.attr_handle,
event->subscribe.cur_notify,
@@ -543,7 +542,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
const char *tx_phy = (tx == BLE_GAP_LE_PHY_2M) ? "2M" :
(tx == BLE_GAP_LE_PHY_1M) ? "1M" :
(tx == BLE_GAP_LE_PHY_CODED) ? "CODED" : "?";
DEBUG_INFO(TAG, "PHY 更新: status=%d rx=%s tx=%s",
DEBUG_INFO(TAG, "PHY %d %s/%s",
event->phy_updated.status, rx_phy, tx_phy);
return 0;
}
@@ -573,18 +572,18 @@ void ble_ota_start_advertising(void) // kept as free function for BLE callback
uint8_t own_addr_type;
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
DEBUG_ERR(TAG, "推断地址类型失败: rc=%d, 尝试生成随机地址", rc);
DEBUG_ERR(TAG, "推断地址失败 rc=%d,试随机地址", rc);
// 生成不可解析随机地址作为 fallback
ble_addr_t rnd_addr;
rc = ble_hs_id_gen_rnd(0, &rnd_addr);
if (rc != 0) {
DEBUG_ERR(TAG, "生成随机地址也失败: rc=%d, BLE 广播无法启动", rc);
DEBUG_ERR(TAG, "随机地址失败 rc=%d", rc);
return;
}
own_addr_type = rnd_addr.type;
}
DEBUG_INFO(TAG, "使用地址类型: %s", own_addr_type == BLE_OWN_ADDR_PUBLIC ? "Public" : "Random");
DEBUG_DBG(TAG, "BLE addr type: %s", own_addr_type == BLE_OWN_ADDR_PUBLIC ? "Public" : "Random");
// --- 构建广播数据 ---
uint8_t adv_data[31] = {0};
@@ -640,7 +639,8 @@ void ble_ota_start_advertising(void) // kept as free function for BLE callback
if (rc != 0) {
DEBUG_ERR(TAG, "启动广播失败: %d", rc);
} else {
DEBUG_INFO(TAG, "BLE 广播已启动: \"%s\"", name);
/* 冷启动与 WiFi 共存恢复都会走这里;常态 INFO 不刷,避免上传日志夹杂 BLE */
DEBUG_DBG(TAG, "BLE广播就绪: \"%s\"", name);
}
}
+455 -167
View File
@@ -3,6 +3,7 @@
*/
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <inttypes.h>
#include <algorithm>
#include "freertos/FreeRTOS.h"
@@ -17,6 +18,7 @@
#include "esp_task_wdt.h"
#include "esp_heap_caps.h"
#include "esp_rom_sys.h"
#include "soc/soc.h"
#include "cJSON.h"
#include "app_config.h"
@@ -32,7 +34,8 @@
#include "spi_master.h"
#include "canfd_bridge.h"
#include "monitor/can_feedback_task.h"
#include "monitor/task_stack_check.h"
#include "monitor/gd32_adc_feedback.h"
#include "monitor/task_stack.h"
#include "ble_ota.h"
#include "system_boot.h"
@@ -235,7 +238,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
http_set_cors_headers(req);
uint8_t *recv_buf = s_ota_upload_recv;
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d free=%lu largest=%lu",
DEBUG_WARN("WIFI", "POST /ota/upload clen=%d free=%lu big=%lu",
req->content_len,
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
@@ -318,7 +321,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
size_t total_firmware_written = 0;
size_t total_received = json_header_len;
DEBUG_INFO("WIFI", "ESP32 OTA 开始收流: content_len=%d json_hdr=%d fw_expect=%d",
DEBUG_INFO("WIFI", "ESP32收流 clen=%d hdr=%d exp=%d",
req->content_len, json_header_len,
(req->content_len > json_header_len)
? (req->content_len - json_header_len) : 0);
@@ -338,7 +341,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
if (ret <= 0) {
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d free=%lu",
DEBUG_ERR("WIFI", "HTTP recv fail ret=%d OTA %u/%d free=%lu",
ret, (unsigned)total_received, req->content_len,
(unsigned long)esp_get_free_heap_size());
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
@@ -377,10 +380,9 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
httpd_resp_set_hdr(req, "Connection", "close");
esp_err_t send_err = httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
if (send_err != ESP_OK) {
DEBUG_ERR("WIFI", "ESP32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
DEBUG_ERR("WIFI", "ESP32 OTA回包失败: %s free=%lu",
esp_err_to_name(send_err),
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
(unsigned long)esp_get_free_heap_size());
/* 再试一次极简回包 */
send_err = httpd_resp_send(req, "{\"status\":\"ok\",\"target\":\"esp32\"}",
HTTPD_RESP_USE_STRLEN);
@@ -388,8 +390,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
DEBUG_ERR("WIFI", "ESP32 OTA 简短响应仍失败: %s", esp_err_to_name(send_err));
}
} else {
DEBUG_INFO("WIFI",
"ESP32 OTA 存储+响应完成: %u B elapsed=%lld ms free=%lu",
DEBUG_INFO("WIFI", "ESP32存完 %uB %lldms free=%lu",
(unsigned int)core.getReceived(),
(long long)((esp_timer_get_time() - t_recv0) / 1000),
(unsigned long)esp_get_free_heap_size());
@@ -505,11 +506,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
if (sha_err != ESP_OK) {
/* 存盘已完成,仍回 status=ok;relay 阶段会再比前端 SHA。
* 此处必须醒目告警,避免只当 INFO 被忽略。 */
DEBUG_WARN("WIFI",
"GD32 上传完成但 SHA-256 预计算失败: %s(仍返回 stored ok,/ota/relay 将再校验)",
DEBUG_WARN("WIFI", "GD32 SHA预算失败:%s (relay再校)",
esp_err_to_name(sha_err));
DEBUG_ERR("WIFI", "GD32 finishGd32StorageAfterReceive failed: %s",
esp_err_to_name(sha_err));
}
}
@@ -519,10 +517,9 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
httpd_resp_set_hdr(req, "Connection", "close");
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
if (send_err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
DEBUG_ERR("WIFI", "GD32 OTA回包失败: %s free=%lu",
esp_err_to_name(send_err),
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
(unsigned long)esp_get_free_heap_size());
} else {
DEBUG_INFO("WIFI", "GD32 OTA 存储+响应完成: %u / %u free=%lu",
(unsigned)gd32_offset, expected_fw_size_raw,
@@ -581,6 +578,11 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
}
if (verify_err == ESP_ERR_INVALID_CRC) {
DEBUG_ERR("OTA", "GD32 SHA-256 不匹配");
DEBUG_ERR("OTA", "frontend=%s", frontend_sha_hex);
DEBUG_ERR("OTA", "device=%s", device_sha_hex);
/* 未进中继:须恢复 WiFi 上传期挂起的 BLE */
(void)BleOta::instance().resumeStack();
char err_resp[256];
snprintf(err_resp, sizeof(err_resp),
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", "
@@ -589,12 +591,14 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
httpd_resp_send(req, err_resp, strlen(err_resp));
return ESP_FAIL;
} else if (verify_err != ESP_OK) {
(void)BleOta::instance().resumeStack();
return http_send_json_error(req, "500 Internal Server Error",
"sha_compute_failed", "SHA compute failed");
}
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
if (trigger_err != ESP_OK) {
(void)BleOta::instance().resumeStack();
return http_send_json_error(req, "400 Bad Request",
"relay_trigger_failed",
"relay trigger failed - system busy or firmware not ready");
@@ -630,8 +634,53 @@ esp_err_t logs_handler(httpd_req_t *req)
// ============================================================
// GET /can/feedback — 三路 CAN 反馈快照(GD32 推送,ESP32 缓存)
// HTTP JSON 共享工作区(.bss 单份 ~3KB)
// 供 /system/status、/can/feedback、/system/heap_low_log 串行复用;
// 互斥保护,禁止并发写同一缓冲(httpd 多 worker 时会排队)。
// ============================================================
static constexpr size_t HTTP_JSON_WORK_CAP = 3072;
static char s_http_json_work[HTTP_JSON_WORK_CAP];
static SemaphoreHandle_t s_http_json_mux = nullptr;
static portMUX_TYPE s_http_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
static SemaphoreHandle_t httpJsonWorkMutex(void)
{
if (s_http_json_mux == nullptr) {
portENTER_CRITICAL(&s_http_json_init_mux);
if (s_http_json_mux == nullptr) {
s_http_json_mux = xSemaphoreCreateMutex();
}
portEXIT_CRITICAL(&s_http_json_init_mux);
}
return s_http_json_mux;
}
// ============================================================
// GET /can/feedback — 三路 CAN 反馈快照(GD32 推送,ESP32 缓存)
// 分块发送:复用 s_http_json_work,一次 HTTP 内按通道 send_chunk;
// 前端仍一次 fetch 收完整 JSON。
// ============================================================
/** 监控预览:data hex 最多 8 字节(16 字符),避免 FD 64B 撑爆分块 */
static constexpr uint8_t FEEDBACK_DATA_HEX_MAX = 8;
static int feedback_append(char *buf, size_t cap, int off, const char *fmt, ...)
{
if (!buf || cap == 0 || off < 0 || (size_t)off >= cap) {
return -1;
}
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(buf + off, cap - (size_t)off, fmt, ap);
va_end(ap);
if (n < 0) {
return -1;
}
if (off + n >= (int)cap) {
return -1; /* 分块装不下:调用方截断 entries */
}
return off + n;
}
esp_err_t can_feedback_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
@@ -642,20 +691,61 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
valid[i] = getLatestCanFeedback(i, &results[i]);
}
/* 静态响应缓冲,减少频繁大块堆分配 */
static char s_feedback_json[8192];
char *resp = s_feedback_json;
memset(resp, 0, sizeof(s_feedback_json));
const bool feat_known = SpiMaster::instance().gd32FeatKnown();
const uint32_t feat = SpiMaster::instance().gd32FeatFlags();
const bool ch_en[CAN_CHANNEL_MAX] = {
!feat_known || (feat & V2_FEAT_CAN_CH0) != 0U,
!feat_known || (feat & V2_FEAT_CAN_CH1) != 0U,
!feat_known || (feat & V2_FEAT_CAN_CH2) != 0U,
};
const unsigned ch_mask = feat_known
? (unsigned)((feat & V2_FEAT_CAN_CH_MASK) >> V2_FEAT_CAN_CH_SHIFT)
: 0x7U;
SemaphoreHandle_t mux = httpJsonWorkMutex();
if (mux && xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "fb busy");
return ESP_FAIL;
}
char *work = s_http_json_work;
httpd_resp_set_type(req, "application/json");
auto fail_unlock = [&](esp_err_t err) -> esp_err_t {
if (mux) {
xSemaphoreGive(mux);
}
return err;
};
int n = snprintf(work, HTTP_JSON_WORK_CAP,
"{\"feat_known\":%s,\"can_ch_mask\":%u,\"can_brs\":%s,\"channels\":[",
feat_known ? "true" : "false",
ch_mask,
(feat_known && (feat & V2_FEAT_CAN_BRS)) ? "true" : "false");
if (n < 0 || (size_t)n >= HTTP_JSON_WORK_CAP) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "fb hdr");
return fail_unlock(ESP_FAIL);
}
if (httpd_resp_send_chunk(req, work, n) != ESP_OK) {
return fail_unlock(ESP_FAIL);
}
char *p = resp;
p += sprintf(p, "{\"channels\":[");
for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) {
const can_feedback_result_t &r = results[i];
if (i > 0) p += sprintf(p, ",");
p += sprintf(p, "{\"ch\":%u,\"valid\":%s", (unsigned)i,
valid[i] ? "true" : "false");
if (valid[i]) {
p += sprintf(p,
int off = 0;
off = feedback_append(work, HTTP_JSON_WORK_CAP, off,
"%s{\"ch\":%u,\"enabled\":%s,\"valid\":%s",
i == 0 ? "" : ",",
(unsigned)i,
ch_en[i] ? "true" : "false",
valid[i] ? "true" : "false");
if (off < 0) {
break;
}
if (valid[i] && ch_en[i]) {
off = feedback_append(work, HTTP_JSON_WORK_CAP, off,
",\"mode\":%u,\"seq\":%lu,\"rx\":%lu,\"tx\":%lu,"
"\"drop\":%lu,\"err\":%lu,\"tec\":%u,\"rec\":%u,"
"\"bus_state\":%u,\"count\":%u,\"entries\":[",
@@ -669,33 +759,74 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
(unsigned)r.stats.rec,
(unsigned)r.stats.bus_state,
(unsigned)r.total_count);
if (off < 0) {
break;
}
uint8_t emitted = 0;
for (uint8_t j = 0; j < r.total_count && j < CANFD_ID_SNAPSHOT_MAX; ++j) {
const id_snapshot_entry_t &e = r.entries[j];
if (j > 0) p += sprintf(p, ",");
p += sprintf(p,
"{\"id\":\"0x%08lX\",\"dlc\":%u,\"fd\":%u,"
uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
? e.dlc : (uint8_t)CANFD_ID_SNAPSHOT_DATA_MAX;
if (dataLen > FEEDBACK_DATA_HEX_MAX) {
dataLen = FEEDBACK_DATA_HEX_MAX;
}
/* 先估长度:装不下则截断 entries(count 仍报真实 total) */
char entry_tmp[96 + FEEDBACK_DATA_HEX_MAX * 2];
int elen = snprintf(entry_tmp, sizeof(entry_tmp),
"%s{\"id\":\"0x%08lX\",\"dlc\":%u,\"fd\":%u,"
"\"brs\":%u,\"esi\":%u,\"data\":\"",
emitted ? "," : "",
(unsigned long)e.id,
(unsigned)e.dlc,
(unsigned)e.fd_flag,
(unsigned)e.brs,
(unsigned)e.esi);
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
? e.dlc : CANFD_ID_SNAPSHOT_DATA_MAX;
for (uint8_t d = 0; d < dataLen; ++d) {
p += sprintf(p, "%02X", e.data[d]);
if (elen < 0 || (size_t)elen >= sizeof(entry_tmp)) {
break;
}
p += sprintf(p, "\"}");
}
p += sprintf(p, "]");
}
p += sprintf(p, "}");
}
p += sprintf(p, "]}");
for (uint8_t d = 0; d < dataLen; ++d) {
int hn = snprintf(entry_tmp + elen, sizeof(entry_tmp) - (size_t)elen,
"%02X", e.data[d]);
if (hn != 2) {
break;
}
elen += hn;
}
if (elen + 2 >= (int)sizeof(entry_tmp)) {
break;
}
entry_tmp[elen++] = '"';
entry_tmp[elen++] = '}';
entry_tmp[elen] = '\0';
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, (int)(p - resp));
return ESP_OK;
if (off + elen + 2 >= (int)HTTP_JSON_WORK_CAP) {
break; /* 本通道 entries 截断 */
}
memcpy(work + off, entry_tmp, (size_t)elen);
off += elen;
emitted++;
}
off = feedback_append(work, HTTP_JSON_WORK_CAP, off, "]");
if (off < 0) {
break;
}
}
off = feedback_append(work, HTTP_JSON_WORK_CAP, off, "}");
if (off < 0) {
break;
}
if (httpd_resp_send_chunk(req, work, off) != ESP_OK) {
return fail_unlock(ESP_FAIL);
}
}
if (httpd_resp_send_chunk(req, "]}", 2) != ESP_OK) {
return fail_unlock(ESP_FAIL);
}
esp_err_t end_err = httpd_resp_send_chunk(req, NULL, 0);
return fail_unlock(end_err == ESP_OK ? ESP_OK : ESP_FAIL);
}
// ============================================================
@@ -767,79 +898,13 @@ esp_err_t can_mode_handler(httpd_req_t *req)
httpd_resp_send(req, resp, len);
return ESP_OK;
}
esp_err_t can_send_handler(httpd_req_t *req)
{
http_set_cors_headers(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)) {
tx_dlc = (uint8_t)parseHexToBytes(cJSON_GetStringValue(data_item), tx_data, CAN_MAX_DLC);
}
if (tx_dlc == 0 && dlc > 0) {
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
}
cJSON_Delete(root);
uint8_t resp_data[CAN_MAX_DLC] = {0};
uint8_t gd32_err = 0U;
int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc,
resp_data, sizeof(resp_data), 2000, &gd32_err);
char resp[512];
if (resp_len >= 0) {
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
bytesToHexUpper(resp_data, (size_t)resp_len, resp_hex);
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
} else {
snprintf(resp, sizeof(resp),
"{\"status\": \"error\", \"code\": %d, \"gd32_err\": %u}",
resp_len, (unsigned)gd32_err);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
/**
* @brief POST /can/send_batch — 同通道连续发送多帧,单次 HTTP 完成。
* @brief POST /can/send_batch — CAN 发送唯一入口(单帧 = frames 长度 1)。
*
* JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] }
* 返回: { "status":"ok", "results":[ ... ], 可选 skipped/abort_* }
* 帧间间隔 kInterFrameDelayMs;批量期间暂停 CAN 反馈任务。
* 单帧 canFdSend 超时 2000ms(等应答);多帧 80ms/帧 + 帧间隔。
* 发送期间暂停 CAN 反馈任务。
*/
esp_err_t can_send_batch_handler(httpd_req_t *req)
{
@@ -848,6 +913,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
constexpr int kMaxBody = 49152;
constexpr int kMaxFrames = 128;
constexpr uint32_t kPerFrameTimeoutMs = 80U;
constexpr uint32_t kSingleFrameTimeoutMs = 2000U;
constexpr uint32_t kInterFrameDelayMs = 5U;
int content_len = req->content_len;
@@ -893,6 +959,8 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
return http_send_json_error(req, "400 Bad Request", "bad_frames",
"frames 数量无效(1~128)");
}
const uint32_t frame_timeout_ms =
(frame_count == 1) ? kSingleFrameTimeoutMs : kPerFrameTimeoutMs;
cJSON *out_root = cJSON_CreateObject();
cJSON *results = cJSON_CreateArray();
@@ -905,7 +973,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON_AddStringToObject(out_root, "status", "ok");
cJSON_AddItemToObject(out_root, "results", results);
/* 批量发送期间暂停 CAN 反馈,独占 SPI 业务路径 */
/* 发送期间暂停 CAN 反馈,独占 SPI 业务路径 */
pauseCanFeedbackTask(true);
auto fill_err = [](cJSON *item, int code, uint8_t gd32_err) {
@@ -962,7 +1030,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
memset(resp_data, 0, sizeof(resp_data));
uint8_t gd32_err = 0U;
int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc,
resp_data, sizeof(resp_data), kPerFrameTimeoutMs, &gd32_err);
resp_data, sizeof(resp_data), frame_timeout_ms, &gd32_err);
if (resp_len >= 0) {
char resp_hex[CAN_MAX_DLC * 2 + 1];
@@ -1073,8 +1141,9 @@ esp_err_t spi_send_handler(httpd_req_t *req)
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"type\": \"%02X\", \"payload_len\": %zu, \"resp_type\": \"%02X\", \"resp_len\": %zu, \"recv\": \"%s\"}",
frame_type, payload_len, resp_type, resp_len, resp_hex);
DEBUG_INFO("SPI", "WEB SPI 响应: type=0x%02X len=%zu payload=%s",
resp_type, resp_len, resp_hex);
/* 日志只记 type/len,完整 payload 仅在 HTTP JSON 返回给前端 */
DEBUG_INFO("SPI", "WEB SPI ok: type=0x%02X len=%zu",
resp_type, resp_len);
} else {
snprintf(resp, sizeof(resp),
"{\"status\": \"error\", \"code\": %d, \"message\": \"%s\"}",
@@ -1088,25 +1157,27 @@ esp_err_t spi_send_handler(httpd_req_t *req)
}
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
/* 含 boot 模块列表与 CPU 任务;OTA 后 largest free 常偏紧,仍用静态缓冲避免 calloc */
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 7168;
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
static SemaphoreHandle_t s_status_json_mux = nullptr;
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
static SemaphoreHandle_t statusJsonMutex(void)
/** 发送当前工作区并清空 offset(分块;失败返回 false) */
static bool status_flush_chunk(httpd_req_t *req, char *buf, size_t cap, int *len)
{
if (s_status_json_mux == nullptr) {
portENTER_CRITICAL(&s_status_json_init_mux);
if (s_status_json_mux == nullptr) {
s_status_json_mux = xSemaphoreCreateMutex();
}
portEXIT_CRITICAL(&s_status_json_init_mux);
if (!req || !buf || !len || *len <= 0) {
return true;
}
return s_status_json_mux;
if (httpd_resp_send_chunk(req, buf, *len) != ESP_OK) {
return false;
}
*len = 0;
if (cap > 0) {
buf[0] = '\0';
}
return true;
}
/* CPU 快照静态缓冲:替代每秒 calloc(1.2KB),消除 IDLE 期持续的堆抖动。
* 与 s_http_json_work 同由 httpJsonWorkMutex 串行化。 */
static CpuUsageSnapshot s_cpu_snapshot;
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
{
if (handle == NULL) return 0;
@@ -1136,9 +1207,10 @@ static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...)
return offset + (int)written;
}
/** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样) */
/** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样)。最多 16 条任务以免撑爆 3KB 分块。 */
static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu)
{
static constexpr size_t kCpuTaskJsonMax = 16;
offset = append_jsonf(buf, cap, offset,
"\"cpu\":{\"available\":%s,\"valid\":%s,\"usage_pct\":%.2f,\"idle_pct\":%.2f,\"sample_ms\":%lu,\"tasks\":[",
cpu.available ? "true" : "false",
@@ -1147,7 +1219,9 @@ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUs
cpu.valid ? cpu.idle_pct : 0.0,
(unsigned long)(cpu.valid ? cpu.sample_ms : 0U));
for (size_t i = 0; i < cpu.task_count; ++i) {
const size_t ntask =
(cpu.task_count > kCpuTaskJsonMax) ? kCpuTaskJsonMax : cpu.task_count;
for (size_t i = 0; i < ntask; ++i) {
const CpuTaskMetric& task = cpu.tasks[i];
offset = append_jsonf(buf, cap, offset,
"%s{\"name\":\"%s\",\"task_number\":%lu,\"runtime_delta\":%" PRIu64 ",\"usage_pct\":%.2f}",
@@ -1166,6 +1240,20 @@ esp_err_t system_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
/* layout=1:附带链接期静态布局(IRAM/.data/.bss/堆容量等),前端缓存后轮询不再要。
* 默认只下发 memory 动态字段,减 status JSON 体积。 */
bool want_mem_layout = false;
{
char query[48];
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
char val[8] = {};
if (httpd_query_key_value(query, "layout", val, sizeof(val)) == ESP_OK) {
want_mem_layout = (val[0] == '1' || val[0] == 'y' || val[0] == 'Y' ||
val[0] == 't' || val[0] == 'T');
}
}
}
/* 堆极紧时仍返回精简 JSON,禁止 500(前端轮询会刷红)。 */
const size_t largest_now = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
if (largest_now < 512U) {
@@ -1184,10 +1272,10 @@ esp_err_t system_status_handler(httpd_req_t *req)
return ESP_OK;
}
SemaphoreHandle_t mux = statusJsonMutex();
SemaphoreHandle_t mux = httpJsonWorkMutex();
if (mux) {
if (xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
/* 并发 status 冲突:仍回简包,避免 200 */
/* 与 feedback/heap_low 争用工作区:仍回简包,避免 500 刷红 */
char busy[160];
snprintf(busy, sizeof(busy),
"{\"version\":\"%s\",\"ota\":{\"sys_state\":\"busy\"},"
@@ -1229,10 +1317,58 @@ esp_err_t system_status_handler(httpd_req_t *req)
size_t internal_largest_free_block = heap_caps_get_largest_free_block(INTERNAL_HEAP_CAPS);
size_t internal_heap_total = heap_caps_get_total_size(INTERNAL_HEAP_CAPS);
size_t default_heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
/* 内存全景:堆只是 SRAM 中可 malloc 的部分。
* 静态段用链接符号拆分(与 map 文件一致):
* [IRAM code | .data | .bss | heap ...] (C3 上 IRAM/DRAM 同一物理 SRAM)
* 手册 400KB = DRAM 窗口 + 约 16KB 指令 cache(cache 不在 SOC_DRAM_* 窗口内)。 */
multi_heap_info_t internal_info = {};
heap_caps_get_info(&internal_info, INTERNAL_HEAP_CAPS);
extern char _iram_start[];
extern char _iram_end[];
extern char _data_start[];
extern char _data_end[];
extern char _bss_start[];
extern char _bss_end[];
extern char _heap_start[];
auto sym_span = [](const char *a, const char *b) -> size_t {
const uintptr_t ua = reinterpret_cast<uintptr_t>(a);
const uintptr_t ub = reinterpret_cast<uintptr_t>(b);
return (ub >= ua) ? static_cast<size_t>(ub - ua) : 0U;
};
const size_t iram_bytes = sym_span(_iram_start, _iram_end);
const size_t data_bytes = sym_span(_data_start, _data_end);
const size_t bss_bytes = sym_span(_bss_start, _bss_end);
/* C3 DIRAM:DRAM 基址起至 _heap_start 即为 IRAM+.data+.bss 占位 */
const uintptr_t heap_start_addr = reinterpret_cast<uintptr_t>(_heap_start);
const size_t static_to_heap =
(heap_start_addr >= static_cast<uintptr_t>(SOC_DRAM_LOW))
? static_cast<size_t>(heap_start_addr - static_cast<uintptr_t>(SOC_DRAM_LOW))
: (iram_bytes + data_bytes + bss_bytes);
const size_t sram_soc =
(SOC_DRAM_HIGH > SOC_DRAM_LOW) ? (size_t)(SOC_DRAM_HIGH - SOC_DRAM_LOW) : 0U;
/* 应用实际接管:静态链接区 + 内部堆(比整窗更贴近“可用 DRAM”) */
const size_t sram_app = static_to_heap + internal_heap_total;
const size_t sram_total = (sram_app > 0) ? sram_app : sram_soc;
const size_t internal_heap_used = internal_info.total_allocated_bytes;
const size_t static_reserved = static_to_heap;
const size_t reserved_tail =
(sram_soc > sram_app) ? (sram_soc - sram_app) : 0U;
const size_t dma_total = heap_caps_get_total_size(MALLOC_CAP_DMA);
const size_t dma_free = heap_caps_get_free_size(MALLOC_CAP_DMA);
const size_t rtc_total = heap_caps_get_total_size(MALLOC_CAP_RTCRAM);
const size_t rtc_free = heap_caps_get_free_size(MALLOC_CAP_RTCRAM);
// 任务栈指标复用 health_monitor 共享的任务表(collectTaskStackMetrics),
// 避免与巡检逻辑两地维护;http_server 在本 handler 上下文中,句柄即当前任务。
TaskStackMetric task_metrics[12];
size_t task_metrics_count = collectTaskStackMetrics(task_metrics, 12);
size_t task_stack_total_bytes = 0;
for (size_t i = 0; i < task_metrics_count; ++i) {
task_stack_total_bytes += task_metrics[i].stack_size_bytes;
}
// http_server 行的句柄在 httpd 线程中取当前任务更准确
for (size_t i = 0; i < task_metrics_count; ++i) {
if (strcmp(task_metrics[i].name, "http_server") == 0) {
@@ -1241,28 +1377,22 @@ esp_err_t system_status_handler(httpd_req_t *req)
}
/* CPU:只读 cpu_stats 任务的 1Hz 快照,禁止在 httpd 里 uxTaskGetSystemState */
CpuUsageSnapshot cpu_local = {};
CpuUsageSnapshot *cpu = &cpu_local;
CpuUsageSnapshot *cpu_heap = nullptr;
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 512U)) {
cpu_heap = (CpuUsageSnapshot *)calloc(1, sizeof(CpuUsageSnapshot));
if (cpu_heap) {
cpu = cpu_heap;
}
}
CpuUsageSnapshot *cpu = &s_cpu_snapshot; // 用静态缓冲,不再每秒 calloc(1.2KB)
CpuStats::instance().getSnapshot(*cpu);
char *resp = s_status_json;
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
char *resp = s_http_json_work;
memset(resp, 0, HTTP_JSON_WORK_CAP);
httpd_resp_set_type(req, "application/json");
int len = snprintf(resp, SYSTEM_STATUS_JSON_CAP,
/* ── 分块 1:核心状态 + OTA/WiFi ── */
int len = snprintf(resp, HTTP_JSON_WORK_CAP,
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, "
"\"free_heap\": %lu, \"min_free_heap\": %lu, \"largest_free_block\": %lu, "
"\"internal_free_heap\": %lu, \"internal_min_free_heap\": %lu, \"internal_largest_free_block\": %lu, "
"\"internal_heap_total\": %lu, \"default_heap_total\": %lu, "
"\"temperature\": %.1f, \"uptime_sec\": %lu, "
"\"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, "
"\"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"channel\": %d, \"channel_name\": \"%s\", \"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, "
"\"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"channel\": %d, \"channel_name\": \"%s\", "
"\"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, "
"\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}, ",
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
@@ -1284,9 +1414,6 @@ esp_err_t system_status_handler(httpd_req_t *req)
(int)snap.sys_state,
(int)snap.channel,
snap.channel == OTA_CHANNEL_BLE ? "BLE" : "WIFI",
(unsigned long)snap.meta.total_size,
(unsigned long)snap.meta.stored_size,
snap.storage_done ? "true" : "false",
snap.relay.state,
(unsigned long)snap.relay.seq,
(unsigned long)snap.relay.total_packets,
@@ -1295,11 +1422,30 @@ esp_err_t system_status_handler(httpd_req_t *req)
snap.relay.version.app_patch, snap.relay.version.app_build,
snap.relay.version.boot_major, snap.relay.version.boot_minor,
snap.relay.version.boot_patch, snap.relay.version.boot_build);
if (len < 0 || (size_t)len >= HTTP_JSON_WORK_CAP) {
if (mux) {
xSemaphoreGive(mux);
}
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "status hdr");
return ESP_FAIL;
}
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
if (mux) {
xSemaphoreGive(mux);
}
return ESP_FAIL;
}
len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, *cpu);
/* ── 分块 2:CPU ── */
len = append_cpu_status_json(resp, HTTP_JSON_WORK_CAP, 0, *cpu);
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
if (mux) {
xSemaphoreGive(mux);
}
return ESP_FAIL;
}
/* SPI Link v2 统计(SPEC §12.1)— linkStatus 内部用 5ms 有界超时取 SPI 锁,
* OTA 中继期间拿不到锁时返回零值统计,绝不阻塞 /system/status 响应。 */
/* ── 分块 3:SPI / ADC / features / boot / memory ── */
{
SpiMaster::SpiLinkStatus ls = SpiMaster::instance().linkStatus();
const char *role_str = "unknown";
@@ -1308,7 +1454,7 @@ esp_err_t system_status_handler(httpd_req_t *req)
case 2: role_str = "boot"; break;
default: break;
}
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, 0,
",\"spi_link\":{\"version\":%u,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\","
"\"v2_active\":%s,\"local_credit\":%u,\"remote_credit\":%u,"
"\"crc_errors\":%lu,\"duplicate_requests\":%lu,\"seq_errors\":%lu,"
@@ -1330,14 +1476,114 @@ esp_err_t system_status_handler(httpd_req_t *req)
(unsigned long)ls.stats.max_resp_latency_us);
}
/* 启动自检位图(与串口 BOOT report 同源) */
len = SystemBoot::appendStatusJson(resp, SYSTEM_STATUS_JSON_CAP, len);
{
gd32_adc_feedback_t ga = {};
const bool have = getLatestGd32AdcFeedback(&ga);
const uint32_t age = gd32AdcFeedbackAgeMs();
const bool fresh = have && age != UINT32_MAX && age < 5000U;
const bool feat_known = SpiMaster::instance().gd32FeatKnown();
const bool feat_adc = SpiMaster::instance().gd32FeatAdcFeedback();
const char *en = feat_known ? (feat_adc ? "true" : "false")
: (have ? "true" : "null");
if (have) {
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len,
",\"gd32_adc\":{\"enabled\":%s,\"feat_known\":%s,\"fresh\":%s,\"valid\":%s,"
"\"temperature\":%.2f,\"vdda\":%.3f,\"vin_adc\":%.3f,\"vcc\":%.3f,"
"\"temp_raw\":%u,\"vref_raw\":%u,\"seq\":%lu,"
"\"uptime_sec\":%lu,\"age_ms\":%lu}",
en, feat_known ? "true" : "false",
fresh ? "true" : "false", ga.valid ? "true" : "false",
(double)ga.temperature_c, (double)ga.vdda_v, (double)ga.vin_adc_v,
(double)ga.vcc_v,
(unsigned)ga.temp_raw, (unsigned)ga.vref_raw,
(unsigned long)ga.seq, (unsigned long)ga.uptime_sec,
(unsigned long)(age == UINT32_MAX ? 0UL : (unsigned long)age));
} else {
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len,
",\"gd32_adc\":{\"enabled\":%s,\"feat_known\":%s,\"fresh\":false,\"valid\":false}",
en, feat_known ? "true" : "false");
}
const uint32_t fl = SpiMaster::instance().gd32FeatFlags();
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len,
",\"gd32_features\":{\"known\":%s,\"flags\":%lu,"
"\"adc_feedback\":%s,\"uart_temp_print\":%s,"
"\"spi_app_log\":%s,\"can_brs\":%s,"
"\"can_ch0\":%s,\"can_ch1\":%s,\"can_ch2\":%s,\"can_ch_mask\":%u}",
feat_known ? "true" : "false",
(unsigned long)fl,
(feat_known && (fl & V2_FEAT_ADC_FEEDBACK)) ? "true" : "false",
(feat_known && (fl & V2_FEAT_UART_TEMP_PRINT)) ? "true" : "false",
(feat_known && (fl & V2_FEAT_SPI_APP_LOG)) ? "true" : "false",
(feat_known && (fl & V2_FEAT_CAN_BRS)) ? "true" : "false",
(feat_known && (fl & V2_FEAT_CAN_CH0)) ? "true" : "false",
(feat_known && (fl & V2_FEAT_CAN_CH1)) ? "true" : "false",
(feat_known && (fl & V2_FEAT_CAN_CH2)) ? "true" : "false",
feat_known ? (unsigned)((fl & V2_FEAT_CAN_CH_MASK) >> V2_FEAT_CAN_CH_SHIFT) : 7U);
}
// 前导逗号与前一块分隔;之前因 append_cpu_status_json 尾随逗号叠加
// 形成 "],," 导致前端 JSON.parse 报 "Expected double-quoted property name"
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, ",\"task_stack\": {");
len = SystemBoot::appendStatusJson(resp, HTTP_JSON_WORK_CAP, len);
/* memory 动态字段:每次轮询;layout 仅 ?layout=1 时附带(链接期常量)。 */
if (want_mem_layout) {
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len,
",\"memory\":{\"layout\":{\"chip\":\"esp32c3\",\"sram_total\":%lu,\"sram_soc\":%lu,"
"\"static_reserved\":%lu,\"reserved_tail\":%lu,"
"\"iram\":%lu,\"data\":%lu,\"bss\":%lu,"
"\"heap_total\":%lu,\"dma_total\":%lu,\"rtc_total\":%lu,"
"\"task_stack_total\":%lu,\"default_heap_total\":%lu},"
"\"heap_used\":%lu,\"heap_free\":%lu,"
"\"heap_min_free\":%lu,\"heap_largest_free\":%lu,"
"\"alloc_blocks\":%lu,\"free_blocks\":%lu,"
"\"dma_free\":%lu,\"rtc_free\":%lu,\"default_heap_free\":%lu}",
(unsigned long)sram_total,
(unsigned long)sram_soc,
(unsigned long)static_reserved,
(unsigned long)reserved_tail,
(unsigned long)iram_bytes,
(unsigned long)data_bytes,
(unsigned long)bss_bytes,
(unsigned long)internal_heap_total,
(unsigned long)dma_total,
(unsigned long)rtc_total,
(unsigned long)task_stack_total_bytes,
(unsigned long)default_heap_total,
(unsigned long)internal_heap_used,
(unsigned long)internal_info.total_free_bytes,
(unsigned long)internal_info.minimum_free_bytes,
(unsigned long)internal_info.largest_free_block,
(unsigned long)internal_info.allocated_blocks,
(unsigned long)internal_info.free_blocks,
(unsigned long)dma_free,
(unsigned long)rtc_free,
(unsigned long)free_heap);
} else {
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len,
",\"memory\":{\"heap_used\":%lu,\"heap_free\":%lu,"
"\"heap_min_free\":%lu,\"heap_largest_free\":%lu,"
"\"alloc_blocks\":%lu,\"free_blocks\":%lu,"
"\"dma_free\":%lu,\"rtc_free\":%lu,\"default_heap_free\":%lu}",
(unsigned long)internal_heap_used,
(unsigned long)internal_info.total_free_bytes,
(unsigned long)internal_info.minimum_free_bytes,
(unsigned long)internal_info.largest_free_block,
(unsigned long)internal_info.allocated_blocks,
(unsigned long)internal_info.free_blocks,
(unsigned long)dma_free,
(unsigned long)rtc_free,
(unsigned long)free_heap);
}
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
if (mux) {
xSemaphoreGive(mux);
}
return ESP_FAIL;
}
/* ── 分块 4:task_stack + 闭合 ── */
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, 0, ",\"task_stack\": {");
for (size_t i = 0; i < task_metrics_count; ++i) {
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len,
"%s\"%s\":{\"stack_size_bytes\":%lu,\"high_watermark_bytes\":%lu,\"peak_used_bytes\":%lu}",
i == 0 ? "" : ",",
task_metrics[i].name,
@@ -1345,13 +1591,17 @@ esp_err_t system_status_handler(httpd_req_t *req)
(unsigned long)get_task_stack_high_watermark_bytes(task_metrics[i].handle),
(unsigned long)get_task_stack_peak_used_bytes(task_metrics[i].handle, task_metrics[i].stack_size_bytes));
}
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, "}}");
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, "}}");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len > 0 ? len : 0);
if (cpu_heap) {
free(cpu_heap);
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
if (mux) {
xSemaphoreGive(mux);
}
return ESP_FAIL;
}
/* 结束 chunked body */
(void)httpd_resp_send_chunk(req, NULL, 0);
if (mux) {
xSemaphoreGive(mux);
}
@@ -1386,3 +1636,41 @@ esp_err_t system_probe_handler(httpd_req_t *req)
httpd_resp_send(req, resp, len);
return ESP_OK;
}
// ============================================================
// GET /system/heap_low_log — 设备唯一快照槽(0 或 1 条)
// ============================================================
/**
* @brief 返回设备侧最新一条"最低空闲创新低"上下文。
* 多条历史由网页 IndexedDB 累积;JSON 复用 s_http_json_work(无独立 .bss)。
*/
esp_err_t system_heap_low_log_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
SemaphoreHandle_t mux = httpJsonWorkMutex();
if (mux && xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, "{\"records\":[]}", HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
char *resp = s_http_json_work;
int len = HealthMonitor::instance().dumpHeapLowWaterJson(resp, HTTP_JSON_WORK_CAP, 0);
if (len < 0) {
len = 0;
}
if ((size_t)len >= HTTP_JSON_WORK_CAP) {
len = (int)HTTP_JSON_WORK_CAP - 1;
}
resp[len] = '\0';
httpd_resp_set_type(req, "application/json");
esp_err_t err = httpd_resp_send(req, resp, len);
if (mux) {
xSemaphoreGive(mux);
}
return err;
}
+4 -2
View File
@@ -15,13 +15,15 @@ esp_err_t restart_handler(httpd_req_t *req);
// Utility handlers
esp_err_t logs_handler(httpd_req_t *req);
esp_err_t can_send_handler(httpd_req_t *req);
/** @brief 批量 CAN 发送:一次 HTTP 连续发多帧,减少帧间 HTTP 往返延迟。 */
/** @brief CAN 发送(唯一入口):同通道 1~N 帧,单帧即 frames 长度为 1。 */
esp_err_t can_send_batch_handler(httpd_req_t *req);
esp_err_t spi_send_handler(httpd_req_t *req);
esp_err_t system_status_handler(httpd_req_t *req);
esp_err_t system_probe_handler(httpd_req_t *req);
/** @brief GET /system/heap_low_log — 返回设备侧最新 1 条创新低上下文。 */
esp_err_t system_heap_low_log_handler(httpd_req_t *req);
// CAN 反馈快照(被动 0xC1)与接收模式
esp_err_t can_feedback_handler(httpd_req_t *req);
esp_err_t can_mode_handler(httpd_req_t *req);
+56 -27
View File
@@ -397,9 +397,26 @@ esp_err_t WifiOta::httpServerStart()
config.server_port = HTTP_SERVER_PORT;
config.stack_size = HTTP_SERVER_STACK_SIZE;
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
/* LWIP_MAX_SOCKETS=12;HTTP 内部约占 3 + UDP 日志 1;业务连接上限 7 */
config.max_open_sockets = 7;
/* 满连接时淘汰最久未用的空闲 socket */
/* Socket 预算(无法“合并” httpd 内部 3 个 fd:listen + ctrl 对):
*
* LWIP 总池 CONFIG_LWIP_MAX_SOCKETS
* ├─ httpd 内部固定 3
* ├─ max_open_sockets = 业务并发(下面 want)
* └─ 余量:UDP 调试日志 1 + 偶发其它
*
* 约束:max_open_sockets <= LWIP_MAX_SOCKETS - 3
*
* 业务并发压到 4(tools 单浏览器已足够):
* 1× /system/status 轮询 + 1× /logs 轮询 + 1× 操作(OTA/CAN) + 1× 余量
* OTA 大上传期间前端 pausePolling,只占 1 条长连接,不与轮询抢槽。
* lru_purge:满槽时踢最久空闲连接,避免 EMFILE 卡死。
* 硬编码 7 在 LWIP=8 时会 ESP_ERR_INVALID_ARG → HTTP 起不来。 */
{
const int lwip_cap = CONFIG_LWIP_MAX_SOCKETS - 3; // httpd 硬上限
const int want = 4; // 合并并发需求后的业务槽位(原 7 偏松)
config.max_open_sockets = (lwip_cap < want) ? ((lwip_cap > 1) ? lwip_cap : 1) : want;
}
/* 满连接时淘汰最久未用的空闲 socket(相当于“复用/腾挪”槽位) */
config.lru_purge_enable = true;
/* idle 收发超时(秒);进行中的 handler 连接不受此 idle 检测踢掉 */
config.recv_wait_timeout = 10;
@@ -437,7 +454,7 @@ esp_err_t WifiOta::httpServerStart()
{"/logs", HTTP_GET, logs_handler},
{"/system/status", HTTP_GET, system_status_handler},
{"/system/probe", HTTP_GET, system_probe_handler},
{"/can/send", HTTP_POST, can_send_handler},
{"/system/heap_low_log", HTTP_GET, system_heap_low_log_handler},
{"/can/send_batch", HTTP_POST, can_send_batch_handler},
{"/can/feedback", HTTP_GET, can_feedback_handler},
{"/can/mode", HTTP_GET, can_mode_handler},
@@ -445,7 +462,10 @@ esp_err_t WifiOta::httpServerStart()
{"/spi/send", HTTP_POST, spi_send_handler},
};
for (const auto &r : kHttpRoutes) {
/* 同一 path 可有多 method(如 GET+POST);OPTIONS 每个 path 只注册一次,
* 否则 httpd 报 “handler ... method 6 already registered”。 */
for (size_t i = 0; i < sizeof(kHttpRoutes) / sizeof(kHttpRoutes[0]); ++i) {
const UriEntry &r = kHttpRoutes[i];
httpd_uri_t main_uri = {};
main_uri.uri = r.uri;
main_uri.method = r.method;
@@ -453,15 +473,24 @@ esp_err_t WifiOta::httpServerStart()
main_uri.user_ctx = NULL;
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &main_uri);
httpd_uri_t opts_uri = {};
opts_uri.uri = r.uri;
opts_uri.method = HTTP_OPTIONS;
opts_uri.handler = http_options_handler;
opts_uri.user_ctx = NULL;
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
bool need_opts = true;
for (size_t j = 0; j < i; ++j) {
if (strcmp(kHttpRoutes[j].uri, r.uri) == 0) {
need_opts = false;
break;
}
}
if (need_opts) {
httpd_uri_t opts_uri = {};
opts_uri.uri = r.uri;
opts_uri.method = HTTP_OPTIONS;
opts_uri.handler = http_options_handler;
opts_uri.user_ctx = NULL;
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
}
}
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /can/send_batch, /spi/send, /can/feedback, /can/mode");
DEBUG_INFO("WIFI", "HTTP端点已注册 (ota/can/spi/logs/status)");
return ESP_OK;
}
@@ -562,7 +591,7 @@ bool WifiOta::earlyInit()
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
} else {
if (nvs_erased) {
DEBUG_WARN("WIFI", "NVS 已擦除重建,WiFi PHY 校准数据已清除,将重新校准");
DEBUG_WARN("WIFI", "NVS重建,PHY校准已清,将重校");
}
DEBUG_INFO("WIFI", "NVS 初始化成功");
}
@@ -721,7 +750,7 @@ void WifiOta::taskEntry(void *pvParameters)
ota_meta_t boot_meta;
if (OtaCoordinator::instance().loadGd32Meta(&boot_meta) == ESP_OK &&
boot_meta.total_size > 0 && boot_meta.stored_size < boot_meta.total_size) {
DEBUG_WARN("WIFI", "检测到不完整 OTA 缓存 (stored=%u total=%u),清除残留状态",
DEBUG_WARN("WIFI", "不完整OTA缓存 stored=%u total=%u,清残留",
(unsigned)boot_meta.stored_size, (unsigned)boot_meta.total_size);
OtaCoordinator::instance().clearGd32Meta();
OtaCoordinator::instance().abortCurrent();
@@ -742,21 +771,21 @@ void WifiOta::taskEntry(void *pvParameters)
// 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
vTaskDelay(pdMS_TO_TICKS(500));
// 启动摘要,发两遍确保上位机收到(第一包可能触发 ARP 被丢)
// 启动摘要(单次;前面已 delay 等网络/ARP 稳定)
esp_chip_info_t chip;
esp_chip_info(&chip);
for (int i = 0; i < 2; i++) {
DEBUG_INFO("WIFI", "========================================");
DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", WifiOta::getMode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
DEBUG_INFO("WIFI", "========================================");
DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
}
DEBUG_INFO("WIFI", "========================================");
DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", WifiOta::getMode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
DEBUG_INFO("WIFI", "========================================");
DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
/* 启动日志 burst 已过:环 64→16 槽,释放大约 7.5KB 给业务堆 */
DebugLog::instance().shrinkRingAfterBoot();
// WiFi 状态监控
// 每秒一次循环:每 10 秒检查 RSSI
+17 -11
View File
@@ -139,8 +139,8 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
size_t aligned_len = (total + 3) & ~(size_t)3;
if (aligned_offset + aligned_len > part->size) {
DEBUG_ERR("OTA", "flashWriteChunk越界: offset=%" PRIu32 " len=%zu aligned_off=%" PRIu32 " aligned_len=%zu part=%" PRIu32,
offset, len, aligned_offset, aligned_len, part->size);
DEBUG_ERR("OTA", "flashWrite越界 off=%" PRIu32 " n=%zu part=%" PRIu32,
offset, len, part->size);
return ESP_ERR_INVALID_SIZE;
}
@@ -156,8 +156,8 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
}
if (head_pad > 0 || len % 4 != 0) {
DEBUG_WARN("OTA", "flashWriteChunk 非对齐: offset=%" PRIu32 " len=%zu head=%zu aligned_len=%zu",
offset, len, head_pad, aligned_len);
DEBUG_WARN("OTA", "flashWrite非对齐 off=%" PRIu32 " n=%zu head=%zu",
offset, len, head_pad);
}
uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len);
@@ -485,7 +485,7 @@ esp_err_t Gd32FlashStore::metaVerifyChecksum()
// 若直接返回 OK 跳过校验,meta 被意外清零时会掩盖固件损坏。
static const uint8_t kZeroSha[32] = {0};
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,返回 INVALID_STATE(调用方决定容忍或失败)");
DEBUG_WARN("OTA", "metaVerify: checksum未算→INVALID_STATE");
return ESP_ERR_INVALID_STATE;
}
@@ -517,14 +517,20 @@ esp_err_t Gd32FlashStore::verifyFrontendSha(const uint8_t frontend_sha32[32],
{
if (!frontend_sha32) return ESP_ERR_INVALID_ARG;
// 1) 计算设备端 SHA 并写入 meta.checksum
esp_err_t err = metaFinalizeChecksum();
/* 上传收尾 finishGd32StorageAfterReceive 通常已 metaFinalizeChecksum(并打过 SHA 日志)。
* relay 触发时若 checksum 已有,直接比对,避免整盘重算 + 再打一遍「固件 SHA-256」。 */
ota_meta_t meta;
esp_err_t err = metaLoad(&meta);
if (err != ESP_OK) return err;
// 2) 读回 meta 拿到 checksum
ota_meta_t meta;
err = metaLoad(&meta);
if (err != ESP_OK) return err;
static const uint8_t kZeroSha[32] = {0};
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
err = metaFinalizeChecksum();
if (err != ESP_OK) return err;
err = metaLoad(&meta);
if (err != ESP_OK) return err;
}
if (out_device_sha) memcpy(out_device_sha, meta.checksum, 32);
// 3) 比对前端 SHA 与设备端 SHA
+3 -3
View File
@@ -69,7 +69,7 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
static int s_last_spi_err = 0;
const int err_i = static_cast<int>(ret);
if (s_spi_fail_n == 0U || err_i != s_last_spi_err || (s_spi_fail_n % 32U) == 0U) {
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s (x%lu)",
DEBUG_WARN("CAN", "SEND SPI fail ch=%u id=%08lX %s x%lu",
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret),
(unsigned long)(s_spi_fail_n + 1U));
}
@@ -100,7 +100,7 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
static uint32_t s_rej_n = 0U;
static uint8_t s_last_rej = 0xFFU;
if (s_rej_n == 0U || gd32_err != s_last_rej || (s_rej_n % 32U) == 0U) {
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X (x%lu)",
DEBUG_WARN("CAN", "SEND拒 ch=%u id=%08lX r=%02X e=%02X x%lu",
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err,
(unsigned long)(s_rej_n + 1U));
}
@@ -280,7 +280,7 @@ int canFdSetRxMode(can_rx_mode_t mode,
return 0;
}
DEBUG_WARN("CAN", "SET_MODE 失败: want=%u spi=%s resp=0x%02X len=%u",
DEBUG_WARN("CAN", "SET_MODE fail want=%u %s r=0x%02X n=%u",
(unsigned)mode,
esp_err_to_name(ret),
(unsigned)resp_type,
+165 -32
View File
@@ -97,11 +97,19 @@ SpiMaster& SpiMaster::instance()
// SPI Worker:唯一持 SPI 的任务;ISR 只通知本任务
// ============================================================
/** @brief 当前任务是否为 SPI worker(仅 worker 可直接走无锁路径)。 */
bool SpiMaster::onWorker() const
{
return m_workerTask != nullptr && xTaskGetCurrentTaskHandle() == m_workerTask;
}
/**
* @brief 将 GD32 EVENT 帧入队(worker 上下文)。
* @param type 业务 TYPE(如 0xC1/0xC4)
* @param payload 载荷指针,可为 nullptr
* @param len 载荷长度
* @return 始终 true;队列满时丢最旧一帧再写入
*/
bool SpiMaster::pushEvent(uint8_t type, const uint8_t *payload, size_t len)
{
if (m_eventCount >= kEventSlots) {
@@ -126,6 +134,14 @@ bool SpiMaster::pushEvent(uint8_t type, const uint8_t *payload, size_t len)
return true;
}
/**
* @brief 从事件队列弹出一帧(非阻塞)。
* @param[out] type 业务 TYPE
* @param[out] payload 拷贝载荷;可为 nullptr
* @param max_len payload 缓冲容量
* @param[out] len 实际拷贝长度
* @return true 取到一帧;false 队列空
*/
bool SpiMaster::popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len)
{
if (m_eventCount == 0U) {
@@ -150,6 +166,10 @@ bool SpiMaster::popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t
return true;
}
/**
* @brief 暂停/恢复对 GD32 主动按铃的 serviceRing 处理。
* @param suppress true=OTA 等关键路径禁止偷读 PB6;false=恢复
*/
void SpiMaster::setSuppressRingService(bool suppress)
{
m_suppressRingService = suppress;
@@ -159,6 +179,10 @@ void SpiMaster::setSuppressRingService(bool suppress)
}
}
/**
* @brief 业务侧确认本端 TX 序号已被对端接受,推进 m_txSeq。
* @note 在收到明确业务 ACK 后调用,避免重发与序号失配。
*/
void SpiMaster::confirmTxSeqAccepted()
{
/* 业务 ACK 已确认:保证下一包使用 lastSentSeq+1,即使头 ack_seq 曾失配 */
@@ -167,6 +191,10 @@ void SpiMaster::confirmTxSeqAccepted()
m_lastRxAckMatched = true;
}
/**
* @brief worker 单次服务 GD32 按铃:读一帧并入 EVENT 队列。
* @return ESP_OK 入队成功;ESP_ERR_NOT_FOUND 无线/被抑制;其它为读失败
*/
esp_err_t SpiMaster::serviceRingOnce()
{
/* OTA 中继期间禁止按铃读:OTA ACK/WAIT 也拉低 PB6,偷读会导致 poll 空等。 */
@@ -230,6 +258,10 @@ esp_err_t SpiMaster::serviceRingOnce()
return ESP_OK;
}
/**
* @brief worker 执行一条排队 Job(Transact/Hello/Ping/EnterBoot 等)。
* @param job 入出参:写入 job.result
*/
void SpiMaster::executeJob(Job &job)
{
/* 已在 worker 上下文;下列 public API 见 onWorker() 后走实现体,不再二次入队。 */
@@ -263,6 +295,10 @@ void SpiMaster::executeJob(Job &job)
}
}
/**
* @brief worker 完成堆上 Job 后的收尾:释放完成信号量,超时放弃时由 worker free。
* @param job 堆分配的 Job 指针
*/
void SpiMaster::finishHeapJob(Job *job)
{
if (job == nullptr) {
@@ -288,6 +324,11 @@ void SpiMaster::finishHeapJob(Job *job)
}
}
/**
* @brief 将 Job 提交给 SPI worker 串行执行(非 worker 线程走队列)。
* @param job 任务描述(栈上 Job 会拷到堆上再排队)
* @return 业务结果码;队列满/超时返回 ESP_ERR_TIMEOUT
*/
esp_err_t SpiMaster::submitJob(Job &job)
{
if (m_jobQueue == nullptr || m_workerTask == nullptr || onWorker()) {
@@ -363,11 +404,16 @@ esp_err_t SpiMaster::submitJob(Job &job)
return ESP_ERR_TIMEOUT;
}
/** @brief FreeRTOS 任务入口,转到 workerLoop。 */
void SpiMaster::workerEntry(void *arg)
{
static_cast<SpiMaster *>(arg)->workerLoop();
}
/**
* @brief SPI worker 主循环:处理按铃 EVENT + 执行 job 队列。
* @note 全栈唯一允许直接访问 SPI 总线的任务上下文。
*/
void SpiMaster::workerLoop()
{
m_workerTask = xTaskGetCurrentTaskHandle();
@@ -490,7 +536,7 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
}
if ((tx_len > 0U && tx_buf == NULL) || (rx_len > 0U && rx_buf == NULL)) {
DEBUG_WARN("SPI", "transferClocked INVALID_ARG: tx_len=%u tx_buf=%p rx_len=%u rx_buf=%p dev=%p",
DEBUG_WARN("SPI", "xfer INVALID_ARG tx=%u rx=%u dev=%p",
(unsigned)tx_len, tx_buf, (unsigned)rx_len, rx_buf, m_spiDevice);
return ESP_ERR_INVALID_ARG;
}
@@ -556,7 +602,7 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
esp_err_to_name(ret), (unsigned)(total_len * 8U));
ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, pdMS_TO_TICKS(1000));
if (ret != ESP_OK) {
DEBUG_ERR("SPI", "spi_device_get_trans_result 放弃: %s(可能需复位 SPI)",
DEBUG_ERR("SPI", "trans_result放弃:%s 或复位SPI",
esp_err_to_name(ret));
/* 尽力排空 ret_queue,避免后续 queue_trans 永久堵死 */
spi_transaction_t *junk = nullptr;
@@ -690,7 +736,7 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
if (!is_event) {
static uint32_t s_sess_mm_logs = 0U;
if ((++s_sess_mm_logs % 32U) == 1U) {
DEBUG_WARN("SPI", "v2 SESSION mismatch: got=0x%08lX expected=0x%08lX type=0x%02X (log#%lu)",
DEBUG_WARN("SPI", "v2 SES mismatch got=%08lX exp=%08lX t=%02X #%lu",
(unsigned long)hdr.session_id, (unsigned long)self.m_sessionId,
hdr.type, (unsigned long)s_sess_mm_logs);
}
@@ -714,7 +760,7 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
if (!self.m_lastRxAckMatched) {
static uint32_t s_stale_logs = 0U;
if ((++s_stale_logs % 32U) == 1U) {
DEBUG_DBG("SPI", "v2 stale RESPONSE delivered: type=0x%02X ack_seq=%u expected=%u (log#%lu)",
DEBUG_DBG("SPI", "v2 stale rsp t=0x%02X ack=%u exp=%u #%lu",
hdr.type, hdr.ack_seq, self.m_lastSentSeq,
(unsigned long)s_stale_logs);
}
@@ -791,7 +837,7 @@ esp_err_t SpiMaster::init()
return ret;
}
DEBUG_INFO("SPI", "SPI2 总线初始化完成: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO%d, CLK=%d Hz",
DEBUG_INFO("SPI", "SPI2 ok CS=GPIO%d CLK=%dHz",
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
/* 配置 SPI Ready 握手引脚(GPIO10),v2 DATA_READY 模式:仅输入。
@@ -873,12 +919,20 @@ esp_err_t SpiMaster::init()
return ESP_OK;
}
/**
* @brief L0 本机自检:SPI 设备、互斥、worker、job 队列是否就绪。
* @return true 本机 SPI 栈可用
*/
bool SpiMaster::selfTestL0() const
{
return (m_spiDevice != nullptr) && (m_mutex != nullptr) &&
(m_workerTask != nullptr) && (m_jobQueue != nullptr);
}
/**
* @brief L0 对端轻检:短超时 HELLO,确认链路可达 GD32。
* @return true 至少一次 HELLO 成功
*/
bool SpiMaster::selfTestPeerL0()
{
/* 短超时、最多 2 次;不拉长整体启动 */
@@ -1415,7 +1469,7 @@ esp_err_t SpiMaster::appRequestBootloader()
} else if (hdr.type == V2_TYPE_LINK_ERROR) {
const uint8_t errc = (hdr.payload_len > 0U && pload) ? pload[0] : 0xFFU;
const uint8_t exp = (hdr.payload_len > 1U && pload) ? pload[1] : 0xFFU;
DEBUG_WARN("SPI", "ENTER_BOOT 被拒 LINK_ERROR code=0x%02X expected_seq=%u last_sent=%u",
DEBUG_WARN("SPI", "ENTER_BOOT拒 code=0x%02X exp=%u last=%u",
errc, exp, (unsigned)m_lastSentSeq);
break;
} else if (hdr.type == SPI_OTA_RSP_ERROR) {
@@ -1441,7 +1495,7 @@ esp_err_t SpiMaster::appRequestBootloader()
/* 静默:APP 擦配置页(可达数十 ms) + 软复位 + BOOT 起机。
* 此间禁止 HELLO,否则 role 会一直像卡在 APP。 */
const uint32_t kQuietMs = got_ack ? 100U : 30U;
DEBUG_INFO("SPI", "ENTER_BOOT 已上总线 (ack=%s),静默 %lums 等待 APP 复位...",
DEBUG_INFO("SPI", "ENTER_BOOT ok ack=%s waitAPP %lums",
got_ack ? "yes" : "no", (unsigned long)kQuietMs);
vTaskDelay(pdMS_TO_TICKS(kQuietMs));
return got_ack ? ESP_OK : ESP_ERR_INVALID_RESPONSE;
@@ -1515,17 +1569,92 @@ esp_err_t SpiMaster::waitForBootloaderReady(uint32_t timeout_ms)
vTaskDelay(miss_gap);
}
DEBUG_WARN("SPI", "Bootloader 就绪超时 (%lums, rounds=%lu, app_hits=%lu)",
DEBUG_WARN("SPI", "BOOT就绪超时 %lums r=%lu app=%lu",
(unsigned long)timeout_ms, (unsigned long)rounds, (unsigned long)app_hits);
return ESP_ERR_TIMEOUT;
}
/**
* @brief 解析 GD32 APP 身份 16B 并写入本端缓存。
*
* 布局与 HELLO_ACK 附加块 / APP PING body 相同(见 spi_link_v2.h V2_APP_IDENTITY_LEN):
* [0..3] APP ver · [4..7] BOOT ver · [8..11] uptime_ms LE
* [12] feat_map_ver · [13..14] feature_flags LE · [15] rsv
*
* @param id 身份块指针(至少 16 字节)
* @param out_version 可选,输出解析后的版本结构
* @param out_uptime_sec 可选,输出 uptime(秒)
*/
void SpiMaster::applyAppIdentity16(const uint8_t *id, gd32_version_t *out_version,
uint32_t *out_uptime_sec)
{
if (!id) {
return;
}
gd32_version_t ver{};
ver.app_major = id[0];
ver.app_minor = id[1];
ver.app_patch = id[2];
ver.app_build = id[3];
ver.boot_major = id[4];
ver.boot_minor = id[5];
ver.boot_patch = id[6];
ver.boot_build = id[7];
const uint32_t uptime_ms = (uint32_t)id[8]
| ((uint32_t)id[9] << 8)
| ((uint32_t)id[10] << 16)
| ((uint32_t)id[11] << 24);
if (out_uptime_sec) {
*out_uptime_sec = uptime_ms / 1000U;
}
uint16_t feat_flags = 0U;
bool feat_known = false;
if (id[12] == V2_FEAT_MAP_VER) {
feat_flags = v2_get_u16le(&id[13]);
feat_known = true;
}
/* HELLO 与 PING 都会带同一份 16B 身份:仅版本/feat 变化时打 INFO,避免连打两遍 */
const bool ver_changed =
!m_gd32IdKnown ||
m_gd32Version.app_major != ver.app_major ||
m_gd32Version.app_minor != ver.app_minor ||
m_gd32Version.app_patch != ver.app_patch ||
m_gd32Version.app_build != ver.app_build ||
m_gd32Version.boot_major != ver.boot_major ||
m_gd32Version.boot_minor != ver.boot_minor ||
m_gd32Version.boot_patch != ver.boot_patch ||
m_gd32Version.boot_build != ver.boot_build ||
m_gd32FeatKnown != feat_known ||
(feat_known && m_gd32FeatFlags != feat_flags);
m_gd32Version = ver;
m_gd32IdKnown = true;
if (out_version) {
*out_version = ver;
}
m_gd32FeatKnown = feat_known;
m_gd32FeatFlags = feat_known ? feat_flags : 0U;
if (ver_changed && feat_known) {
DEBUG_INFO("SPI", "ID APP=%u.%u.%u.%u feat=%04lX",
(unsigned)ver.app_major, (unsigned)ver.app_minor,
(unsigned)ver.app_patch, (unsigned)ver.app_build,
(unsigned long)m_gd32FeatFlags);
DEBUG_INFO("SPI", "ID BOOT=%u.%u.%u.%u ch=%u",
(unsigned)ver.boot_major, (unsigned)ver.boot_minor,
(unsigned)ver.boot_patch, (unsigned)ver.boot_build,
(unsigned)((m_gd32FeatFlags & V2_FEAT_CAN_CH_MASK) >> V2_FEAT_CAN_CH_SHIFT));
}
}
/**
* @brief 向 GD32 APP 发送存活探测(帧长发送 + DATA_READY 变长读)。
*
* 两步法(持锁):otaTransferUnlocked(expect_stale) + otaPollResponseUnlocked。
* GD32 App PING 响应 type=kRspAck(0x80),
* payload 16B = APP ver(4) + BOOT ver(4) + uptime_ms(4) + build_hash(4)。
* 成功时 body 为身份 16B,与 HELLO_ACK 附加块同布局,经 applyAppIdentity16 入库。
*/
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
{
@@ -1576,34 +1705,26 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver
}
const uint8_t *pload = resp_payload;
if (version) {
if (resp_len >= V2_APP_IDENTITY_LEN) {
applyAppIdentity16(pload, version, uptime_sec);
} else if (version) {
memset(version, 0, sizeof(gd32_version_t));
version->app_major = pload[0];
version->app_minor = pload[1];
version->app_patch = pload[2];
version->app_build = pload[3];
if (resp_len >= 16U) {
version->boot_major = pload[4];
version->boot_minor = pload[5];
version->boot_patch = pload[6];
version->boot_build = pload[7];
}
}
if (uptime_sec) {
const uint8_t *ut = (resp_len >= 16U) ? &pload[8] : &pload[4];
const uint32_t uptime_ms = (uint32_t)ut[0]
| ((uint32_t)ut[1] << 8)
| ((uint32_t)ut[2] << 16)
| ((uint32_t)ut[3] << 24);
*uptime_sec = uptime_ms / 1000U;
}
DEBUG_DBG("SPI", "APP PING v2 成功");
DEBUG_DBG("SPI", "APP PING 成功 len=%u", (unsigned)resp_len);
return ESP_OK;
}
// ============================================================
// SPI Link v2 链路状态
// ============================================================
/**
* @brief 拷贝链路统计快照(有界取锁,失败返回零值)。
* @return 统计结构副本
*/
SpiMaster::SpiLinkStats SpiMaster::linkStats()
{
/* OTA 中继期间 SPI worker 在 otaSendAndPoll 全程持有 m_mutex(发送+轮询 ACK,
@@ -1618,6 +1739,10 @@ SpiMaster::SpiLinkStats SpiMaster::linkStats()
return ret;
}
/**
* @brief 完整链路状态(session/role/credit + 统计),供 /system/status。
* @return 状态快照
*/
SpiMaster::SpiLinkStatus SpiMaster::linkStatus()
{
/* 状态字段为 volatile 单字节/双字读,原子性足够;统计复用 linkStats() 的有界取锁。 */
@@ -1631,6 +1756,12 @@ SpiMaster::SpiLinkStatus SpiMaster::linkStatus()
return st;
}
/**
* @brief 发起 HELLO 建 session;APP 侧 ACK 可附带身份 16B(版本+开关)。
* @param timeout_ms 总超时
* @param role_hint 0=any,1=app,2=boot
* @return ESP_OK 成功;超时/坏响应见错误码
*/
esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
{
if (!onWorker() && m_workerTask != nullptr) {
@@ -1729,20 +1860,22 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
/* payload[5]/hdr.credit 均为对端 credit_max → 写入 m_remoteCredit。
* 勿写入 m_localCredit(本端 RX 窗口,init 为 8)。 */
m_remoteCredit = (pload[5] != 0U) ? pload[5] : hdr.credit;
/* APP:HELLO_ACK 带 16B 身份 → 建连即完成开机身份(与 PING 同布局) */
if (m_gd32Role == V2_ROLE_APP &&
hdr.payload_len >= V2_HELLO_ACK_APP_LEN) {
applyAppIdentity16(&pload[V2_HELLO_ACK_BASE_LEN], nullptr, nullptr);
}
m_v2Active = true;
m_stats.resyncs++;
return ESP_OK;
}
if (!saw_any_frame) {
/* 不打全包 hex:只记 attempts + 前 4 字节便于判 SOF */
DEBUG_WARN("SPI",
"HELLO: no valid v2 frame in response (attempts=%lu) rx[0..15]="
"%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
"HELLO no v2 frame att=%lu rx0-3=%02X %02X %02X %02X",
(unsigned long)attempt,
last_rx_head[0], last_rx_head[1], last_rx_head[2], last_rx_head[3],
last_rx_head[4], last_rx_head[5], last_rx_head[6], last_rx_head[7],
last_rx_head[8], last_rx_head[9], last_rx_head[10], last_rx_head[11],
last_rx_head[12], last_rx_head[13], last_rx_head[14], last_rx_head[15]);
last_rx_head[0], last_rx_head[1], last_rx_head[2], last_rx_head[3]);
} else {
DEBUG_WARN("SPI", "HELLO: bad/lag response type=0x%02X flags=0x%02X attempts=%lu",
last_type, last_flags, (unsigned long)attempt);
+41
View File
@@ -99,6 +99,10 @@ public:
/** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */
TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; }
/** @brief SPI 任务队列当前待处理 job 数(每个堆 Job 持有 calloc 的 Job 结构)。
* 供 /system/heap_low_log 堆低水位快照采样,定位"低谷是不是 SPI 队列堆积引起"。 */
UBaseType_t getPendingJobCount() const { return m_jobQueue ? uxQueueMessagesWaiting(m_jobQueue) : 0; }
/**
* @brief OTA 中继期间禁止 worker 将 DATA_READY 当 EVENT 读取。
* 中继开始设 true,结束/复位设 false。
@@ -160,6 +164,26 @@ public:
/** @brief 最近一次 HELLO 得到的 GD32 角色(0=unknown, 1=app, 2=boot)。 */
uint8_t gd32Role() const { return m_gd32Role; }
/**
* @brief 是否已从 HELLO_ACK/PING 身份块拿到功能位图。
* @note false=尚未建连身份或旧固件。
*/
bool gd32FeatKnown() const { return m_gd32FeatKnown; }
/** @brief GD32 功能位图(V2_FEAT_*);未知时为 0。 */
uint32_t gd32FeatFlags() const { return m_gd32FeatFlags; }
/** @brief 便捷:GD32 是否开启 ADC 0xC4(需 gd32FeatKnown)。 */
bool gd32FeatAdcFeedback() const {
return m_gd32FeatKnown && (m_gd32FeatFlags & V2_FEAT_ADC_FEEDBACK) != 0U;
}
/** @brief 是否已从 HELLO_ACK/PING 拿到 APP/BOOT 版本。 */
bool gd32IdKnown() const { return m_gd32IdKnown; }
/** @brief 最近身份块中的版本(HELLO 或 PING)。 */
gd32_version_t gd32VersionCached() const { return m_gd32Version; }
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
SpiLinkStats linkStats();
@@ -315,11 +339,24 @@ private:
void executeJob(Job &job);
/** @brief worker 完成堆 Job 后的生命周期收尾(give done + 可能 free)。 */
void finishHeapJob(Job *job);
/** @brief FreeRTOS 任务入口,转到 workerLoop。 */
static void workerEntry(void *arg);
/** @brief SPI worker 主循环:EVENT 按铃 + Job 队列。 */
void workerLoop();
/** @brief 单次服务 GD32 按铃读帧并入 EVENT 队列。 */
esp_err_t serviceRingOnce();
/** @brief EVENT 入队(队列满丢最旧)。 */
bool pushEvent(uint8_t type, const uint8_t *payload, size_t len);
/** @brief EVENT 出队(非阻塞)。 */
bool popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len);
/**
* @brief 解析并缓存 APP 身份 16B(HELLO_ACK 附加块 / PING body 同布局)。
* @param id 身份块
* @param out_version 可选输出版本
* @param out_uptime_sec 可选输出 uptime 秒
*/
void applyAppIdentity16(const uint8_t *id, gd32_version_t *out_version,
uint32_t *out_uptime_sec);
// ── 帧内同步状态机 ──
volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口)
@@ -332,6 +369,10 @@ private:
volatile uint8_t m_rxAckedSeq = 0; ///< 已确认的对端最新序号
volatile uint8_t m_expectedRxSeq = 0; ///< 期望收到的对端序号
volatile uint8_t m_gd32Role = 0; ///< GD32 角色(0=unknown, 1=app, 2=boot)
volatile bool m_gd32FeatKnown = false; ///< HELLO/PING 身份是否带 feat_map
volatile uint32_t m_gd32FeatFlags = 0; ///< V2_FEAT_*
volatile bool m_gd32IdKnown = false; ///< 是否已缓存版本号
gd32_version_t m_gd32Version = {}; ///< HELLO/PING 缓存的版本
volatile bool m_v2Active = false; ///< v2 session 是否激活
/** 最近一次 otaFindFrame 解析到的 RESPONSE 是否与 m_lastSentSeq 归属匹配 */
volatile bool m_lastRxAckMatched = false;