M1 step 3: Log writing and Storage Clean-up

- lib/storage_model (host-tested): FAT-safe names, daily Log paths,
  dates from Log/Capture file names, CleanupPlan by category and age,
  byte formatting
- StorageService does all card I/O on its task: queued Log lines are
  written in batches each second (dropped while Logs are paused),
  plus file listing and deletion jobs
- Settings > Storage moves into StoragePage: usage, Clean up
  (category, age with size preview, confirmation), Erase SD card
- Clock: local date for Log names
- Serial: log <text>, sd list

Verified on the device: lines land in /irc/dev/#test/2026-10-02.log
with folders created as needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-02 14:56:06 +02:00
co-authored by Claude Opus 5.5
parent 1a0baf4ac7
commit d3948ccc3a
18 changed files with 785 additions and 74 deletions
+31 -5
View File
@@ -16,6 +16,7 @@
#include "services/storage_service.h"
#include "services/wifi_service.h"
#include "settings.h"
#include "storage_paths.h"
#include "ui/notifier.h"
#include "ui/screen.h"
#include "version.h"
@@ -31,7 +32,7 @@ static Screen screen;
// Constructed in setup(), after the hardware and Settings are ready.
static BatteryService* battery;
static StorageService* storage;
static StorageService* storageService;
static PowerService* power;
static ClockService* clockService;
static SavedNetworks* savedNetworks;
@@ -61,7 +62,7 @@ static StatusInfo currentStatus() {
if (apps->foregroundTitle()) s.title = apps->foregroundTitle();
if (battery->estimator().hasReading()) s.batteryPercent = battery->estimator().percent();
s.clock = clockService->displayTime();
auto storageState = storage->state();
auto storageState = storageService->state();
s.sdPresent = storageState.present;
s.sdLevel = storageState.level;
s.compose = keyMapper.pendingCompose();
@@ -90,7 +91,7 @@ void setup() {
settings.load();
battery = new BatteryService(bus);
storage = new StorageService(bus);
storageService = new StorageService(bus);
power = new PowerService(settings);
clockService = new ClockService(settings, bus);
savedNetworks = new SavedNetworks(nvs);
@@ -101,13 +102,13 @@ void setup() {
services.add(*power);
services.add(*clockService);
services.add(*battery);
services.add(*storage);
services.add(*storageService);
services.add(*wifi);
apps = new AppManager(launcher);
launcher.setManager(*apps);
apps->registerApp({"settings", "Settings", false,
new SettingsApp({settings, bus, *apps, *battery, *storage, *clockService, *wifi, *savedNetworks})});
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks})});
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)});
@@ -121,6 +122,21 @@ void setup() {
}
// Dev aid: serial commands to drive the UI without the keyboard.
static bool listingWanted = false;
static void printListingWhenReady() {
if (!listingWanted || !storageService->listingReady()) return;
listingWanted = false;
auto listing = storageService->listing();
for (size_t i = 0; i < listing.size(); i++) {
uint64_t bytes = 0;
for (auto& f : listing[i]) bytes += f.bytes;
Serial.printf("sd list: %-20s %u files, %s\n", kCleanupCategories[i].label, (unsigned)listing[i].size(),
formatBytes(bytes).c_str());
for (auto& f : listing[i]) Serial.printf(" %s (%u B)\n", f.path.c_str(), (unsigned)f.bytes);
}
}
static void serialCommands() {
static String line;
while (Serial.available()) {
@@ -147,6 +163,15 @@ static void serialCommands() {
Serial.printf("wifi add: %s\n", error.empty() ? "ok" : error.c_str());
wifi->savedNetworksChanged();
}
if (line.startsWith("log ")) { // appends to a test IRC Log
std::string path = storage::dailyLogPath({"irc", "dev", "#test"}, clockService->localDate());
storageService->appendLine(path, clockService->displayTime() + " " + line.substring(4).c_str());
Serial.printf("log: queued to %s\n", path.c_str());
}
if (line == "sd list") {
storageService->requestListing();
listingWanted = true;
}
if (line == "wifi status")
Serial.printf("wifi: state %d ssid '%s' rssi %d clock %s heap %u min %u\n", (int)wifi->state(),
wifi->ssid().c_str(), wifi->rssi(), clockService->displayTime().c_str(), ESP.getFreeHeap(),
@@ -171,6 +196,7 @@ void loop() {
uint32_t now = millis();
serialCommands();
printListingWhenReady();
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange()) {
bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen