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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "services/battery_service.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "apps/storage_page.h"
|
||||
#include "apps/wifi_settings_page.h"
|
||||
#include "settings_menu.h"
|
||||
#include "ui/theme.h"
|
||||
@@ -34,7 +35,10 @@ struct SettingsAppDeps {
|
||||
class SettingsApp : public App {
|
||||
public:
|
||||
explicit SettingsApp(const SettingsAppDeps& deps)
|
||||
: d_(deps), menu_(deps.settings), wifiPage_(deps.settings, deps.savedNetworks, deps.wifi, deps.bus) {}
|
||||
: d_(deps),
|
||||
menu_(deps.settings),
|
||||
wifiPage_(deps.settings, deps.savedNetworks, deps.wifi, deps.bus),
|
||||
storagePage_(deps.storage, deps.clock, deps.bus) {}
|
||||
void onEnter() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
@@ -49,20 +53,18 @@ class SettingsApp : public App {
|
||||
bool onMenuKey(const KeyEvent& e);
|
||||
bool onTextKey(const KeyEvent& e);
|
||||
bool onChoiceKey(const KeyEvent& e);
|
||||
bool onStorageKey(const KeyEvent& e);
|
||||
bool onAboutKey(const KeyEvent& e);
|
||||
void warn(const std::string& text);
|
||||
std::vector<std::string> storageLines() const;
|
||||
std::vector<std::string> aboutLines() const;
|
||||
|
||||
SettingsAppDeps d_;
|
||||
SettingsMenu menu_;
|
||||
WifiSettingsPage wifiPage_;
|
||||
StoragePage storagePage_;
|
||||
Page page_ = Page::Menu;
|
||||
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
||||
ListModel choices_{theme::kContent.h / theme::kLineHeight};
|
||||
LineEditor editor_{39};
|
||||
std::unique_ptr<DialogModel> eraseDialog_;
|
||||
int editingRow_ = 0;
|
||||
uint32_t lastRefreshMs_ = 0;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "cleanup_plan.h"
|
||||
#include "dialog_model.h"
|
||||
#include "event_bus.h"
|
||||
#include "key_event.h"
|
||||
#include "list_model.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "ui/canvas.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Settings → Storage: card usage, Storage Clean-up (category → age → confirm) and erasing the card.
|
||||
class StoragePage {
|
||||
public:
|
||||
StoragePage(StorageService& storage, ClockService& clock, EventBus& bus)
|
||||
: storage_(storage), clock_(clock), bus_(bus) {}
|
||||
|
||||
void enter();
|
||||
bool onKey(const KeyEvent& e); // false: leave the page
|
||||
void draw(Canvas& c);
|
||||
|
||||
private:
|
||||
enum class View { Main, Categories, Ages };
|
||||
enum Row { kCard, kLogs, kCaptures, kCleanUp, kErase, kRows };
|
||||
|
||||
CleanupPlan planFor(int age) const;
|
||||
uint64_t categoryBytes(int category) const;
|
||||
void warn(const char* text);
|
||||
|
||||
StorageService& storage_;
|
||||
ClockService& clock_;
|
||||
EventBus& bus_;
|
||||
View view_ = View::Main;
|
||||
ListModel main_{theme::kContent.h / theme::kLineHeight};
|
||||
ListModel categories_{theme::kContent.h / theme::kLineHeight};
|
||||
ListModel ages_{theme::kContent.h / theme::kLineHeight};
|
||||
std::vector<std::vector<StoredFile>> listing_;
|
||||
bool haveListing_ = false;
|
||||
std::unique_ptr<DialogModel> dialog_;
|
||||
CleanupPlan pending_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user