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
+8 -43
View File
@@ -14,7 +14,6 @@ using Row = SettingsMenu::Row;
void SettingsApp::onEnter() {
page_ = Page::Menu;
eraseDialog_.reset();
list_.setCount(menu_.count());
}
@@ -28,7 +27,9 @@ bool SettingsApp::onKey(const KeyEvent& e) {
case Page::Menu: return onMenuKey(e);
case Page::Text: return onTextKey(e);
case Page::Choice: return onChoiceKey(e);
case Page::Storage: return onStorageKey(e);
case Page::Storage:
if (!storagePage_.onKey(e)) page_ = Page::Menu;
return true;
case Page::About: return onAboutKey(e);
case Page::Wifi:
if (!wifiPage_.onKey(e)) page_ = Page::Menu;
@@ -66,7 +67,10 @@ bool SettingsApp::onMenuKey(const KeyEvent& e) {
case Kind::Slider: break;
case Kind::Page:
switch (menu_.row(i)) {
case Row::Storage: page_ = Page::Storage; break;
case Row::Storage:
page_ = Page::Storage;
storagePage_.enter();
break;
case Row::Wifi:
page_ = Page::Wifi;
wifiPage_.enter();
@@ -112,21 +116,6 @@ bool SettingsApp::onChoiceKey(const KeyEvent& e) {
return true;
}
bool SettingsApp::onStorageKey(const KeyEvent& e) {
if (eraseDialog_) {
eraseDialog_->onKey(e);
if (eraseDialog_->result() == 1 && !d_.storage.requestFormat()) warn("A format is already running");
if (eraseDialog_->result() != DialogModel::kPending) eraseDialog_.reset();
return true;
}
if (e.key == Key::Select) {
if (d_.storage.state().present) eraseDialog_.reset(new DialogModel({"Cancel", "Erase"}));
else warn("No SD card");
}
if (e.key == Key::Back) page_ = Page::Menu;
return true;
}
bool SettingsApp::onAboutKey(const KeyEvent& e) {
if (e.key == Key::Back) page_ = Page::Menu;
if (e.key == Key::Char && e.ch == 'w') d_.apps.open("demo"); // hidden widget demo
@@ -142,25 +131,6 @@ void SettingsApp::update(uint32_t nowMs) {
}
}
std::vector<std::string> SettingsApp::storageLines() const {
auto s = d_.storage.state();
std::vector<std::string> lines;
char buf[64];
if (d_.storage.formatting()) {
lines.push_back("Erasing the SD card...");
} else if (!s.present) {
lines.push_back("No SD card inserted.");
} else {
snprintf(buf, sizeof(buf), "SD card: %u MB, %d%% used", static_cast<unsigned>(s.totalBytes >> 20), s.usedPercent);
lines.push_back(buf);
lines.push_back(s.logsAllowed ? "Logs: recording" : "Logs: paused (over 90%)");
lines.push_back(s.capturesAllowed ? "Captures: allowed" : "Captures: stopped (card full)");
}
lines.push_back("");
lines.push_back("Enter: erase SD card");
return lines;
}
std::vector<std::string> SettingsApp::aboutLines() const {
std::vector<std::string> lines;
char buf[64];
@@ -210,12 +180,7 @@ void SettingsApp::draw(Canvas& c) {
widgets::list(c, choices_, area, [&](int i) { return (i == current ? "* " : " ") + options[i]; });
break;
}
case Page::Storage:
widgets::textLines(c, storageLines(), 0, area);
if (eraseDialog_)
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
*eraseDialog_);
break;
case Page::Storage: storagePage_.draw(c); break;
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
case Page::Wifi: wifiPage_.draw(c); break;
}