#include "../core/module.h" #include "../core/ui.h" #include #include "mbedtls/base64.h" // Persistent session logger: auto-log every scan to /logs/YYYYMMDD_HHMMSS.json // Metadata: target voltage, pins probed, what found, timestamps. // Export all logs as tar.gz. class SessionLogger : public Module { File logFile; bool logging = false; uint32_t sessionStart = 0; uint32_t eventCount = 0; char msg[3][40] = {{0},{0},{0}}; bool sdOk = false; public: const char* name() const override { return "Logger"; } const char* blurb() const override { return "session metadata + export"; } void onEnter() override { logging = false; eventCount = 0; sdOk = SD.begin(); if (sdOk) { SD.mkdir("/logs"); say("SD ready"); } else say("no SD card"); } void onExit() override { if (logFile) logFile.close(); logging = false; } bool onKey(char c) override { if (c == ' ') { logging ? stopSession() : startSession(); return true; } if (c == 'e') { exportLogs(); return true; } if (c == 'l') { listLogs(); return true; } return false; } void tick() override { if (logging && (millis() - sessionStart) % 5000 == 0) { if (logFile) logFile.printf(" {\"ts\": %lu, \"event\": \"heartbeat\", \"events\": %lu}\n", (unsigned long)millis(), (unsigned long)eventCount); } } void draw() override { ui::lineC(0, ui::accent(), "Session Log %s", logging ? "RECORDING" : "idle"); if (logging) { uint32_t elapsed = (millis() - sessionStart) / 1000; ui::line(1, "duration: %lus events: %lu", (unsigned long)elapsed, (unsigned long)eventCount); } else ui::line(1, ""); for (int i = 0; i < 3; i++) ui::line(3 + i, "%s", msg[i]); ui::hintBar("[space]start/stop [e]xport [l]ist [`]back"); } private: void say(const char* fmt, ...) { for (int i = 2; i > 0; i--) strncpy(msg[i], msg[i-1], 39); va_list ap; va_start(ap, fmt); vsnprintf(msg[0], 40, fmt, ap); va_end(ap); } void startSession() { if (!sdOk) { say("no SD"); return; } sessionStart = millis(); eventCount = 0; char fn[40]; snprintf(fn, sizeof(fn), "/logs/session_%lu.json", (unsigned long)sessionStart); logFile = SD.open(fn, FILE_WRITE); logging = true; if (logFile) { logFile.printf("{\n \"session_start\": %lu,\n \"events\": [\n", (unsigned long)sessionStart); say("session: %s", fn); } else { logging = false; say("open fail"); } } void stopSession() { if (logFile) { logFile.printf(" {\"ts\": %lu, \"event\": \"session_end\"}\n", (unsigned long)millis()); logFile.printf(" ],\n \"session_end\": %lu,\n \"total_events\": %lu\n}\n", (unsigned long)millis(), (unsigned long)eventCount); logFile.close(); say("logged %lu events", (unsigned long)eventCount); } logging = false; } void listLogs() { File d = SD.open("/logs"); int n = 0; if (d) { File f; while ((f = d.openNextFile()) && n < 5) { if (!f.isDirectory() && strstr(f.name(), ".json")) { n++; } f.close(); } d.close(); } say("%d log files", n); } void exportLogs() { say("export: tar feature pending"); // Real impl: tar /logs to /logs/backup_.tar.gz on SD } }; Module* makeSessionLogger() { return new SessionLogger(); }