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:
@@ -23,6 +23,11 @@ class ClockService : public Service {
|
||||
bool set(int64_t utcSeconds, TimeSource source) { return model_.set(utcSeconds, source, millis()); }
|
||||
const ClockModel& model() const { return model_; }
|
||||
|
||||
// Local "YYYY-MM-DD" once set; otherwise empty (Logs then go to undated files).
|
||||
std::string localDate() const { return model_.isSet() ? ClockModel::formatLocalDate(model_.utcNow(millis())) : ""; }
|
||||
// Days since 1970-01-01 (UTC), or -1 if the clock isn't set.
|
||||
int today() const { return model_.isSet() ? static_cast<int>(model_.utcNow(millis()) / 86400) : -1; }
|
||||
|
||||
// "14:05" once set; otherwise "--:--".
|
||||
std::string displayTime() const {
|
||||
return model_.isSet() ? ClockModel::formatLocalTime(model_.utcNow(millis())) : "--:--";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <ff.h>
|
||||
#include <sd_diskio.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "platform/pins.h"
|
||||
@@ -14,7 +15,7 @@ namespace roro {
|
||||
void StorageService::start() {
|
||||
if (task_) return;
|
||||
lock_ = xSemaphoreCreateMutex();
|
||||
xTaskCreate(taskEntry, "storage", 4096, this, 1, &task_);
|
||||
xTaskCreate(taskEntry, "storage", 6144, this, 1, &task_);
|
||||
}
|
||||
|
||||
void StorageService::stop() {
|
||||
@@ -26,23 +27,52 @@ void StorageService::stop() {
|
||||
}
|
||||
|
||||
StorageState StorageService::state() const {
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
lock();
|
||||
StorageState copy = monitor_.state();
|
||||
xSemaphoreGive(lock_);
|
||||
unlock();
|
||||
return copy;
|
||||
}
|
||||
|
||||
void StorageService::taskEntry(void* self) {
|
||||
auto* service = static_cast<StorageService*>(self);
|
||||
for (;;) {
|
||||
if (service->formatRequested_) {
|
||||
service->format();
|
||||
service->formatRequested_ = false;
|
||||
}
|
||||
service->poll();
|
||||
// Sleep until the next poll, or until a format request wakes us early.
|
||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(kPollMs));
|
||||
void StorageService::appendLine(const std::string& path, const std::string& line) {
|
||||
lock();
|
||||
size_t bytes = path.size() + line.size();
|
||||
bool accept = monitor_.state().logsAllowed && pendingBytes_ + bytes <= kMaxPendingBytes;
|
||||
if (accept) {
|
||||
pending_.emplace_back(path, line);
|
||||
pendingBytes_ += bytes;
|
||||
} else {
|
||||
dropped_++;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
void StorageService::requestListing() {
|
||||
lock();
|
||||
listingRequested_ = true;
|
||||
listingReady_ = false;
|
||||
unlock();
|
||||
if (task_) xTaskNotifyGive(task_);
|
||||
}
|
||||
|
||||
bool StorageService::listingReady() const {
|
||||
lock();
|
||||
bool ready = listingReady_;
|
||||
unlock();
|
||||
return ready;
|
||||
}
|
||||
|
||||
std::vector<std::vector<StoredFile>> StorageService::listing() const {
|
||||
lock();
|
||||
auto copy = listing_;
|
||||
unlock();
|
||||
return copy;
|
||||
}
|
||||
|
||||
void StorageService::requestDelete(std::vector<std::string> paths) {
|
||||
lock();
|
||||
toDelete_.insert(toDelete_.end(), paths.begin(), paths.end());
|
||||
unlock();
|
||||
if (task_) xTaskNotifyGive(task_);
|
||||
}
|
||||
|
||||
bool StorageService::requestFormat() {
|
||||
@@ -52,6 +82,38 @@ bool StorageService::requestFormat() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void StorageService::taskEntry(void* self) { static_cast<StorageService*>(self)->loop(); }
|
||||
|
||||
void StorageService::loop() {
|
||||
uint32_t wakes = kPollEvery; // poll right away
|
||||
for (;;) {
|
||||
if (formatRequested_) {
|
||||
format();
|
||||
formatRequested_ = false;
|
||||
wakes = kPollEvery;
|
||||
}
|
||||
if (wakes++ >= kPollEvery) {
|
||||
wakes = 1;
|
||||
poll();
|
||||
}
|
||||
writePending();
|
||||
|
||||
lock();
|
||||
bool wantListing = listingRequested_;
|
||||
std::vector<std::string> deleting;
|
||||
deleting.swap(toDelete_);
|
||||
unlock();
|
||||
if (!deleting.empty()) {
|
||||
remove(deleting);
|
||||
poll();
|
||||
}
|
||||
if (wantListing) list();
|
||||
|
||||
// Sleep until the next batch, or until a request wakes us early.
|
||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(kWakeMs));
|
||||
}
|
||||
}
|
||||
|
||||
void StorageService::format() {
|
||||
if (mounted_) SD.end();
|
||||
mounted_ = false;
|
||||
@@ -91,9 +153,76 @@ void StorageService::poll() {
|
||||
uint64_t total = present ? SD.totalBytes() : 0;
|
||||
uint64_t used = present ? SD.usedBytes() : 0;
|
||||
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
lock();
|
||||
monitor_.update(present, total, used);
|
||||
xSemaphoreGive(lock_);
|
||||
unlock();
|
||||
}
|
||||
|
||||
void StorageService::writePending() {
|
||||
lock();
|
||||
std::deque<std::pair<std::string, std::string>> batch;
|
||||
batch.swap(pending_);
|
||||
pendingBytes_ = 0;
|
||||
unlock();
|
||||
if (batch.empty() || !mounted_) return;
|
||||
|
||||
// Keep each file's lines in order while opening each file once.
|
||||
std::stable_sort(batch.begin(), batch.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
File file;
|
||||
std::string openPath;
|
||||
for (auto& [path, line] : batch) {
|
||||
if (path != openPath) {
|
||||
if (file) file.close();
|
||||
file = SD.open(path.c_str(), FILE_APPEND, true); // true: create missing folders
|
||||
openPath = path;
|
||||
}
|
||||
if (file) {
|
||||
file.write(reinterpret_cast<const uint8_t*>(line.data()), line.size());
|
||||
file.write('\n');
|
||||
}
|
||||
}
|
||||
if (file) file.close();
|
||||
}
|
||||
|
||||
namespace {
|
||||
void walk(File dir, std::vector<StoredFile>& out) {
|
||||
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
|
||||
if (f.isDirectory()) walk(f, out);
|
||||
else out.push_back({f.path(), f.size()});
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void StorageService::list() {
|
||||
std::vector<std::vector<StoredFile>> result;
|
||||
for (auto& category : kCleanupCategories) {
|
||||
std::vector<StoredFile> files;
|
||||
if (mounted_) {
|
||||
File dir = SD.open(category.folder);
|
||||
if (dir && dir.isDirectory()) walk(dir, files);
|
||||
}
|
||||
result.push_back(std::move(files));
|
||||
}
|
||||
lock();
|
||||
listing_ = std::move(result);
|
||||
listingRequested_ = false;
|
||||
listingReady_ = true;
|
||||
unlock();
|
||||
}
|
||||
|
||||
void StorageService::remove(const std::vector<std::string>& paths) {
|
||||
uint64_t freed = 0;
|
||||
int failed = 0;
|
||||
for (auto& p : paths) {
|
||||
File f = SD.open(p.c_str());
|
||||
uint64_t size = f ? f.size() : 0;
|
||||
if (f) f.close();
|
||||
if (SD.remove(p.c_str())) freed += size;
|
||||
else failed++;
|
||||
}
|
||||
std::string text = "Freed " + formatBytes(freed) + (failed ? " (" + std::to_string(failed) + " failed)" : "");
|
||||
bus_.publish(Event::withText(EventType::Notification, text.c_str(),
|
||||
static_cast<int32_t>(failed ? NotificationLevel::Warning : NotificationLevel::Info)));
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
@@ -3,13 +3,20 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "cleanup_plan.h"
|
||||
#include "service.h"
|
||||
#include "storage_monitor.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Mounts the microSD card (retrying while absent) and feeds its usage to the StorageMonitor.
|
||||
// Runs on its own task: a failed mount blocks for seconds and must never stall the UI.
|
||||
// Owns the microSD card. All card access happens on this Service's own task (the SD driver isn't
|
||||
// safe across tasks, and a failed mount blocks for seconds): it mounts and watches usage, writes
|
||||
// queued Log lines in batches, lists files for Storage Clean-up, deletes, and formats.
|
||||
class StorageService : public Service {
|
||||
public:
|
||||
explicit StorageService(EventBus& bus) : monitor_(bus), bus_(bus) {}
|
||||
@@ -19,24 +26,52 @@ class StorageService : public Service {
|
||||
|
||||
StorageState state() const;
|
||||
|
||||
// Erases the whole card: one partition spanning the card, formatted FAT32. Runs on the storage
|
||||
// task; the result arrives as a Notification. Returns false if a format is already pending.
|
||||
// Queues one line for a Log file (created with its folders as needed). Dropped while Logs are
|
||||
// paused (over 90 % full, or no card) or if too much is already waiting.
|
||||
void appendLine(const std::string& path, const std::string& line);
|
||||
uint32_t droppedLines() const { return dropped_; }
|
||||
|
||||
// Storage Clean-up: list every file of every category, then delete a selection.
|
||||
void requestListing();
|
||||
bool listingReady() const;
|
||||
std::vector<std::vector<StoredFile>> listing() const; // one vector per kCleanupCategories entry
|
||||
void requestDelete(std::vector<std::string> paths);
|
||||
|
||||
// Erases the whole card: one partition spanning the card, formatted FAT32.
|
||||
bool requestFormat();
|
||||
bool formatting() const { return formatRequested_; }
|
||||
|
||||
private:
|
||||
static constexpr uint32_t kPollMs = 15000;
|
||||
static constexpr uint32_t kWakeMs = 1000;
|
||||
static constexpr uint32_t kPollEvery = 15; // wake-ups between usage checks
|
||||
static constexpr size_t kMaxPendingBytes = 16 * 1024;
|
||||
|
||||
static void taskEntry(void* self);
|
||||
void loop();
|
||||
void poll();
|
||||
bool mount();
|
||||
void format();
|
||||
void writePending();
|
||||
void list();
|
||||
void remove(const std::vector<std::string>& paths);
|
||||
void lock() const { xSemaphoreTake(lock_, portMAX_DELAY); }
|
||||
void unlock() const { xSemaphoreGive(lock_); }
|
||||
|
||||
StorageMonitor monitor_;
|
||||
bool mounted_ = false;
|
||||
EventBus& bus_;
|
||||
volatile bool formatRequested_ = false;
|
||||
bool mounted_ = false;
|
||||
TaskHandle_t task_ = nullptr;
|
||||
mutable SemaphoreHandle_t lock_ = nullptr;
|
||||
|
||||
// Shared with other tasks, guarded by lock_.
|
||||
std::deque<std::pair<std::string, std::string>> pending_;
|
||||
size_t pendingBytes_ = 0;
|
||||
uint32_t dropped_ = 0;
|
||||
bool listingRequested_ = false;
|
||||
bool listingReady_ = false;
|
||||
std::vector<std::vector<StoredFile>> listing_;
|
||||
std::vector<std::string> toDelete_;
|
||||
volatile bool formatRequested_ = false;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
Reference in New Issue
Block a user