ESP32 实战教程:做一个带 WebSocket 的远程日志系统

设备放在桌面上时,串口监视器是好朋友;设备装到墙里、屋顶或别人家里后,串口线就变成了哲学问题。远程日志系统的目标不是把所有变量都疯狂打印,而是让设备在网络在线时实时给出关键事件,网络离线时也能留下有限的现场记录。

ESP32 远程日志系统架构

一、为什么需要结构化日志

Serial.println("error") 在实验阶段很方便,但项目变大后会遇到三个问题:不知道错误来自哪个模块,不知道发生时间,也无法区分普通提示和严重故障。结构化日志至少包含等级、时间戳、模块名和消息,例如:

2026-09-30T10:12:04.512 [WARN] wifi: reconnect attempt=2
2026-09-30T10:12:05.101 [INFO] sensor: temperature=24.6
2026-09-30T10:12:08.900 [ERROR] sd: open failed path=/log/001.txt

日志不是越多越好。高频传感器的每次采样都写一行,会拖慢系统并迅速撑满文件。应该记录状态变化、错误、重连、配置更新和关键业务动作,把“正常重复的事情”用计数器或周期摘要表示。

二、统一日志宏和等级

先让业务代码不依赖具体输出方式。今天日志送串口,明天可能送 WebSocket、文件或 MQTT;如果到处散落 Serial.printf(),迁移会很痛苦。

enum LogLevel : uint8_t { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR };

const char* levelName(LogLevel level) {
  switch (level) {
    case LOG_DEBUG: return "DEBUG";
    case LOG_INFO: return "INFO";
    case LOG_WARN: return "WARN";
    default: return "ERROR";
  }
}

void logLine(LogLevel level, const char* module, const char* text) {
  uint32_t ms = millis();
  Serial.printf("[%lu] [%s] %s: %s\\n",
                (unsigned long)ms, levelName(level), module, text);
}

#define LOGI(module, text) logLine(LOG_INFO, module, text)
#define LOGW(module, text) logLine(LOG_WARN, module, text)
#define LOGE(module, text) logLine(LOG_ERROR, module, text)

宏只是入口,真正的系统还应该把格式化后的记录复制到队列。注意不要在中断服务函数里调用 String、文件写入或 WebSocket 发送;中断里只置位或投递轻量事件。

三、环形缓冲区避免日志拖垮业务

远程日志的降级策略

日志消费者可能暂时不在线,网络发送也可能阻塞。使用固定长度环形缓冲区,可以让业务模块快速把记录放进去;当缓冲区满时,丢弃最低等级的 DEBUG,保留 WARN 和 ERROR。

struct LogRecord {
  uint32_t ms;
  LogLevel level;
  char module[12];
  char message[96];
};

constexpr size_t LOG_CAP = 32;
LogRecord ring[LOG_CAP];
size_t head = 0;
size_t count = 0;

void pushLog(const LogRecord& record) {
  if (count == LOG_CAP) {
    if (record.level < LOG_WARN) return;
    // 丢掉最老的一条,为警告/错误腾位置
    head = (head + 1) % LOG_CAP;
    --count;
  }
  size_t index = (head + count) % LOG_CAP;
  ring[index] = record;
  ++count;
}

真实多任务项目里,head 和 count 要用互斥锁、临界区或 FreeRTOS 队列保护。环形缓冲区的容量按现场故障密度估算:既不能只放两条,也不要为了“万一”占掉大量 RAM。日志系统本身不应该成为系统内存最大的模块。

四、WebSocket 实时推送

已有 WebSocket 库或 Arduino WebServer 扩展通常可以提供连接回调和广播接口。日志任务从队列取出记录,序列化成短 JSON,再推送给浏览器。浏览器页面可以按等级过滤、暂停滚动、复制单条记录。

// 伪代码:不同 WebSocket 库的接口名称可能不同
void publishLog(const LogRecord& r) {
  String json = "{\"ms\":" + String(r.ms);
  json += ",\"level\":\"" + String(levelName(r.level)) + "\"";
  json += ",\"module\":\"" + String(r.module) + "\"";
  json += ",\"message\":\"" + escapeJson(r.message) + "\"}";
  websocket.broadcastTXT(json);
}

