- 将日志环形缓冲区从静态数组改为动态堆分配
- 实现启动时大容量缓冲区和运行时小容量缓冲区的切换机制
- 添加日志环满时优先保留错误和警告信息的清理策略
- 优化内存使用,在日志为空时避免不必要的内存分配
- 移除调试日志中的冗余字段显示
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模式配置位置
824 lines
24 KiB
C++
824 lines
24 KiB
C++
/*
|
||
* 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 未获 IP 时跳过 sendto;日志仍写 UART 与 HTTP ring
|
||
*
|
||
* 线程:仅任务上下文;禁止 ISR 调用 DEBUG_*(内部 portMAX_DELAY mutex)。
|
||
*/
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <unistd.h>
|
||
#include <sys/socket.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;
|
||
|
||
// ============================================================
|
||
// 单例 / 工具
|
||
// ============================================================
|
||
DebugLog& DebugLog::instance()
|
||
{
|
||
static DebugLog inst;
|
||
return inst;
|
||
}
|
||
|
||
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)
|
||
{
|
||
/* 返回 nullptr 让 caller 走"[]"字面量分支,避免空环时每秒 malloc(3)/free(3)。
|
||
* ringDrainJson 的所有"返空"路径都调本函数,caller logs_handler 见 nullptr
|
||
* 后直接发 "[]" 字面量,零分配。 */
|
||
if (out_len) {
|
||
*out_len = 0;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
/**
|
||
* 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)
|
||
// ============================================================
|
||
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) {
|
||
return 0;
|
||
}
|
||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||
const int count = m_logRingCount;
|
||
xSemaphoreGive(m_logRingMutex);
|
||
return count;
|
||
}
|
||
|
||
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 || !m_logRing || m_logRingSlots <= 0) {
|
||
return false;
|
||
}
|
||
|
||
const int idx = (m_logRingHead - m_logRingCount + m_logRingSlots) % m_logRingSlots;
|
||
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, ringSlot(idx), (size_t)copy_len);
|
||
}
|
||
out[copy_len] = '\0';
|
||
if (out_len) {
|
||
*out_len = copy_len;
|
||
}
|
||
|
||
m_logRingLens[idx] = 0;
|
||
m_logRingCount--;
|
||
return true;
|
||
}
|
||
|
||
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);
|
||
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;
|
||
}
|
||
if (!m_logRing || m_logRingSlots <= 0) {
|
||
return 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 + m_logRingSlots) % m_logRingSlots;
|
||
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 || !m_logRing || !m_logRingLens || m_logRingSlots <= 0) {
|
||
return;
|
||
}
|
||
|
||
int copy_len = len;
|
||
if (copy_len >= LOG_RING_SIZE) {
|
||
copy_len = LOG_RING_SIZE - 1;
|
||
}
|
||
|
||
/* 满环时优先覆盖 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) % m_logRingSlots;
|
||
if (m_logRingCount < m_logRingSlots) {
|
||
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;
|
||
|
||
const uint32_t free_now = esp_get_free_heap_size();
|
||
if (free_now < kDrainMinFree || !m_logRingMutex) {
|
||
return allocEmptyJson(out_len);
|
||
}
|
||
|
||
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);
|
||
|
||
/* 堆不够则减条数重估(仍持锁,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);
|
||
}
|
||
|
||
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];
|
||
|
||
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) {
|
||
buf[pos++] = ',';
|
||
}
|
||
first = false;
|
||
buf[pos++] = '"';
|
||
pos = jsonEscapeAppend(buf, pos, capacity, entry, entry_len);
|
||
if (pos < capacity - 1) {
|
||
buf[pos++] = '"';
|
||
} else if (pos < capacity) {
|
||
buf[pos - 1] = '"'; /* 极端:覆盖末字节闭合 */
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ============================================================
|
||
// UART
|
||
// ============================================================
|
||
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) {
|
||
/* 可能 console 已占用 UART0——降级为 ring-only */
|
||
ESP_LOGW(TAG, "UART0 驱动安装失败(可能 console 占用): %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;
|
||
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;
|
||
}
|
||
|
||
esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
|
||
{
|
||
if (!m_uartInitialized || !m_uartAvailable) {
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
const int ret = uart_write_bytes(UART_NUM_0, data, len);
|
||
if (ret < 0) {
|
||
return ESP_FAIL;
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
// ============================================================
|
||
// 初始化 / UDP
|
||
// ============================================================
|
||
esp_err_t DebugLog::init()
|
||
{
|
||
if (m_initialized) {
|
||
return ESP_OK;
|
||
}
|
||
|
||
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();
|
||
if (!m_logRingMutex) {
|
||
ESP_LOGE(TAG, "ring mutex 创建失败,HTTP /logs 将不可用");
|
||
/* 仍标记 initialized,避免 log() 反复 init;UART 路径可工作 */
|
||
m_initialized = true;
|
||
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);
|
||
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;
|
||
}
|
||
|
||
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);
|
||
|
||
const int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||
if (s < 0) {
|
||
ESP_LOGE(TAG, "创建 UDP socket 失败 errno=%d", errno);
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
/* 避免 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;
|
||
}
|
||
|
||
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();
|
||
|
||
if (m_uartAvailable) {
|
||
uart_ret = uartSend(data, len);
|
||
}
|
||
|
||
/* UDP:堆紧 / 无 IP 时跳过 sendto */
|
||
if (m_initialized && free_now >= kSkipUdpFree && udpNetifHasIp()) {
|
||
if (m_udpSock < 0) {
|
||
(void)ensureUdpSocket();
|
||
}
|
||
|
||
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 && 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, (int)sizeof(kUdpSkipMsg) - 1);
|
||
}
|
||
}
|
||
|
||
ringPush((const char *)data, (int)len);
|
||
|
||
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
|
||
return ESP_OK;
|
||
}
|
||
if (m_logRingMutex) {
|
||
return ESP_OK;
|
||
}
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...)
|
||
{
|
||
if (!m_initialized) {
|
||
(void)init();
|
||
}
|
||
|
||
if (level > m_logLevel) {
|
||
return;
|
||
}
|
||
|
||
const uint32_t free_now = esp_get_free_heap_size();
|
||
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 (!keep) {
|
||
if (free_now < kDropInfoFree) {
|
||
return;
|
||
}
|
||
} else if (level >= DEBUG_LOG_DBG && free_now < kDropInfoFree) {
|
||
/* keep-alive DBG:中等堆紧也丢,减刷屏 */
|
||
return;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* 单缓冲:与 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)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;
|
||
}
|
||
|
||
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);
|
||
}
|