Public Access
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
276 lines
8.0 KiB
C++
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
|