Initial public release
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#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';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user