- Batch-fetch asset IDs in deep sleep mode with NVS caching to reduce network calls and improve battery life (configurable batch size 1-50) - Add wake_log module: persistent CSV log of each photo cycle surviving reboots and deep sleep, with download/clear via web UI - Add browser-based log viewer and log comparison tools (tools/) - Update .gitignore to exclude .cursor/, chat-summaries/, wake_*.csv, and .DS_Store Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
5.6 KiB
C++
184 lines
5.6 KiB
C++
#include "wake_log.h"
|
|
#include <LittleFS.h>
|
|
#include <Preferences.h>
|
|
#include <esp_heap_caps.h>
|
|
#include "time_utils.h"
|
|
|
|
static const char* LOG_PATH = "/wake_log.csv";
|
|
static const char* NVS_LOG_NS = "wake_log";
|
|
static constexpr size_t MAX_LOG_SIZE = 200 * 1024; // 200KB cap
|
|
static constexpr size_t KEEP_SIZE = 100 * 1024; // Keep last 100KB on truncation
|
|
|
|
static bool _logReady = false;
|
|
static uint32_t _bootCount = 0;
|
|
static bool _rtcValidAtBoot = false;
|
|
|
|
static const char* resultStr(WakeLogResult r) {
|
|
switch (r) {
|
|
case WakeLogResult::Ok: return "ok";
|
|
case WakeLogResult::FailFetch: return "fail_fetch";
|
|
case WakeLogResult::FailDownload: return "fail_download";
|
|
case WakeLogResult::FailProcess: return "fail_process";
|
|
default: {
|
|
WakeLogResult unreachable = r;
|
|
(void)unreachable;
|
|
return "unknown";
|
|
}
|
|
}
|
|
}
|
|
|
|
void wakeLogBegin() {
|
|
// Record RTC validity before anything else might change the clock
|
|
_rtcValidAtBoot = hasValidTime();
|
|
|
|
// Mount LittleFS (idempotent — safe to call if already mounted by web server)
|
|
if (!LittleFS.begin(true)) {
|
|
Serial.println("[wake_log] LittleFS mount failed — logging disabled");
|
|
_logReady = false;
|
|
return;
|
|
}
|
|
|
|
// Increment boot counter in NVS (fire-and-forget)
|
|
Preferences prefs;
|
|
if (prefs.begin(NVS_LOG_NS, false)) {
|
|
_bootCount = prefs.getUInt("boot_cnt", 0) + 1;
|
|
prefs.putUInt("boot_cnt", _bootCount);
|
|
prefs.end();
|
|
} else {
|
|
Serial.println("[wake_log] NVS open failed — using boot_count=0");
|
|
_bootCount = 0;
|
|
}
|
|
|
|
_logReady = true;
|
|
Serial.printf("[wake_log] Ready (boot #%u, rtc_valid=%d)\n",
|
|
_bootCount, _rtcValidAtBoot ? 1 : 0);
|
|
}
|
|
|
|
// Truncate log file to keep only the last KEEP_SIZE bytes.
|
|
// Uses PSRAM buffer. Skips silently on failure.
|
|
static void truncateIfNeeded() {
|
|
File f = LittleFS.open(LOG_PATH, "r");
|
|
if (!f) return;
|
|
|
|
size_t fileSize = f.size();
|
|
if (fileSize <= MAX_LOG_SIZE) {
|
|
f.close();
|
|
return;
|
|
}
|
|
|
|
Serial.printf("[wake_log] Log at %uKB — truncating to last %uKB\n",
|
|
(unsigned)(fileSize / 1024), (unsigned)(KEEP_SIZE / 1024));
|
|
|
|
// Allocate buffer in PSRAM
|
|
uint8_t* buf = (uint8_t*)ps_malloc(KEEP_SIZE);
|
|
if (!buf) {
|
|
Serial.println("[wake_log] PSRAM alloc failed — skipping truncation");
|
|
f.close();
|
|
return;
|
|
}
|
|
|
|
// Seek to (fileSize - KEEP_SIZE) and read the tail
|
|
f.seek(fileSize - KEEP_SIZE);
|
|
size_t bytesRead = f.read(buf, KEEP_SIZE);
|
|
f.close();
|
|
|
|
if (bytesRead == 0) {
|
|
free(buf);
|
|
return;
|
|
}
|
|
|
|
// Find first newline to avoid a partial line at the start
|
|
size_t start = 0;
|
|
for (size_t i = 0; i < bytesRead; i++) {
|
|
if (buf[i] == '\n') {
|
|
start = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Rewrite the file with just the tail
|
|
File out = LittleFS.open(LOG_PATH, "w");
|
|
if (!out) {
|
|
Serial.println("[wake_log] File rewrite failed — skipping truncation");
|
|
free(buf);
|
|
return;
|
|
}
|
|
|
|
out.write(buf + start, bytesRead - start);
|
|
out.close();
|
|
free(buf);
|
|
|
|
Serial.printf("[wake_log] Truncated to %uKB\n",
|
|
(unsigned)((bytesRead - start) / 1024));
|
|
}
|
|
|
|
void wakeLogAppend(uint8_t battPct, int wifiRssi, uint32_t wifiMs,
|
|
const String& assetId, const String& photoDate,
|
|
const String& filter, WakeLogResult result, bool isDeepSleep) {
|
|
if (!_logReady) return;
|
|
|
|
// Read error tracking from NVS (defaults on failure)
|
|
uint32_t errorsTotal = 0;
|
|
String lastErrorTs = "";
|
|
|
|
Preferences prefs;
|
|
if (prefs.begin(NVS_LOG_NS, false)) {
|
|
errorsTotal = prefs.getUInt("err_total", 0);
|
|
lastErrorTs = prefs.getString("err_last_ts", "");
|
|
|
|
// Update error counters if this is a failure
|
|
if (result != WakeLogResult::Ok) {
|
|
errorsTotal++;
|
|
lastErrorTs = hasValidTime() ? isoNow() : "unknown";
|
|
prefs.putUInt("err_total", errorsTotal);
|
|
prefs.putString("err_last_ts", lastErrorTs);
|
|
}
|
|
prefs.end();
|
|
} else {
|
|
Serial.println("[wake_log] NVS read failed — using defaults");
|
|
}
|
|
|
|
// Build CSV line
|
|
uint32_t cycleMs = millis();
|
|
uint32_t freePsramKb = (uint32_t)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024);
|
|
uint32_t freeHeapKb = (uint32_t)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024);
|
|
String timestamp = hasValidTime() ? isoNow() : "no_clock";
|
|
|
|
char line[512];
|
|
snprintf(line, sizeof(line),
|
|
"%s,%u,%u,%u,%d,%u,%u,%d,%u,%s,%s,%s,%s,%s,%s,%u\n",
|
|
timestamp.c_str(),
|
|
(unsigned)battPct,
|
|
(unsigned)freePsramKb,
|
|
(unsigned)freeHeapKb,
|
|
wifiRssi,
|
|
(unsigned)wifiMs,
|
|
(unsigned)cycleMs,
|
|
_rtcValidAtBoot ? 1 : 0,
|
|
(unsigned)_bootCount,
|
|
filter.c_str(),
|
|
assetId.c_str(),
|
|
photoDate.c_str(),
|
|
resultStr(result),
|
|
isDeepSleep ? "sleep" : "normal",
|
|
lastErrorTs.c_str(),
|
|
(unsigned)errorsTotal);
|
|
|
|
// Append to log file
|
|
File f = LittleFS.open(LOG_PATH, "a");
|
|
if (!f) {
|
|
Serial.println("[wake_log] File open failed — skipping log entry");
|
|
return;
|
|
}
|
|
|
|
f.print(line);
|
|
f.close();
|
|
|
|
Serial.printf("[wake_log] Logged: %s %s (%s)\n",
|
|
resultStr(result), assetId.c_str(),
|
|
isDeepSleep ? "sleep" : "normal");
|
|
|
|
// Check if truncation is needed
|
|
truncateIfNeeded();
|
|
}
|