Files
roro9stack/src/services/storage_service.cpp
T
twislaandClaude Opus 5.5 3f2650c56e SD driver: a dummy byte before the ready test; say why a write failed (#21)
The card "refused" a write about once in 2,000 multi-block writes: three
1.7 MB uploads in ten. Measured with a driver that records where it gives
up: every time, all blocks were accepted, and the status check after Stop
Tran came back as 0xFF or 0x1F. The driver tests for ready with the first
byte after selecting the card, which reads 0xFF before the card has
signalled busy, so CMD13 went out mid-programming. A dummy byte first, as
in ChaN's reference driver, and one after Stop Tran.

30 uploads in a row since, each read back by SHA-256, ten with the radio
listening: no fault. 10 MHz made no difference; the card stays at 20 MHz.

`info` shows the driver's write faults; `put` prints the step and the
card's answer when one happens. ADR 0007.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-05 22:55:02 +02:00

276 lines
8.0 KiB
C++

#include "storage_service.h"
#include <SD.h>
#include <ff.h>
#include <sd_diskio.h>
#include <algorithm>
#include <atomic>
#include <memory>
#include "platform/pins.h"
#include "platform/shared_spi.h"
namespace roro {
void StorageService::start() {
if (task_) return;
xTaskCreate(taskEntry, "storage", 6144, this, 1, &task_); // peak 3.9 KB (SD install with its signature check, M2)
}
void StorageService::stop() {
if (!task_) return;
vTaskDelete(task_);
task_ = nullptr;
if (mounted_) SD.end();
mounted_ = false;
}
StorageState StorageService::state() const {
lock();
StorageState copy = monitor_.state();
unlock();
return copy;
}
void StorageService::appendLine(const std::string& path, const std::string& line, bool capture) {
append(path, line, capture, true);
}
void StorageService::appendBytes(const std::string& path, const std::string& bytes, bool capture) {
append(path, bytes, capture, false);
}
void StorageService::append(const std::string& path, const std::string& data, bool capture, bool newline) {
lock();
size_t bytes = path.size() + data.size();
bool allowed = capture ? monitor_.state().capturesAllowed : monitor_.state().logsAllowed;
bool accept = allowed && pendingBytes_ + bytes <= kMaxPendingBytes;
if (accept) {
pending_.push_back({path, data, newline});
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_);
}
void StorageService::runJob(std::function<void()> job) {
lock();
jobs_.push_back(std::move(job));
unlock();
if (task_) xTaskNotifyGive(task_);
}
bool StorageService::runAndWait(std::function<void()> job) {
// Shared with the job, which outlives this wait if the card is missing and it never starts.
// Exactly one side wins the state change: the job (Queued -> Running) or the wait giving up
// (Queued -> Abandoned), so an abandoned job can never touch this stack frame.
enum : int { Queued, Running, Abandoned };
struct Run {
std::atomic<int> state{Queued};
SemaphoreHandle_t done = xSemaphoreCreateBinary();
~Run() { vSemaphoreDelete(done); }
};
auto run = std::make_shared<Run>();
runJob([run, job]() {
int expected = Queued;
if (!run->state.compare_exchange_strong(expected, Running)) return;
job();
xSemaphoreGive(run->done);
});
for (int waited = 0; xSemaphoreTake(run->done, pdMS_TO_TICKS(500)) != pdTRUE; waited += 500) {
int expected = Queued;
if (waited >= 5000 && run->state.compare_exchange_strong(expected, Abandoned)) return false;
}
return true;
}
bool StorageService::requestFormat() {
if (formatRequested_ || !task_) return false;
formatRequested_ = true;
xTaskNotifyGive(task_);
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();
lock();
std::vector<std::function<void()>> jobs;
jobs.swap(jobs_);
unlock();
for (auto& job : jobs)
if (mounted_) job();
// 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;
bool ok = false;
uint8_t pdrv = sdcard_init(pins::kSdCs, &sharedSpi(), kSdHz);
if (pdrv != 0xFF) {
constexpr size_t kWorkSize = 4096; // FF_MAX_SS
std::unique_ptr<uint8_t[]> work(new uint8_t[kWorkSize]);
LBA_t partitions[] = {100, 0}; // one partition, 100 % of the card
char drive[3] = {static_cast<char>('0' + pdrv), ':', 0};
MKFS_PARM options = {FM_FAT32, 0, 0, 0, 0};
ok = f_fdisk(pdrv, partitions, work.get()) == FR_OK &&
f_mkfs(drive, &options, work.get(), kWorkSize) == FR_OK;
sdcard_uninit(pdrv);
}
bus_.publish(Event::withText(EventType::Notification, ok ? "SD card formatted" : "SD card format failed",
static_cast<int32_t>(ok ? NotificationLevel::Info : NotificationLevel::Warning)));
}
bool StorageService::mount() {
return SD.begin(pins::kSdCs, sharedSpi(), kSdHz, "/sd", 5, false);
}
void StorageService::poll() {
if (mounted_) {
// Detect removal: the root can no longer be opened.
File root = SD.open("/");
if (!root) {
SD.end();
mounted_ = false;
}
}
if (!mounted_) mounted_ = mount();
bool present = mounted_;
uint64_t total = present ? SD.totalBytes() : 0;
uint64_t used = present ? SD.usedBytes() : 0;
lock();
monitor_.update(present, total, used);
unlock();
}
void StorageService::writePending() {
lock();
std::deque<Pending> 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.path < b.path; });
File file;
std::string openPath;
for (auto& p : batch) {
if (p.path != openPath) {
if (file) file.close();
file = SD.open(p.path.c_str(), FILE_APPEND, true); // true: create missing folders
openPath = p.path;
}
if (file) {
file.write(reinterpret_cast<const uint8_t*>(p.data.data()), p.data.size());
if (p.newline) 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