Files
immich-frame/src/wake_log.cpp
cottongin 7fa74c03c1 feat: add deep sleep batch caching, wake log, and log viewer tools
- 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>
2026-08-31 12:59:03 -04:00

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();
}