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:
@@ -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; // 幂等:状态未变时不重复日志
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 风格一致)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user