feat(protocol): 更新SPI Link协议至v2版本并优化CAN通信

- 将SPI OTA帧格式升级为SPI Link v2,采用新的帧格式:
  | SOF0 0xA5 | SOF1 0x5A | VER 0x02 | FLAGS | SESSION 4B LE |
  TX_SEQ | ACK_SEQ | CREDIT | TYPE | LEN 2B LE | RSV 2B | PAYLOAD |
  CRC32 4B LE |
- 新增V2_OVERHEAD=20字节开销,APP最大payload为236字节,BOOT为108字节
- 修改帧类型定义:ENTER_BOOT从0xB5合并至0xB0,新增CAN业务类型0xC0~C3
- 更新CAN协议文档,支持SPI Link v2-only模式,调整最大业务payload计算
- 扩展0xC1帧功能,支持双向通信和1Hz主动推送ID快照
- 添加0xC3完整ID快照帧类型,包含seq/rx_count等完整字段
- 更新协议文档中的错误码定义和反馈payload格式

refactor(main): 重构主程序入口并添加CAN反馈任务

- 在main.cpp中添加CAN反馈任务,更新启动流程注释
- 创建createCriticalTask辅助函数,统一关键任务创建逻辑
- 调整SPI初始化顺序,确保CAN反馈任务依赖正确
- 优化错误处理机制,关键任务失败时立即重启
- 将GD32版本探测移至ota_relay任务处理
- 更新版本号至0.3.48,调整编译时间和Git哈希

config: 更新SPI和BLE配置参数

