141 lines
3.9 KiB
C++
141 lines
3.9 KiB
C++
#pragma once
|
|
|
|
#include <LittleFS.h>
|
|
#include "config.h"
|
|
|
|
// Rolling debug log stored in LittleFS at /debug.log
|
|
// Format: one JSON line per entry: {"t":<millis>,"m":"message"}
|
|
// Max MAX_LOG_ENTRIES entries; oldest are trimmed on save.
|
|
|
|
#define LOG_FILE "/debug.log"
|
|
#define MAX_LOG_ENTRIES 250
|
|
#define MAX_LOG_MSG 128
|
|
|
|
class DebugLog {
|
|
public:
|
|
void begin() {
|
|
_entryCount = 0;
|
|
if (LittleFS.exists(LOG_FILE)) {
|
|
File f = LittleFS.open(LOG_FILE, "r");
|
|
if (f) {
|
|
while (f.available()) {
|
|
String line = f.readStringUntil('\n');
|
|
line.trim();
|
|
if (line.length() > 0) _entryCount++;
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
void log(const char* msg) {
|
|
// Mirror to serial for live debugging
|
|
Serial.printf("[LOG] %s\n", msg);
|
|
|
|
if (_entryCount >= MAX_LOG_ENTRIES) {
|
|
trimLog();
|
|
}
|
|
|
|
File f = LittleFS.open(LOG_FILE, "a");
|
|
if (!f) return;
|
|
|
|
char escaped[MAX_LOG_MSG * 2];
|
|
escapeJson(msg, escaped, sizeof(escaped));
|
|
|
|
f.printf("{\"t\":%lu,\"m\":\"%s\"}\n", millis(), escaped);
|
|
f.close();
|
|
_entryCount++;
|
|
}
|
|
|
|
void logf(const char* fmt, ...) {
|
|
char buf[MAX_LOG_MSG];
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
va_end(args);
|
|
log(buf);
|
|
}
|
|
|
|
// Send the entire log as a JSON array over serial (handles "get_log" cmd).
|
|
void sendOverSerial() {
|
|
Serial.print("{\"rsp\":\"log\",\"entries\":[");
|
|
Serial.flush();
|
|
|
|
if (LittleFS.exists(LOG_FILE)) {
|
|
File f = LittleFS.open(LOG_FILE, "r");
|
|
if (f) {
|
|
bool first = true;
|
|
while (f.available()) {
|
|
String line = f.readStringUntil('\n');
|
|
line.trim();
|
|
if (line.length() == 0) continue;
|
|
if (!first) Serial.print(",");
|
|
Serial.print(line);
|
|
first = false;
|
|
Serial.flush();
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
|
|
Serial.println("]}");
|
|
Serial.flush();
|
|
}
|
|
|
|
void clear() {
|
|
LittleFS.remove(LOG_FILE);
|
|
_entryCount = 0;
|
|
}
|
|
|
|
private:
|
|
int _entryCount = 0;
|
|
|
|
// Drop the oldest half of the log when MAX_LOG_ENTRIES is hit.
|
|
void trimLog() {
|
|
File f = LittleFS.open(LOG_FILE, "r");
|
|
if (!f) return;
|
|
|
|
String lines[MAX_LOG_ENTRIES];
|
|
int count = 0;
|
|
while (f.available() && count < MAX_LOG_ENTRIES) {
|
|
String line = f.readStringUntil('\n');
|
|
line.trim();
|
|
if (line.length() > 0) {
|
|
lines[count++] = line;
|
|
}
|
|
}
|
|
f.close();
|
|
|
|
int keepFrom = count / 2;
|
|
File out = LittleFS.open(LOG_FILE, "w");
|
|
if (!out) return;
|
|
for (int i = keepFrom; i < count; i++) {
|
|
out.println(lines[i]);
|
|
}
|
|
out.close();
|
|
_entryCount = count - keepFrom;
|
|
}
|
|
|
|
void escapeJson(const char* input, char* output, size_t maxLen) {
|
|
size_t o = 0;
|
|
for (const char* p = input; *p && o < maxLen - 7; p++) {
|
|
unsigned char c = (unsigned char)*p;
|
|
if (c == '"' || c == '\\') {
|
|
output[o++] = '\\';
|
|
output[o++] = c;
|
|
} else if (c == '\n') {
|
|
output[o++] = '\\'; output[o++] = 'n';
|
|
} else if (c == '\r') {
|
|
output[o++] = '\\'; output[o++] = 'r';
|
|
} else if (c == '\t') {
|
|
output[o++] = '\\'; output[o++] = 't';
|
|
} else if (c < 0x20) {
|
|
o += snprintf(output + o, maxLen - o, "\\u%04x", c);
|
|
} else {
|
|
output[o++] = c;
|
|
}
|
|
}
|
|
output[o] = '\0';
|
|
}
|
|
};
|