Files
ESP32_COMM/main/monitor/debug_log.cpp
T
小龙虾 51c89b3065 feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制
移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2
CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发
实现流量控制,提高OTA传输稳定性。

refactor(stack): 统一任务栈大小配置并添加CPU统计功能

- 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配
- 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样
- 更新main.cpp中的任务创建使用新的栈大小常量
- 调整health_monitor和ota_relay等任务的栈分配

perf(ota): 改进ESP32和GD32 OTA分区预擦策略

- ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞
- GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量
- 优化OTA协调器状态管理,修复FINALIZING状态残留问题

fix(log): 改进UDP日志发送的堆内存管理

- 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer
- 降低UDP发送跳过的内存阈值从10KB到6KB
- 降低日志排水最小内存阈值从8KB到4KB

docs(readme): 更新OTA配置参数文档说明

移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为
基于CREDIT背压机制的描述。
2026-07-16 01:24:12 +08:00

477 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* debug_log.cpp - 调试日志输出实现
*
* 日志会同时写入三处:
* 1. UART0 串口(本地串口调试)
* 2. UDP 单播到指定上位机(独立 UDP 调试接收端可选)
* 3. 内部环形缓冲区(供 HTTP /logs 拉取,WebUI 实际使用这一条)
*
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
* UDP: <DEBUG_LOG_IP>:<DEBUG_LOG_PORT>
*
* 时序说明:
* esp_netif_init() 创建 tcpip 线程后才可安全使用 socket
* 本模块在首次 send() 时才创建 socket(延迟创建)
* WiFi 连接前 sendto() 会失败(无路由),日志仍可写入 UART 与 HTTP 日志缓冲
* WiFi 连接后 UDP 立即生效,无需额外操作
*/
#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.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 "driver/uart.h"
#include "debug_log.h"
static const char *TAG = "DEBUG_LOG";
// ============================================================
// 单例
// ============================================================
/**
* @brief 获取 DebugLog 单例实例。
* @return DebugLog 引用。
*/
DebugLog& DebugLog::instance()
{
static DebugLog inst;
return inst;
}
// ============================================================
// 日志环形缓冲区(供 HTTP /logs 拉取)
// ============================================================
/**
* @brief 获取当前环形日志缓冲区中的日志条数。
* @return 日志条数。
*/
int DebugLog::ringCount()
{
if (!m_logRingMutex) return 0;
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
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)
{
if (out_len) {
*out_len = 0;
}
if (!m_logRingMutex || !out || out_size == 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;
int len = m_logRingLens[idx];
if (len < 0) {
len = 0;
}
int copy_len = len;
if ((size_t)copy_len >= out_size) {
copy_len = (int)out_size - 1;
}
if (copy_len > 0) {
memcpy(out, m_logRing[idx], (size_t)copy_len);
}
out[copy_len] = '\0';
if (out_len) {
*out_len = copy_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)
{
if (!out_len) return NULL;
*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;
}
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;
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;
}
char *buf = (char*)malloc(capacity);
if (!buf) return NULL;
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' 及末尾余量
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; }
}
if (truncated) {
// 确保 "..." + '"' + ']' + '\0' (共 6 字节) 有空间才写入标记
if (pos + 6 <= capacity) {
buf[pos++] = '.';
buf[pos++] = '.';
buf[pos++] = '.';
}
}
buf[pos++] = '"';
}
buf[pos++] = ']';
buf[pos] = '\0';
*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 调试日志
// ============================================================
/**
* @brief 初始化 UART0 调试串口。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::uartInit()
{
if (m_uartInitialized) {
return ESP_OK;
}
const uart_port_t uart_port = UART_NUM_0;
uart_config_t uart_config = {};
uart_config.baud_rate = UART_DEBUG_BAUD_RATE;
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = UART_PARITY_DISABLE;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_config.source_clk = UART_SCLK_DEFAULT;
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));
return ret;
}
ret = uart_param_config(uart_port, &uart_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "UART0 参数配置失败: %s", esp_err_to_name(ret));
return ret;
}
ret = uart_set_pin(uart_port, UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "UART0 引脚配置失败: %s", esp_err_to_name(ret));
return ret;
}
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) {
return ESP_ERR_INVALID_STATE;
}
int ret = uart_write_bytes(UART_NUM_0, data, len);
if (ret < 0) {
return ESP_FAIL;
}
return ESP_OK;
}
// ============================================================
// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
// ============================================================
/**
* @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));
m_uartAvailable = false;
}
m_logRingMutex = xSemaphoreCreateMutex();
// 目标地址:单播到上位机
memset(&m_debugAddr, 0, sizeof(m_debugAddr));
m_debugAddr.sin_family = AF_INET;
m_debugAddr.sin_port = htons(DEBUG_LOG_PORT);
m_debugAddr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP);
m_initialized = true;
ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
return ESP_OK;
}
/**
* @brief 确保 UDP socket 已创建(延迟创建)。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::ensureUdpSocket()
{
if (m_udpSock >= 0) {
return ESP_OK;
}
m_udpSock = socket(AF_INET, SOCK_DGRAM, 0);
if (m_udpSock < 0) {
ESP_LOGE(TAG, "创建 UDP socket 失败");
return ESP_FAIL;
}
ESP_LOGI(TAG, "UDP socket 创建成功");
return ESP_OK;
}
/**
* @brief 发送原始日志数据到底层输出通道。
* @param data 待发送数据指针。
* @param len 数据长度。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::send(const uint8_t *data, size_t len)
{
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) {
if (m_udpSock < 0) {
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));
if (ret >= 0) {
udp_ret = ESP_OK;
}
}
} 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;
static const char kUdpSkipMsg[] = "[W][LOG] UDP 发送已暂停: 堆内存不足\n";
ringPush(kUdpSkipMsg, 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();
}
if (level > 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
}
if (free_now < kDropInfoFree && level > DEBUG_LOG_WARN) {
return; // 无关模块:丢 INFO/DBG
}
} 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;
case DEBUG_LOG_WARN: level_str = "W"; break;
case DEBUG_LOG_INFO: level_str = "I"; break;
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) {
return;
}
if ((size_t)total >= sizeof(log_buf)) {
total = (int)sizeof(log_buf) - 1;
log_buf[total] = '\0';
}
send((const uint8_t *)log_buf, total);
}