JSON 转义不能省略。错误消息中如果包含引号、换行或反斜杠,直接拼接会生成非法 JSON,浏览器随后只显示“解析失败”,真正的错误反而被隐藏。可以使用轻量 JSON 库,或写一个只处理这几个字符的转义函数。

远程日志页最好需要登录或一次性口令。日志里经常包含 IP、设备编号、文件路径甚至用户输入,不能因为它是调试页面就完全不设防。WebSocket 也应设置最大连接数和空闲超时,避免一个浏览器标签页开着就长期占用资源。

五、离线时写入 LittleFS

ESP32 远程日志系统架构

网络断开时,把 WARN/ERROR 写入 LittleFS,等网络恢复后再由用户下载。文件名可以按日期或启动次数分段,例如 /logs/boot-000123.log。不要每条日志都立即同步写闪存,频繁擦写会缩短寿命;可以在缓冲区积累几条后批量写,并在严重错误时强制 flush。

#include <LittleFS.h>

File logFile;

bool openLogFile() {
  if (!LittleFS.begin(true)) return false;
  logFile = LittleFS.open("/logs/current.log", FILE_APPEND);
  return (bool)logFile;
}

void appendOffline(const LogRecord& r) {
  if (!logFile) return;
  logFile.printf("[%lu] [%s] %s: %s\\n",
                 (unsigned long)r.ms, levelName(r.level),
                 r.module, r.message);
  if (r.level >= LOG_ERROR) logFile.flush();
}

文件要设置大小上限,例如 64 KB,超过后滚动到下一个编号。空间不足时删除最老的 DEBUG 日志,至少保留最近的 ERROR。错误处理也要记录“日志存储失败”,但不能因为写日志失败而让主业务重启,这就是典型的“调试功能不能影响被调试对象”。

六、时间戳和时钟来源

millis() 适合表示本次启动以来经过了多久,不能代表真实日期。网络在线后可以用 NTP 校时;离线启动则使用 RTC 或在日志中明确标记 boot_ms。混用两种时钟时,日志格式里最好同时保留序号和相对时间,避免重启后出现两个看似相同的时间点。

可以在每次启动记录固件版本、芯片型号、复位原因和剩余堆内存:

void logBoot() {
  Serial.printf("boot version=%s reset=%d heap=%u\\n",
                FW_VERSION, (int)esp_reset_reason(), ESP.getFreeHeap());
}

这些信息看似琐碎,发生“偶发重启”时却是最有价值的上下文。没有启动记录,远程日志往往只剩下一句“设备不在线”。

七、网络恢复和日志回放

网络重新连上后,不要瞬间把几百条离线日志全部推送出去,这会占满带宽并让浏览器失去响应。可以每秒发送 5 到 10 条,发送成功后再从文件中标记已处理位置。用户网页提供“下载全部”“只看错误”“清空已上传”等按钮,设备端则保留最小可回退文件。

如果设备很重要,日志上传接口应支持断点续传和服务端去重。每条记录带一个单调递增序号,服务器收到重复序号时只保留一份。重试时也要使用退避,避免网络故障期间设备集体同时重连,形成一场不必要的“日志流量高峰”。

八、隐私、安全与性能边界

日志里不要输出密码、完整令牌和个人隐私;调试版本可以输出更多信息,发布版本通过编译宏关闭 DEBUG。WebSocket 和下载接口应使用前面 HTTPS/TLS 文章中的安全连接思路,至少在管理网之外不要暴露无认证的日志页。

性能方面,日志格式化、JSON 序列化和文件写入都需要 CPU 和 RAM。高频采样任务只记录统计值,例如“过去 10 秒收到 16000 个样本,最大值 812”,不要把 16000 行原始数据塞进日志。日志系统要帮助你发现问题,而不是创造新的问题。

九、总结

远程日志的核心不是“把串口搬到浏览器”,而是为设备建立一条有等级、有限缓存、在线推送、离线留存和安全边界的诊断链路。统一入口让业务模块不关心输出渠道,环形缓冲区保护实时任务,LittleFS 负责离线证据,WebSocket 负责现场观察。

当 ESP32 被安装到远处后,日志就是你和设备之间的对话。把这条对话设计好,很多曾经只能“重新烧录试试”的问题,就能变成可复现、可定位、可修复的工程问题。

参考资料

打赏

如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏码

打赏支持:王庆岩

posted on 2026-09-30 10:18  潇洒石头  阅读(11)  评论(0)    收藏  举报

导航