Files
ESP32_COMM/main/monitor/debug_log.h
T
小龙虾 2138a572ee feat(esp32): 添加多电机OTA中继功能并优化调试日志系统
- 新增多电机状态、版本和OTA中继监控模块(monitor/motor_state.cpp,
  monitor/motor_version.cpp, monitor/motor_ota_relay.cpp)
- 在CAN反馈任务中添加0xC5/0xC6/0xD2类型消息处理,支持多电机状态推送
- 重构调试日志系统,将一次性JSON分配改为512B栈缓冲流式发送,降低内存峰值
- 调试UART波特率从115200提升至30000
- 修改版本管理方式,从自动生version.h改为手动维护
- 更新VSCode设置以支持ESP-IDF开发环境配置
- 在OTA协调器中添加电机OTA互斥机制,防止并发冲突
- 完善中继状态机异常处理,添加远程取消通知机制
2026-07-26 20:40:16 +08:00

174 lines
7.0 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.h - 调试日志输出
*
* 当前日志会并行写入:
* - UART0 串口
* - UDP 单播(独立调试接收端可选)
* - 环形缓冲区,供 HTTP /logs 拉取
*
* 注意:当前 tools/WebUI 日志面板走的是 HTTP /logs,
* 不是直接监听 UDP 6666。
*
* UART0: TX=GPIO21, RX=GPIO20, 3000000 波特率
*
* 线程模型:DEBUG_* / log() / send() 仅允许在任务上下文调用
* (内部使用 portMAX_DELAY 互斥锁,禁止 ISR)。
*/
#ifndef DEBUG_LOG_H
#define DEBUG_LOG_H
#include <stdint.h>
#include <stdarg.h>
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "app_config.h"
#include <sys/socket.h>
#include <netinet/in.h>
// 调试日志配置(DEBUG_LOG_IP/PORT 在 app_config.h)
// UART0 调试配置
#define UART_DEBUG_TX_GPIO 21 ///< UART0 TX GPIO 引脚号
#define UART_DEBUG_RX_GPIO 20 ///< UART0 RX GPIO 引脚号
#define UART_DEBUG_BAUD_RATE 3000000 ///< UART0 波特率
// 日志级别
typedef enum {
DEBUG_LOG_ERR = 1, // 错误
DEBUG_LOG_WARN = 2, // 警告
DEBUG_LOG_INFO = 3, // 信息
DEBUG_LOG_DBG = 4 // 调试
} debug_log_level_t;
/**
* @brief /logs 流式发送 sink:每个 chunk 回调一次;返回 false 表示发送失败需中止。
* @param ctx 透传上下文(如 httpd_req_t*)。
*/
typedef bool (*LogJsonSink)(void *ctx, const char *data, int len);
/**
* @brief 调试日志类(单例)
*
* 封装 UART0、UDP 和环形缓冲区三路日志输出通道,
* 所有内部状态(socket、缓冲区、互斥锁等)均为私有成员。
*/
class DebugLog {
public:
static DebugLog& instance();
/** @brief 初始化日志系统(UART + 开机大环;UDP socket 延迟创建)。 */
esp_err_t init();
/**
* @brief 启动 burst 结束后把环从开机容量缩到稳态小环,释放大块堆。
* 幂等;失败则保留大环。宜在 WiFi/HTTP 就绪后、日志变少时调用。
*/
void shrinkRingAfterBoot();
/** @brief 初始化 UART0 调试串口。 */
esp_err_t uartInit();
/** @brief 通过 UART0 直接发送一段原始字节数据。 */
esp_err_t uartSend(const uint8_t *data, size_t len);
/** @brief 发送原始日志数据到底层输出通道(任务上下文)。 */
esp_err_t send(const uint8_t *data, size_t len);
/** @brief 按日志级别与标签格式化输出一条日志(任务上下文)。 */
void log(debug_log_level_t level, const char *tag, const char *fmt, ...);
/** @brief 获取当前环形日志缓冲区中的日志条数。 */
int ringCount();
/** @brief 弹出一条最旧日志并从环形缓冲区移除。 */
bool ringPopOldest(char *out, size_t out_size, int *out_len);
/**
* @brief 将环形日志以 chunked JSON 数组流式发送,消除一次性大块 malloc。
* 按条 pop(短持锁),512B 栈缓冲攒满即经 sink 发一个 chunk;
* 峰值从原 ~4KB malloc 降为栈缓冲 + 小 chunk。
* @param sink 每个 chunk 的发送回调;返回 false 表示发送失败。
* @param ctx 透传给 sink 的上下文。
* @return 1=已流式发送完整数组(调用方需补发终止 chunk);
* 0=无日志/堆门控(调用方一次性发 "[]");-1=中途发送失败。
*/
int ringDrainJsonStream(LogJsonSink sink, void *ctx);
void setLogLevel(debug_log_level_t level) { m_logLevel = level; }
debug_log_level_t getLogLevel() const { return m_logLevel; }
private:
DebugLog() = default;
~DebugLog() = default;
DebugLog(const DebugLog&) = delete;
DebugLog& operator=(const DebugLog&) = delete;
int m_udpSock = -1;
struct sockaddr_in m_debugAddr = {};
bool m_initialized = false;
bool m_uartInitialized = false;
/** uartInit 成功后才为 true;send() 据此跳过 uart_write_bytes */
bool m_uartAvailable = false;
debug_log_level_t m_logLevel = DEBUG_LOG_INFO;
/* 环形日志(堆上分配,非 .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
* free < kDropWarnFree → 全局丢 DBG
* free < kSkipUdpFree → 不 sendto(仍 UART+ring)
* free < kDrainMinFree → /logs 返 "[]"(低堆时连流式小 chunk 的 pbuf 也省)
* 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;
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 int jsonEscapeAppend(char *buf, int pos, int capacity, const char *src, int src_len);
};
// 便捷宏 — 直接调用单例方法(禁止在 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