Public Access
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:
@@ -0,0 +1,171 @@
|
||||
#include "storage_page.h"
|
||||
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
constexpr int kAges = sizeof(kCleanupAgeLabels) / sizeof(kCleanupAgeLabels[0]);
|
||||
constexpr int kCategories = sizeof(kCleanupCategories) / sizeof(kCleanupCategories[0]);
|
||||
} // namespace
|
||||
|
||||
void StoragePage::enter() {
|
||||
view_ = View::Main;
|
||||
dialog_.reset();
|
||||
main_.setCount(kRows);
|
||||
main_.select(kCleanUp);
|
||||
}
|
||||
|
||||
void StoragePage::warn(const char* text) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text, static_cast<int32_t>(NotificationLevel::Warning)));
|
||||
}
|
||||
|
||||
uint64_t StoragePage::categoryBytes(int category) const {
|
||||
uint64_t sum = 0;
|
||||
if (category < static_cast<int>(listing_.size()))
|
||||
for (auto& f : listing_[category]) sum += f.bytes;
|
||||
return sum;
|
||||
}
|
||||
|
||||
CleanupPlan StoragePage::planFor(int age) const {
|
||||
int today = clock_.today();
|
||||
auto cutoff = static_cast<CleanupAge>(age);
|
||||
// Without a clock only "Everything" can be judged.
|
||||
if (today < 0 && cutoff != CleanupAge::Everything) return {};
|
||||
return CleanupPlan::make(listing_[categories_.selected()], cutoff, today);
|
||||
}
|
||||
|
||||
bool StoragePage::onKey(const KeyEvent& e) {
|
||||
if (dialog_) {
|
||||
dialog_->onKey(e);
|
||||
int result = dialog_->result();
|
||||
if (result == 1) {
|
||||
if (view_ == View::Main) {
|
||||
storage_.requestFormat();
|
||||
} else {
|
||||
storage_.requestDelete(pending_.paths);
|
||||
enter();
|
||||
}
|
||||
}
|
||||
if (result != DialogModel::kPending) dialog_.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (view_) {
|
||||
case View::Main:
|
||||
switch (e.key) {
|
||||
case Key::Up: main_.up(); break;
|
||||
case Key::Down: main_.down(); break;
|
||||
case Key::Back: return false;
|
||||
case Key::Select:
|
||||
if (!storage_.state().present) {
|
||||
warn("No SD card");
|
||||
} else if (main_.selected() == kCleanUp) {
|
||||
storage_.requestListing();
|
||||
haveListing_ = false;
|
||||
categories_.setCount(kCategories);
|
||||
view_ = View::Categories;
|
||||
} else if (main_.selected() == kErase) {
|
||||
dialog_.reset(new DialogModel({"Cancel", "Erase"}));
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
|
||||
case View::Categories:
|
||||
switch (e.key) {
|
||||
case Key::Up: categories_.up(); break;
|
||||
case Key::Down: categories_.down(); break;
|
||||
case Key::Back: view_ = View::Main; break;
|
||||
case Key::Select:
|
||||
if (!haveListing_) break;
|
||||
if (listing_[categories_.selected()].empty()) {
|
||||
warn("Nothing to clean up here");
|
||||
break;
|
||||
}
|
||||
ages_.setCount(kAges);
|
||||
view_ = View::Ages;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
|
||||
case View::Ages:
|
||||
switch (e.key) {
|
||||
case Key::Up: ages_.up(); break;
|
||||
case Key::Down: ages_.down(); break;
|
||||
case Key::Back: view_ = View::Categories; break;
|
||||
case Key::Select:
|
||||
pending_ = planFor(ages_.selected());
|
||||
if (pending_.paths.empty()) warn(clock_.today() < 0 ? "Clock not set: only Everything works"
|
||||
: "No files that old");
|
||||
else dialog_.reset(new DialogModel({"Cancel", "Delete"}));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void StoragePage::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
if (!haveListing_ && storage_.listingReady()) {
|
||||
listing_ = storage_.listing();
|
||||
haveListing_ = true;
|
||||
}
|
||||
|
||||
switch (view_) {
|
||||
case View::Main: {
|
||||
auto s = storage_.state();
|
||||
widgets::list(
|
||||
c, main_, area,
|
||||
[](int i) -> std::string {
|
||||
const char* labels[] = {"SD card", "Logs", "Captures", "Clean up", "Erase SD card"};
|
||||
return labels[i];
|
||||
},
|
||||
[&](int i) -> std::string {
|
||||
if (storage_.formatting() && i == kCard) return "erasing...";
|
||||
if (!s.present) return i == kCard ? "none" : "";
|
||||
switch (i) {
|
||||
case kCard: return formatBytes(s.totalBytes) + ", " + std::to_string(s.usedPercent) + "% used";
|
||||
case kLogs: return s.logsAllowed ? "recording" : "paused (over 90%)";
|
||||
case kCaptures: return s.capturesAllowed ? "allowed" : "stopped (full)";
|
||||
default: return ">";
|
||||
}
|
||||
});
|
||||
if (dialog_)
|
||||
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
|
||||
*dialog_);
|
||||
break;
|
||||
}
|
||||
case View::Categories:
|
||||
if (!haveListing_) {
|
||||
widgets::textLines(c, {"Listing files..."}, 0, area);
|
||||
break;
|
||||
}
|
||||
widgets::list(
|
||||
c, categories_, area, [](int i) { return std::string(kCleanupCategories[i].label); },
|
||||
[this](int i) {
|
||||
return std::to_string(listing_[i].size()) + " files, " + formatBytes(categoryBytes(i));
|
||||
});
|
||||
break;
|
||||
case View::Ages:
|
||||
widgets::list(
|
||||
c, ages_, area, [](int i) { return std::string(kCleanupAgeLabels[i]); },
|
||||
[this](int i) {
|
||||
auto plan = planFor(i);
|
||||
return plan.paths.empty() ? std::string("-") : formatBytes(plan.bytes);
|
||||
});
|
||||
if (dialog_) {
|
||||
std::string message = "Delete " + std::to_string(pending_.paths.size()) + " files from " +
|
||||
kCleanupCategories[categories_.selected()].label + ", freeing " +
|
||||
formatBytes(pending_.bytes) + "?";
|
||||
widgets::dialog(c, "Clean up", message, *dialog_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user