- 添加SPI主机相关配置参数,包括CS GPIO、READY GPIO等引脚定义
- 配置SPI缓冲区大小:APP为256字节,BOOT为128字节
- 更新BLE滑动窗口参数,ACK窗口设为32,数据队列深度为72
- 调整SPI工作队列大小至5120字节,增加任务堆栈监控说明
This commit is contained in:
小龙虾
2026-07-16 23:00:15 +08:00
parent 51c89b3065
commit 5db3f66b8e
57 changed files with 4728 additions and 2098 deletions
+67 -40
View File
@@ -6,12 +6,17 @@
#include <algorithm>
#include <cstring>
#include <inttypes.h>
#include <stdlib.h>
#include "app_config.h"
#include "esp_timer.h"
#include "monitor/debug_log.h"
/** 截断/回绕 WARN 最小间隔(30s) */
static TickType_t cpuStatsWarnIntervalTicks()
{
return pdMS_TO_TICKS(30000);
}
CpuStats& CpuStats::instance()
{
static CpuStats inst;
@@ -78,91 +83,113 @@ static bool taskNameIsIdle(const char* name)
void CpuStats::sampleOnce()
{
UBaseType_t task_count = uxTaskGetNumberOfTasks();
if (task_count == 0) {
const UBaseType_t live = uxTaskGetNumberOfTasks();
if (live == 0) {
return;
}
task_count = std::min<UBaseType_t>(task_count + 2U, (UBaseType_t)CPU_TASK_SNAPSHOT_MAX);
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
if (!tasks) {
return;
/* P1:任务数超出快照上限时节流告警(否则静默丢任务) */
if (live > (UBaseType_t)CPU_TASK_SNAPSHOT_MAX) {
const TickType_t now_tick = xTaskGetTickCount();
if (m_lastTruncWarnTick == 0 ||
(now_tick - m_lastTruncWarnTick) >= cpuStatsWarnIntervalTicks()) {
m_lastTruncWarnTick = now_tick;
DEBUG_WARN("CPU",
"任务数 %lu > 快照上限 %u,本拍仅统计前 %u 个任务",
(unsigned long)live,
(unsigned)CPU_TASK_SNAPSHOT_MAX,
(unsigned)CPU_TASK_SNAPSHOT_MAX);
}
}
/* P1:固定成员缓冲,免每拍 heap */
const UBaseType_t buf_slots =
std::min<UBaseType_t>(live + 2U, (UBaseType_t)CPU_TASK_SNAPSHOT_MAX);
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
UBaseType_t captured = uxTaskGetSystemState(tasks, task_count, &total_runtime);
const UBaseType_t captured =
uxTaskGetSystemState(m_taskStatusBuf, buf_slots, &total_runtime);
const int64_t now_us = esp_timer_get_time();
if (captured == 0 || captured > task_count) {
if (captured == 0 || captured > buf_slots) {
DEBUG_WARN("CPU", "uxTaskGetSystemState 异常: captured=%lu buf=%lu",
(unsigned long)captured, (unsigned long)task_count);
free(tasks);
(unsigned long)captured, (unsigned long)buf_slots);
return;
}
CpuUsageSnapshot snap = {};
snap.available = true;
/*
* P2:先用 m_lastSamples 算本窗口,再写回基线(无需栈上 previous 拷贝)。
* total 回绕(total_runtime < m_lastTotal)时丢弃本窗,只重建基线。
*/
const bool had_previous = m_hasLast;
const configRUN_TIME_COUNTER_TYPE previous_total = m_lastTotal;
const int64_t previous_time_us = m_lastTimeUs;
const size_t previous_count = m_lastCount;
TaskSample previous_samples[CPU_TASK_SNAPSHOT_MAX];
for (size_t i = 0; i < previous_count && i < CPU_TASK_SNAPSHOT_MAX; ++i) {
previous_samples[i] = m_lastSamples[i];
const bool wrapped = had_previous && total_runtime < m_lastTotal;
if (wrapped) {
const TickType_t now_tick = xTaskGetTickCount();
if (m_lastWrapWarnTick == 0 ||
(now_tick - m_lastWrapWarnTick) >= cpuStatsWarnIntervalTicks()) {
m_lastWrapWarnTick = now_tick;
DEBUG_WARN("CPU",
"run-time 计数回绕,丢弃本采样窗 total=%" PRIu64 " prev=%" PRIu64,
(uint64_t)total_runtime,
(uint64_t)m_lastTotal);
}
}
/* 先更新基线,再算本窗口(与旧 HTTP 路径语义一致) */
size_t copy_count = std::min<size_t>((size_t)captured, CPU_TASK_SNAPSHOT_MAX);
for (size_t i = 0; i < copy_count; ++i) {
m_lastSamples[i].task_number = tasks[i].xTaskNumber;
m_lastSamples[i].runtime = tasks[i].ulRunTimeCounter;
}
m_lastCount = copy_count;
m_lastTotal = total_runtime;
m_lastTimeUs = now_us;
m_hasLast = true;
const bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
const uint64_t total_delta = can_compute ? runtimeDelta(total_runtime, previous_total) : 0U;
const bool can_compute =
had_previous && !wrapped && now_us > m_lastTimeUs;
const uint64_t total_delta =
can_compute ? runtimeDelta(total_runtime, m_lastTotal) : 0U;
uint64_t idle_delta = 0U;
if (can_compute && total_delta > 0U) {
for (UBaseType_t i = 0; i < captured; ++i) {
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
bool found = false;
for (size_t j = 0; j < previous_count; ++j) {
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
last_runtime = previous_samples[j].runtime;
for (size_t j = 0; j < m_lastCount; ++j) {
if (m_lastSamples[j].task_number == m_taskStatusBuf[i].xTaskNumber) {
last_runtime = m_lastSamples[j].runtime;
found = true;
break;
}
}
if (!found) {
continue;
continue; /* 新任务:本窗无 delta */
}
uint64_t delta = runtimeDelta(tasks[i].ulRunTimeCounter, last_runtime);
/* 单任务计数回绕时 runtimeDelta→0,本任务本窗记 0 */
uint64_t delta = runtimeDelta(m_taskStatusBuf[i].ulRunTimeCounter, last_runtime);
double pct = (double)delta * 100.0 / (double)total_delta;
if (taskNameIsIdle(tasks[i].pcTaskName)) {
if (taskNameIsIdle(m_taskStatusBuf[i].pcTaskName)) {
idle_delta += delta;
}
if (snap.task_count < CPU_TASK_SNAPSHOT_MAX) {
CpuTaskMetric& m = snap.tasks[snap.task_count++];
const char* n = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
const char* n = m_taskStatusBuf[i].pcTaskName ? m_taskStatusBuf[i].pcTaskName : "";
strncpy(m.name, n, sizeof(m.name) - 1);
m.name[sizeof(m.name) - 1] = '\0';
m.task_number = tasks[i].xTaskNumber;
m.task_number = m_taskStatusBuf[i].xTaskNumber;
m.runtime_delta = delta;
m.usage_pct = pct;
}
}
snap.valid = true;
snap.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
snap.sample_ms = (uint32_t)((now_us - m_lastTimeUs) / 1000);
snap.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
snap.usage_pct = std::max(0.0, 100.0 - snap.idle_pct);
}
free(tasks);
/* 更新基线(首拍 / 正常窗 / 回绕丢窗后均重建) */
const size_t copy_count = std::min<size_t>((size_t)captured, CPU_TASK_SNAPSHOT_MAX);
for (size_t i = 0; i < copy_count; ++i) {
m_lastSamples[i].task_number = m_taskStatusBuf[i].xTaskNumber;
m_lastSamples[i].runtime = m_taskStatusBuf[i].ulRunTimeCounter;
}
m_lastCount = copy_count;
m_lastTotal = total_runtime;
m_lastTimeUs = now_us;
m_hasLast = true;
portENTER_CRITICAL(&m_mux);
m_published = snap;
+8
View File
@@ -70,4 +70,12 @@ private:
configRUN_TIME_COUNTER_TYPE m_lastTotal = 0;
int64_t m_lastTimeUs = 0;
bool m_hasLast = false;
/** uxTaskGetSystemState 目标缓冲(固定、免每拍 calloc) */
TaskStatus_t m_taskStatusBuf[CPU_TASK_SNAPSHOT_MAX] = {};
/** 任务数截断 WARN 节流(tick) */
TickType_t m_lastTruncWarnTick = 0;
/** 运行时计数回绕 WARN 节流(tick) */
TickType_t m_lastWrapWarnTick = 0;
};
+448 -221
View File
@@ -12,79 +12,165 @@
* 时序说明:
* esp_netif_init() 创建 tcpip 线程后才可安全使用 socket
* 本模块在首次 send() 时才创建 socket(延迟创建)
* WiFi 连接前 sendto() 会失败(无路由),日志仍可写入 UART 与 HTTP 日志缓冲
* WiFi 连接后 UDP 立即生效,无需额外操作
* WiFi 未获 IP 时跳过 sendto;日志仍写 UART 与 HTTP ring
*
* 线程:仅任务上下文;禁止 ISR 调用 DEBUG_*(内部 portMAX_DELAY mutex)。
*/
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"
#include "esp_heap_caps.h"
#include "esp_netif.h"
#include "driver/uart.h"
#include "debug_log.h"
static const char *TAG = "DEBUG_LOG";
static TickType_t warnIntervalTicks()
{
return pdMS_TO_TICKS(10000);
}
static constexpr int kUdpSndTimeoutMs = 200;
static constexpr uint8_t kUdpFailRecreate = 8;
// ============================================================
// 单例
// 单例 / 工具
// ============================================================
/**
* @brief 获取 DebugLog 单例实例。
* @return DebugLog 引用。
*/
DebugLog& DebugLog::instance()
{
static DebugLog inst;
return inst;
}
// ============================================================
// 日志环形缓冲区(供 HTTP /logs 拉取)
// ============================================================
bool DebugLog::isKeepAliveTag(const char *tag)
{
if (!tag) {
return false;
}
/* OTA / OTA_COORD 等;上传路径 WIFI;总线 SPI */
if (strncmp(tag, "OTA", 3) == 0) {
return true;
}
if (strcmp(tag, "SPI") == 0 || strcmp(tag, "WIFI") == 0) {
return true;
}
return false;
}
char *DebugLog::allocEmptyJson(int *out_len)
{
char *empty = (char *)malloc(3);
if (!empty) {
return nullptr;
}
empty[0] = '[';
empty[1] = ']';
empty[2] = '\0';
if (out_len) {
*out_len = 2;
}
return empty;
}
/**
* @brief 获取当前环形日志缓冲区中的日志条数。
* @return 日志条数。
* JSON 字符串内容转义写入 buf[pos..];返回新 pos。
* 控制字符除 \n\r\t 外写成 \u00XX;空间不足时截断并尽量写 "..."。
*/
int DebugLog::jsonEscapeAppend(char *buf, int pos, int capacity, const char *src, int src_len)
{
const int content_start = pos;
const int hard_limit = capacity - 6; /* 预留 "..." + '"' + ']' + '\0' 量级 */
bool truncated = false;
for (int i = 0; i < src_len; ++i) {
if (pos >= hard_limit) {
truncated = true;
break;
}
/* 单条转义上限,避免异常长行占满 drain */
if (pos - content_start >= LOG_RING_SIZE * 2) {
truncated = true;
break;
}
const unsigned char c = (unsigned char)src[i];
if (c == '"') {
if (pos + 2 > hard_limit) { truncated = true; break; }
buf[pos++] = '\\';
buf[pos++] = '"';
} else if (c == '\\') {
if (pos + 2 > hard_limit) { truncated = true; break; }
buf[pos++] = '\\';
buf[pos++] = '\\';
} else if (c == '\n') {
if (pos + 2 > hard_limit) { truncated = true; break; }
buf[pos++] = '\\';
buf[pos++] = 'n';
} else if (c == '\r') {
if (pos + 2 > hard_limit) { truncated = true; break; }
buf[pos++] = '\\';
buf[pos++] = 'r';
} else if (c == '\t') {
if (pos + 2 > hard_limit) { truncated = true; break; }
buf[pos++] = '\\';
buf[pos++] = 't';
} else if (c < 0x20) {
/* 其它控制符 → \u00XX(6 字符) */
if (pos + 6 > hard_limit) { truncated = true; break; }
static const char hex[] = "0123456789abcdef";
buf[pos++] = '\\';
buf[pos++] = 'u';
buf[pos++] = '0';
buf[pos++] = '0';
buf[pos++] = hex[(c >> 4) & 0xF];
buf[pos++] = hex[c & 0xF];
} else {
buf[pos++] = (char)c;
}
}
if (truncated && pos + 3 <= hard_limit) {
buf[pos++] = '.';
buf[pos++] = '.';
buf[pos++] = '.';
}
return pos;
}
// ============================================================
// 环形缓冲(Locked 变体假定已持 m_logRingMutex)
// ============================================================
int DebugLog::ringCount()
{
if (!m_logRingMutex) return 0;
if (!m_logRingMutex) {
return 0;
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
int count = m_logRingCount;
const int count = m_logRingCount;
xSemaphoreGive(m_logRingMutex);
return count;
}
/**
* @brief 弹出一条最旧日志并从环形缓冲区移除。
* @param out 输出缓冲区。
* @param out_size 输出缓冲区大小。
* @param out_len 输出日志长度。
* @return true 表示成功弹出一条日志,false 表示缓冲区为空或参数无效。
*/
bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
bool DebugLog::ringPopOldestLocked(char *out, size_t out_size, int *out_len)
{
if (out_len) {
*out_len = 0;
}
if (!m_logRingMutex || !out || out_size == 0) {
if (!out || out_size == 0 || m_logRingCount <= 0) {
return false;
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
if (m_logRingCount <= 0) {
xSemaphoreGive(m_logRingMutex);
return false;
}
int idx = (m_logRingHead - m_logRingCount + LOG_RING_SLOTS) % LOG_RING_SLOTS;
const int idx = (m_logRingHead - m_logRingCount + LOG_RING_SLOTS) % LOG_RING_SLOTS;
int len = m_logRingLens[idx];
if (len < 0) {
len = 0;
@@ -105,132 +191,212 @@ bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
m_logRingLens[idx] = 0;
m_logRingCount--;
xSemaphoreGive(m_logRingMutex);
return true;
}
/**
* @brief 将环形缓冲区所有日志导出为 JSON 数组字符串。
* @param out_len 输出 JSON 字符串长度。
* @return 动态分配的 JSON 字符串,调用者须 free()。
*/
char* DebugLog::ringDrainJson(int *out_len)
bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
{
if (!out_len) return NULL;
if (out_len) {
*out_len = 0;
}
if (!m_logRingMutex || !out || out_size == 0) {
return false;
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
const bool ok = ringPopOldestLocked(out, out_size, out_len);
xSemaphoreGive(m_logRingMutex);
return ok;
}
/** 预览即将 drain 的条数与原始字节总和(用于估 capacity)。 */
int DebugLog::ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const
{
if (total_raw_len) {
*total_raw_len = 0;
}
int n = m_logRingCount;
if (n > max_n) {
n = max_n;
}
if (n <= 0) {
return 0;
}
int sum = 0;
for (int i = 0; i < n; ++i) {
const int idx = (m_logRingHead - m_logRingCount + i + LOG_RING_SLOTS) % LOG_RING_SLOTS;
int len = m_logRingLens[idx];
if (len < 0) {
len = 0;
}
if (len >= LOG_RING_SIZE) {
len = LOG_RING_SIZE - 1;
}
sum += len;
}
if (total_raw_len) {
*total_raw_len = sum;
}
return n;
}
void DebugLog::ringPushLocked(const char *data, int len)
{
if (!data || len <= 0) {
return;
}
int copy_len = len;
if (copy_len >= LOG_RING_SIZE) {
copy_len = LOG_RING_SIZE - 1;
}
memcpy(m_logRing[m_logRingHead], data, (size_t)copy_len);
m_logRing[m_logRingHead][copy_len] = '\0';
m_logRingLens[m_logRingHead] = copy_len;
m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS;
if (m_logRingCount < LOG_RING_SLOTS) {
m_logRingCount++;
}
}
void DebugLog::ringPush(const char *data, int len)
{
if (!m_logRingMutex || len <= 0) {
return;
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
ringPushLocked(data, len);
xSemaphoreGive(m_logRingMutex);
}
/**
* 单次持锁:peek 元数据 →(可选)按堆缩小条数 → pop+转义拼 JSON。
* capacity 按真实 raw 长度估算,避免 48×320 虚高导致 OTA 期返空。
*/
char *DebugLog::ringDrainJson(int *out_len)
{
if (!out_len) {
return nullptr;
}
*out_len = 0;
/* 堆紧时不 malloc:返回堆上最小 "[]",调用方仍 free。 */
const uint32_t free_now = esp_get_free_heap_size();
if (free_now < kDrainMinFree) {
char *empty = (char *)malloc(3);
if (!empty) return NULL;
empty[0] = '[';
empty[1] = ']';
empty[2] = '\0';
*out_len = 2;
return empty;
if (free_now < kDrainMinFree || !m_logRingMutex) {
return allocEmptyJson(out_len);
}
int count = ringCount();
/* OTA 沉默失败排查需要一次多拉;上限 24 条 */
static constexpr int kMaxDrain = 24;
if (count > kMaxDrain) count = kMaxDrain;
int capacity = count * (LOG_RING_SIZE * 2) + 16;
if (capacity < 16) capacity = 16;
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
int total_raw = 0;
int drain_n = ringPeekDrainMetaLocked(LOG_DRAIN_MAX, &total_raw);
if (drain_n <= 0) {
xSemaphoreGive(m_logRingMutex);
return allocEmptyJson(out_len);
}
/* 转义最坏 ~2x + 每条 "" 与逗号 + 数组括号 */
auto calc_capacity = [](int n, int raw) -> int {
int cap = raw * 2 + n * 4 + 16;
if (cap < 16) {
cap = 16;
}
return cap;
};
int capacity = calc_capacity(drain_n, total_raw);
const size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
if (largest < (size_t)capacity) {
char *empty = (char *)malloc(3);
if (!empty) return NULL;
empty[0] = '[';
empty[1] = ']';
empty[2] = '\0';
*out_len = 2;
return empty;
/* 堆不够则减条数重估(仍持锁,peek 只读;减 n 时从最旧开始仍一致) */
while (drain_n > 0 && largest < (size_t)capacity) {
drain_n = drain_n / 2;
if (drain_n == 0) {
break;
}
total_raw = 0;
drain_n = ringPeekDrainMetaLocked(drain_n, &total_raw);
capacity = calc_capacity(drain_n, total_raw);
}
char *buf = (char*)malloc(capacity);
if (!buf) return NULL;
if (drain_n <= 0 || largest < (size_t)capacity) {
xSemaphoreGive(m_logRingMutex);
return allocEmptyJson(out_len);
}
/* 持锁期间尽量短:只 pop 到栈上临时区会爆栈;改为直接写入 malloc 缓冲。
* 先释放锁再 malloc,再二次持锁 pop——中间可能插入新日志,只 drain 最旧 N 条即可。 */
xSemaphoreGive(m_logRingMutex);
char *buf = (char *)malloc((size_t)capacity);
if (!buf) {
return allocEmptyJson(out_len);
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
/* 重新核对:ring 可能变少 */
if (m_logRingCount <= 0) {
xSemaphoreGive(m_logRingMutex);
free(buf);
return allocEmptyJson(out_len);
}
if (drain_n > m_logRingCount) {
drain_n = m_logRingCount;
}
if (drain_n > LOG_DRAIN_MAX) {
drain_n = LOG_DRAIN_MAX;
}
int pos = 0;
buf[pos++] = '[';
bool first = true;
char entry[LOG_RING_SIZE];
int entryLen = 0;
while (ringPopOldest(entry, sizeof(entry), &entryLen) && entryLen > 0) {
// 逗号写入前做边界检查,预留 4 字节给 ']' + '\0' 及末尾余量
for (int k = 0; k < drain_n; ++k) {
/* 先估空间再 pop,避免 pop 后写不下导致丢条 */
const int need_min = first ? 4 : 5; /* ,"x" 或 "x" 量级 + 闭合余量 */
if (pos + need_min >= capacity - 2) {
break;
}
int entry_len = 0;
if (!ringPopOldestLocked(entry, sizeof(entry), &entry_len) || entry_len <= 0) {
break;
}
if (!first) {
if (pos + 1 >= capacity - 4) break;
buf[pos++] = ',';
}
first = false;
buf[pos++] = '"';
// 单条日志转义后超过 LOG_RING_SIZE * 2 时截断,标记 "..." 后缀
int content_start = pos;
bool truncated = false;
for (int i = 0; i < entryLen && pos < capacity - 4; i++) {
if (pos - content_start >= LOG_RING_SIZE * 2) {
truncated = true;
break;
}
char c = entry[i];
if (c == '"') { buf[pos++] = '\\'; buf[pos++] = '"'; }
else if (c == '\\') { buf[pos++] = '\\'; buf[pos++] = '\\'; }
else if (c == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; }
else if (c == '\r') { buf[pos++] = '\\'; buf[pos++] = 'r'; }
else if (c == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; }
else if ((unsigned char)c >= 0x20) { buf[pos++] = c; }
pos = jsonEscapeAppend(buf, pos, capacity, entry, entry_len);
if (pos < capacity - 1) {
buf[pos++] = '"';
} else if (pos < capacity) {
buf[pos - 1] = '"'; /* 极端:覆盖末字节闭合 */
}
if (truncated) {
// 确保 "..." + '"' + ']' + '\0' (共 6 字节) 有空间才写入标记
if (pos + 6 <= capacity) {
buf[pos++] = '.';
buf[pos++] = '.';
buf[pos++] = '.';
}
}
buf[pos++] = '"';
}
buf[pos++] = ']';
buf[pos] = '\0';
if (pos + 1 < capacity) {
buf[pos++] = ']';
buf[pos] = '\0';
} else if (capacity >= 3) {
buf[capacity - 2] = ']';
buf[capacity - 1] = '\0';
pos = capacity - 1;
}
xSemaphoreGive(m_logRingMutex);
*out_len = pos;
return buf;
}
/**
* @brief 向环形日志缓冲区中推送一条日志数据。
* @param data 日志数据指针。
* @param len 数据长度。
*/
void DebugLog::ringPush(const char *data, int len)
{
if (!m_logRingMutex || len <= 0) return;
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
int copy_len = len;
if (copy_len >= LOG_RING_SIZE) copy_len = LOG_RING_SIZE - 1;
memcpy(m_logRing[m_logRingHead], data, copy_len);
m_logRing[m_logRingHead][copy_len] = '\0';
m_logRingLens[m_logRingHead] = copy_len;
m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS;
if (m_logRingCount < LOG_RING_SLOTS) m_logRingCount++;
xSemaphoreGive(m_logRingMutex);
}
// ============================================================
// UART 调试日志
// UART
// ============================================================
/**
* @brief 初始化 UART0 调试串口。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::uartInit()
{
if (m_uartInitialized) {
@@ -248,7 +414,8 @@ esp_err_t DebugLog::uartInit()
esp_err_t ret = uart_driver_install(uart_port, 1024, 0, 0, NULL, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "UART0 驱动安装失败: %s", esp_err_to_name(ret));
/* 可能 console 已占用 UART0——降级为 ring-only */
ESP_LOGW(TAG, "UART0 驱动安装失败(可能 console 占用): %s", esp_err_to_name(ret));
return ret;
}
@@ -266,49 +433,34 @@ esp_err_t DebugLog::uartInit()
}
m_uartInitialized = true;
// 标记 UART 真正可用,send() 依据此标志决定是否调用 uart_write_bytes
m_uartAvailable = true;
ESP_LOGD(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)",
UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_DEBUG_BAUD_RATE);
return ESP_OK;
}
/**
* @brief 通过 UART0 直接发送原始字节数据。
* @param data 待发送数据指针。
* @param len 数据长度。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
{
if (!m_uartInitialized) {
if (!m_uartInitialized || !m_uartAvailable) {
return ESP_ERR_INVALID_STATE;
}
int ret = uart_write_bytes(UART_NUM_0, data, len);
const int ret = uart_write_bytes(UART_NUM_0, data, len);
if (ret < 0) {
return ESP_FAIL;
}
return ESP_OK;
}
// ============================================================
// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
// 初始化 / UDP
// ============================================================
/**
* @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::init()
{
if (m_initialized) {
return ESP_OK;
}
// 检查 uartInit 返回值;失败时仍让 ring buffer 模式工作,
// 但记录"UART 不可用"日志,并在 send() 中跳过 uart_write_bytes 调用。
esp_err_t uart_ret = uartInit();
if (uart_ret != ESP_OK) {
ESP_LOGW(TAG, "UART 不可用,仅 ring buffer 模式 (err=%s)", esp_err_to_name(uart_ret));
@@ -316,8 +468,13 @@ esp_err_t DebugLog::init()
}
m_logRingMutex = xSemaphoreCreateMutex();
if (!m_logRingMutex) {
ESP_LOGE(TAG, "ring mutex 创建失败,HTTP /logs 将不可用");
/* 仍标记 initialized,避免 log() 反复 init;UART 路径可工作 */
m_initialized = true;
return ESP_ERR_NO_MEM;
}
// 目标地址:单播到上位机
memset(&m_debugAddr, 0, sizeof(m_debugAddr));
m_debugAddr.sin_family = AF_INET;
m_debugAddr.sin_port = htons(DEBUG_LOG_PORT);
@@ -325,121 +482,173 @@ esp_err_t DebugLog::init()
m_initialized = true;
ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
return ESP_OK;
}
/**
* @brief 确保 UDP socket 已创建(延迟创建)。
* @return esp_err_t 错误码。
*/
bool DebugLog::udpNetifHasIp() const
{
esp_netif_ip_info_t info = {};
esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (sta && esp_netif_get_ip_info(sta, &info) == ESP_OK && info.ip.addr != 0) {
return true;
}
esp_netif_t *ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (ap && esp_netif_get_ip_info(ap, &info) == ESP_OK && info.ip.addr != 0) {
return true;
}
return false;
}
void DebugLog::closeUdpSocket()
{
int s = -1;
portENTER_CRITICAL(&m_udpMux);
s = m_udpSock;
m_udpSock = -1;
portEXIT_CRITICAL(&m_udpMux);
if (s >= 0) {
close(s);
}
}
esp_err_t DebugLog::ensureUdpSocket()
{
portENTER_CRITICAL(&m_udpMux);
if (m_udpSock >= 0) {
portEXIT_CRITICAL(&m_udpMux);
return ESP_OK;
}
portEXIT_CRITICAL(&m_udpMux);
m_udpSock = socket(AF_INET, SOCK_DGRAM, 0);
if (m_udpSock < 0) {
ESP_LOGE(TAG, "创建 UDP socket 失败");
const int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) {
ESP_LOGE(TAG, "创建 UDP socket 失败 errno=%d", errno);
return ESP_FAIL;
}
ESP_LOGI(TAG, "UDP socket 创建成功");
/* 避免 sendto 在无路由/对端不可达时长时间阻塞日志路径 */
struct timeval tv = {};
tv.tv_sec = kUdpSndTimeoutMs / 1000;
tv.tv_usec = (kUdpSndTimeoutMs % 1000) * 1000;
(void)setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
portENTER_CRITICAL(&m_udpMux);
if (m_udpSock >= 0) {
/* 竞态:另一任务已建好 */
portEXIT_CRITICAL(&m_udpMux);
close(s);
return ESP_OK;
}
m_udpSock = s;
portEXIT_CRITICAL(&m_udpMux);
ESP_LOGI(TAG, "UDP socket 创建成功 (SNDTIMEO=%dms)", kUdpSndTimeoutMs);
return ESP_OK;
}
/**
* @brief 发送原始日志数据到底层输出通道。
* @param data 待发送数据指针。
* @param len 数据长度。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::send(const uint8_t *data, size_t len)
{
if (!data || len == 0) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t udp_ret = ESP_FAIL;
esp_err_t uart_ret = ESP_FAIL;
const uint32_t free_now = esp_get_free_heap_size();
// UART 通道(UART 不可用时跳过 uart_write_bytes 调用)
if (m_uartAvailable) {
uart_ret = uartSend(data, len);
}
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
* WiFi 未连接时 sendto 失败也静默跳过。
* ★ 修复:堆不足跳过时写一条告警到 ring buffer,让前端 /logs 可见。 */
if (m_initialized && free_now >= kSkipUdpFree) {
/* UDP:堆紧 / 无 IP 时跳过 sendto */
if (m_initialized && free_now >= kSkipUdpFree && udpNetifHasIp()) {
if (m_udpSock < 0) {
ensureUdpSocket();
(void)ensureUdpSocket();
}
if (m_udpSock >= 0) {
size_t send_len = len > 1024 ? 1024 : len;
int ret = ::sendto(m_udpSock, data, send_len, 0,
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
int sock = -1;
portENTER_CRITICAL(&m_udpMux);
sock = m_udpSock;
portEXIT_CRITICAL(&m_udpMux);
if (sock >= 0) {
const size_t send_len = len > 1024 ? 1024 : len;
const int ret = ::sendto(sock, data, send_len, 0,
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
if (ret >= 0) {
udp_ret = ESP_OK;
m_udpFailStreak = 0;
} else {
if (m_udpFailStreak < 255) {
m_udpFailStreak++;
}
if (m_udpFailStreak >= kUdpFailRecreate) {
closeUdpSocket();
m_udpFailStreak = 0;
const TickType_t now = xTaskGetTickCount();
if (m_lastUdpFailWarnTick == 0 ||
(now - m_lastUdpFailWarnTick) >= warnIntervalTicks()) {
m_lastUdpFailWarnTick = now;
static const char kMsg[] = "[W][LOG] UDP 连续失败,已重建 socket\n";
ringPush(kMsg, (int)sizeof(kMsg) - 1);
}
}
}
}
} else if (m_initialized) {
/* 堆不足导致 UDP 被跳过——通过 ring buffer 记录,避免上位机无法感知 */
static uint32_t s_lastUdpSkipWarnMs = 0;
uint32_t now_ms = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
/* 限制告警频率:每 10 秒最多一条,避免刷屏 */
if ((now_ms - s_lastUdpSkipWarnMs) > 10000) {
s_lastUdpSkipWarnMs = now_ms;
} else if (m_initialized && free_now < kSkipUdpFree) {
const TickType_t now = xTaskGetTickCount();
if (m_lastUdpSkipWarnTick == 0 ||
(now - m_lastUdpSkipWarnTick) >= warnIntervalTicks()) {
m_lastUdpSkipWarnTick = now;
static const char kUdpSkipMsg[] = "[W][LOG] UDP 发送已暂停: 堆内存不足\n";
ringPush(kUdpSkipMsg, sizeof(kUdpSkipMsg) - 1);
ringPush(kUdpSkipMsg, (int)sizeof(kUdpSkipMsg) - 1);
}
}
// 同时写入环形缓冲区(供 HTTP /logs 拉取)——静态,不占堆
ringPush((const char *)data, (int)len);
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
return ESP_OK;
}
/* 仅 ring 成功也算可用(OTA 堆紧时常见) */
if (m_logRingMutex) {
return ESP_OK;
}
return ESP_FAIL;
}
/**
* @brief 按日志级别与标签格式化输出一条日志。
* @param level 日志级别。
* @param tag 日志标签。
* @param fmt 格式化字符串。
* @param ... 可变参数。
*/
void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...)
{
if (!m_initialized) {
init();
(void)init();
}
if (level > logLevel) {
if (level > m_logLevel) {
return;
}
/* 堆门控:OTA/SPI 的 INFO 必须进 ring,否则 WebUI 只能看到“沉默失败”。 */
const uint32_t free_now = esp_get_free_heap_size();
const bool keep_ota_spi = (tag != nullptr &&
(strcmp(tag, "OTA") == 0 || strcmp(tag, "SPI") == 0));
if (!keep_ota_spi) {
if (free_now < kDropWarnFree && level > DEBUG_LOG_ERR) {
return; // 无关模块:仅 ERR
const bool keep = isKeepAliveTag(tag);
/*
* 堆门控:
* - ERR/WARN:永不因堆丢弃(排障必需)
* - keep-alive(OTA 前缀 / SPI / WIFI):INFO 保留;极紧时仍丢 DBG
* - 其它:free < kDropInfoFree 丢 INFO/DBG
*/
if (level >= DEBUG_LOG_INFO) {
if (level >= DEBUG_LOG_DBG && free_now < kDropWarnFree) {
return;
}
if (free_now < kDropInfoFree && level > DEBUG_LOG_WARN) {
return; // 无关模块:丢 INFO/DBG
if (!keep) {
if (free_now < kDropInfoFree) {
return;
}
} else if (level >= DEBUG_LOG_DBG && free_now < kDropInfoFree) {
/* keep-alive DBG:中等堆紧也丢,减刷屏 */
return;
}
} else if (free_now < kDropWarnFree && level >= DEBUG_LOG_DBG) {
return; // OTA/SPI:极紧时仍丢 DBG,INFO/WARN/ERR 保留
}
char prefix[24];
const char *level_str;
switch (level) {
case DEBUG_LOG_ERR: level_str = "E"; break;
@@ -448,29 +657,47 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
case DEBUG_LOG_DBG: level_str = "D"; break;
default: level_str = "?"; break;
}
snprintf(prefix, sizeof(prefix), "[%s][%s]", level_str, tag);
/* 栈缓冲与 LOG_RING_SIZE 对齐,减轻 httpd 等任务栈压。 */
va_list args;
va_start(args, fmt);
char msg[160];
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
char log_buf[192];
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
// ★ snprintf 截断时返回"本应写入的长度"(可能 > sizeof(log_buf)),
// 直接传给 send() 会越界读取栈外内存 → 破坏相邻任务 TCB → "Stack protection fault"
// (崩溃栈中出现 HTTP User-Agent / "/ota/relay" 等字符串即此症状)。
// 截断到实际缓冲区大小,避免越界读。
if (total < 0) {
/* 单缓冲:与 LOG_RING_SIZE 对齐,避免 msg[]+log_buf[] 双栈 */
char log_buf[LOG_RING_SIZE];
const char *safe_tag = tag ? tag : "?";
int prefix_len = snprintf(log_buf, sizeof(log_buf), "[%s][%s] ", level_str, safe_tag);
if (prefix_len < 0) {
return;
}
if ((size_t)total >= sizeof(log_buf)) {
total = (int)sizeof(log_buf) - 1;
log_buf[total] = '\0';
if ((size_t)prefix_len >= sizeof(log_buf)) {
/* 前缀已截断:保证可发送的以 \\n 结尾的串 */
log_buf[sizeof(log_buf) - 2] = '\n';
log_buf[sizeof(log_buf) - 1] = '\0';
(void)send((const uint8_t *)log_buf, sizeof(log_buf) - 1);
return;
}
send((const uint8_t *)log_buf, total);
va_list args;
va_start(args, fmt);
int body = vsnprintf(log_buf + prefix_len, sizeof(log_buf) - (size_t)prefix_len, fmt, args);
va_end(args);
if (body < 0) {
return;
}
size_t used = (size_t)prefix_len + (size_t)body;
if (used >= sizeof(log_buf)) {
used = sizeof(log_buf) - 1;
log_buf[used] = '\0';
}
/* 保证换行结尾(截断时覆盖末字节) */
if (used == 0 || log_buf[used - 1] != '\n') {
if (used + 1 < sizeof(log_buf)) {
log_buf[used++] = '\n';
log_buf[used] = '\0';
} else {
log_buf[sizeof(log_buf) - 2] = '\n';
log_buf[sizeof(log_buf) - 1] = '\0';
used = sizeof(log_buf) - 1;
}
}
(void)send((const uint8_t *)log_buf, used);
}
+47 -29
View File
@@ -8,8 +8,11 @@
*
* 注意:当前 tools/WebUI 日志面板走的是 HTTP /logs,
* 不是直接监听 UDP 6666。
*
*
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
*
* 线程模型:DEBUG_* / log() / send() 仅允许在任务上下文调用
* (内部使用 portMAX_DELAY 互斥锁,禁止 ISR)。
*/
#ifndef DEBUG_LOG_H
#define DEBUG_LOG_H
@@ -23,7 +26,7 @@
#include <sys/socket.h>
#include <netinet/in.h>
// 调试日志配置(DEBUG_LOG_IP/PORT/ENABLED 已在 app_config.h 中定义)
// 调试日志配置(DEBUG_LOG_IP/PORT 在 app_config.h)
// UART0 调试配置
#define UART_DEBUG_TX_GPIO 21 ///< UART0 TX GPIO 引脚号
@@ -46,13 +49,9 @@ typedef enum {
*/
class DebugLog {
public:
/**
* @brief 获取 DebugLog 单例实例。
* @return DebugLog 引用。
*/
static DebugLog& instance();
/** @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 */
/** @brief 初始化日志系统(UART + ring;UDP socket 延迟创建)。 */
esp_err_t init();
/** @brief 初始化 UART0 调试串口。 */
@@ -61,10 +60,10 @@ public:
/** @brief 通过 UART0 直接发送一段原始字节数据。 */
esp_err_t uartSend(const uint8_t *data, size_t len);
/** @brief 发送原始日志数据到底层输出通道。 */
/** @brief 发送原始日志数据到底层输出通道(任务上下文)。 */
esp_err_t send(const uint8_t *data, size_t len);
/** @brief 按日志级别与标签格式化输出一条日志。 */
/** @brief 按日志级别与标签格式化输出一条日志(任务上下文)。 */
void log(debug_log_level_t level, const char *tag, const char *fmt, ...);
/** @brief 获取当前环形日志缓冲区中的日志条数。 */
@@ -74,14 +73,14 @@ public:
bool ringPopOldest(char *out, size_t out_size, int *out_len);
/**
* @brief 将环形缓冲区所有日志导出为 JSON 数组字符串。
* @brief 将环形缓冲区日志导出为 JSON 数组字符串。
* @param out_len 输出 JSON 字符串长度。
* @return 动态分配的 JSON 字符串,调用者须 free()。
* @return 动态分配的 JSON 字符串,调用者须 free();失败返回 NULL。
*/
char* ringDrainJson(int *out_len);
/** @brief 当前日志级别(运行时可改)。 */
debug_log_level_t logLevel = DEBUG_LOG_INFO;
void setLogLevel(debug_log_level_t level) { m_logLevel = level; }
debug_log_level_t getLogLevel() const { return m_logLevel; }
private:
DebugLog() = default;
@@ -90,40 +89,59 @@ private:
DebugLog& operator=(const DebugLog&) = delete;
int m_udpSock = -1;
struct sockaddr_in m_debugAddr;
struct sockaddr_in m_debugAddr = {};
bool m_initialized = false;
bool m_uartInitialized = false;
// 记录 UART 是否真正可用(uartInit 失败时为 false,跳过 uart_write_bytes)。
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
/** uartInit 成功后才为 true;send() 据此跳过 uart_write_bytes */
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;
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE];
int m_logRingLens[LOG_RING_SLOTS];
/** 单次 /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] = {};
int m_logRingHead = 0;
int m_logRingCount = 0;
SemaphoreHandle_t m_logRingMutex = NULL;
SemaphoreHandle_t m_logRingMutex = nullptr;
/* 堆门控(防 OTA 时 UDP/格式化抢 LWIP 堆):
* free < kDropWarnFree → 非 OTA/SPI 只保留 ERR
* free < kDropInfoFree → 非 OTA/SPI 丢 INFO/DBG(OTA/SPI 的 INFO 仍进 ring)
* ERR/WARN 永不因堆门控丢弃
* free < kDropInfoFree → 非 keep-alive 丢 INFO/DBG;keep-alive 保留 INFO
* free < kDropWarnFree → 全局丢 DBG
* free < kSkipUdpFree → 不 sendto(仍 UART+ring)
* free < kDrainMinFree → /logs 尽量仍返回 ring(降低门槛,避免“沉默失败”) */
* free < kDrainMinFree → /logs 返 "[]" 避免大块 malloc
* keep-alive tag:OTA 前缀、SPI、WIFI */
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
static constexpr uint32_t kSkipUdpFree = 6U * 1024U; // ★ 修复:从 10KB 降至 6KB,sendto 本身不需大块堆
static constexpr uint32_t kDrainMinFree = 4U * 1024U; // ★ 修复:从 8KB 降至 4KB,降低 /logs 返空的概率
static constexpr uint32_t kSkipUdpFree = 6U * 1024U;
static constexpr uint32_t kDrainMinFree = 4U * 1024U;
/** UDP 跳过/失败告警节流与 socket 重建 */
TickType_t m_lastUdpSkipWarnTick = 0;
TickType_t m_lastUdpFailWarnTick = 0;
uint8_t m_udpFailStreak = 0;
portMUX_TYPE m_udpMux = portMUX_INITIALIZER_UNLOCKED;
esp_err_t ensureUdpSocket();
void closeUdpSocket();
bool udpNetifHasIp() const;
void ringPush(const char *data, int len);
void ringPushLocked(const char *data, int len);
bool ringPopOldestLocked(char *out, size_t out_size, int *out_len);
int ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const;
static bool isKeepAliveTag(const char *tag);
static char *allocEmptyJson(int *out_len);
static int jsonEscapeAppend(char *buf, int pos, int capacity, const char *src, int src_len);
};
// 便捷宏 — 直接调用单例方法
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__) ///< 错误级别日志宏
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__) ///< 警告级别日志宏
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__) ///< 信息级别日志宏
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__) ///< 调试级别日志宏
// 便捷宏 — 直接调用单例方法(禁止在 ISR 中使用)
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
#endif // DEBUG_LOG_H
+1 -1
View File
@@ -32,7 +32,7 @@
// 避免 5 秒巡检刷屏。health_mon 任务从不被 OTA 暂停,DEBUG_ERR 经 UDP/UART
// 直发,不受 OTA 门控抑制。
static constexpr float STACK_WARN_RATIO = 0.20f; ///< 剩余水位告警阈值(栈大小比例)
static constexpr size_t STACK_TASK_MAX = 8; ///< 受监控任务上限
static constexpr size_t STACK_TASK_MAX = 12; ///< 受监控任务上限(含 spi_worker 等)
static bool s_stackWarned[STACK_TASK_MAX] = {}; ///< 各任务是否已告警(去重)
// ============================================================
+5
View File
@@ -12,6 +12,7 @@
#include "ota_relay.h"
#include "wifi_ota.h"
#include "ble_ota.h"
#include "spi_master.h"
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
{
@@ -30,9 +31,13 @@ size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
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 };
}
+2
View File
@@ -5,6 +5,8 @@
* - health_monitor 巡检主动告警(低水位时 DEBUG_ERR,突破 OTA 门控)
* - http_handlers /system/status 被动报告(前端每秒轮询展示)
* 两处复用同一份任务表,避免散落维护。
*
* 表内含转发关键:ota_relay、spi_worker(帧缓冲热路径)。
*/
#pragma once