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>
This commit is contained in:
183
src/wake_log.cpp
Normal file
183
src/wake_log.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#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();
|
||||
}
|
||||
Reference in New Issue
Block a user