Codex added unified library, all work

This commit is contained in:
John Poole 2026-04-19 10:20:35 -07:00
commit 8370e546ff
25 changed files with 2935 additions and 0 deletions

View file

@ -0,0 +1,14 @@
{
"name": "tbeam_storage",
"version": "0.1.0",
"description": "Reusable SD mount/watch and file storage service for LilyGO T-Beam Supreme exercises.",
"frameworks": "arduino",
"platforms": "espressif32",
"dependencies": [
{
"name": "XPowersLib",
"owner": "lewisxhe",
"version": "0.3.3"
}
]
}

View file

@ -0,0 +1,526 @@
#include "TBeamStorage.h"
#include <stdarg.h>
#include "driver/gpio.h"
namespace tbeam {
TBeamStorage::TBeamStorage(Print& diagnostic) : diagnostic_(diagnostic) {}
bool TBeamStorage::begin(const StorageConfig& config, SdEventCallback callback) {
config_ = config;
callback_ = callback;
clearError();
forceSpiDeselected();
dumpSdPins("early");
if (!initPmuForSdPower()) {
setStateAbsent();
return false;
}
cycleSdRail(config_.recoveryRailOffMs, config_.recoveryRailOnSettleMs);
delay(config_.startupWarmupMs);
bool mounted = false;
for (uint8_t i = 0; i < 3; ++i) {
if (mountPreferred(false)) {
mounted = true;
break;
}
delay(200);
}
if (!mounted) {
logf("SD: preferred mount failed, trying full scan");
cycleSdRail(400, 1200);
delay(config_.startupWarmupMs);
mounted = mountCardFullScan();
}
if (mounted) {
setStateMounted();
} else {
setError("SD mount failed");
setStateAbsent();
}
return mounted;
}
void TBeamStorage::update() {
const uint32_t now = millis();
const uint32_t pollInterval =
(state_ == SdState::MOUNTED) ? config_.pollIntervalMountedMs : config_.pollIntervalAbsentMs;
if ((uint32_t)(now - lastPollMs_) < pollInterval) {
return;
}
lastPollMs_ = now;
if (state_ == SdState::MOUNTED) {
if (verifyMountedCard()) {
presentVotes_ = 0;
absentVotes_ = 0;
return;
}
if (mountPreferred(false) && verifyMountedCard()) {
presentVotes_ = 0;
absentVotes_ = 0;
return;
}
++absentVotes_;
presentVotes_ = 0;
if (absentVotes_ >= config_.votesToAbsent) {
closeLog();
setError("SD removed or unreadable");
setStateAbsent();
absentVotes_ = 0;
}
return;
}
bool mounted = mountPreferred(false);
if (!mounted && (uint32_t)(now - lastFullScanMs_) >= config_.fullScanIntervalMs) {
lastFullScanMs_ = now;
if (config_.recoveryRailCycleOnFullScan) {
cycleSdRail(config_.recoveryRailOffMs, config_.recoveryRailOnSettleMs);
delay(150);
}
mounted = mountCardFullScan();
}
if (mounted) {
++presentVotes_;
absentVotes_ = 0;
if (presentVotes_ >= config_.votesToPresent) {
clearError();
setStateMounted();
presentVotes_ = 0;
}
} else {
++absentVotes_;
presentVotes_ = 0;
if (absentVotes_ >= config_.votesToAbsent) {
setStateAbsent();
absentVotes_ = 0;
}
}
}
bool TBeamStorage::consumeMountedEvent() {
const bool out = mountedEventPending_;
mountedEventPending_ = false;
return out;
}
bool TBeamStorage::consumeRemovedEvent() {
const bool out = removedEventPending_;
removedEventPending_ = false;
return out;
}
bool TBeamStorage::forceRemount() {
closeLog();
presentVotes_ = 0;
absentVotes_ = 0;
lastPollMs_ = 0;
lastFullScanMs_ = millis();
cycleSdRail(config_.recoveryRailOffMs, config_.recoveryRailOnSettleMs);
delay(config_.startupWarmupMs);
if (mountCardFullScan()) {
clearError();
setStateMounted();
return true;
}
setError("manual SD remount failed");
setStateAbsent();
return false;
}
bool TBeamStorage::ensureDirRecursive(const char* path) {
if (!ready() || !path || path[0] == '\0') {
setError("ensureDirRecursive invalid state");
return false;
}
char normalized[128];
if (!normalizePath(path, normalized, sizeof(normalized))) {
setError("directory path too long");
return false;
}
char partial[128] = "/";
const char* p = normalized + 1;
while (*p) {
const char* slash = strchr(p, '/');
const size_t len = slash ? (size_t)(slash - normalized) : strlen(normalized);
if (len >= sizeof(partial)) {
setError("directory path too long");
return false;
}
memcpy(partial, normalized, len);
partial[len] = '\0';
if (!SD.exists(partial) && !SD.mkdir(partial)) {
setError("SD.mkdir failed");
return false;
}
if (!slash) {
break;
}
p = slash + 1;
}
clearError();
return true;
}
bool TBeamStorage::makeUniqueLogPath(const char* prefix, const char* extension, char* out, size_t outSize) {
if (!out || outSize == 0) {
return false;
}
out[0] = '\0';
if (!ready() && !forceRemount()) {
return false;
}
if (!ensureDirRecursive(config_.logDir)) {
return false;
}
const char* safePrefix = (prefix && prefix[0]) ? prefix : "log";
const char* safeExt = (extension && extension[0]) ? extension : ".log";
char base[80];
snprintf(base, sizeof(base), "%s_%lu", safePrefix, (unsigned long)(millis() / 1000UL));
for (uint16_t suffix = 0; suffix < 1000; ++suffix) {
const int n = suffix == 0
? snprintf(out, outSize, "%s/%s%s", config_.logDir, base, safeExt)
: snprintf(out, outSize, "%s/%s_%03u%s", config_.logDir, base, (unsigned)suffix, safeExt);
if (n < 0 || (size_t)n >= outSize) {
setError("log path buffer too small");
out[0] = '\0';
return false;
}
if (!SD.exists(out)) {
clearError();
return true;
}
}
setError("could not allocate unique log path");
out[0] = '\0';
return false;
}
bool TBeamStorage::openLog(const char* path) {
closeLog();
if (!ready() && !forceRemount()) {
return false;
}
if (!path || path[0] == '\0') {
setError("openLog missing path");
return false;
}
char normalized[sizeof(currentLogPath_)];
if (!normalizePath(path, normalized, sizeof(normalized))) {
setError("log path too long");
return false;
}
const char* slash = strrchr(normalized, '/');
if (slash && slash != normalized) {
char dir[sizeof(currentLogPath_)];
const size_t len = (size_t)(slash - normalized);
memcpy(dir, normalized, len);
dir[len] = '\0';
if (!ensureDirRecursive(dir)) {
return false;
}
}
logFile_ = SD.open(normalized, FILE_APPEND);
if (!logFile_) {
logFile_ = SD.open(normalized, FILE_WRITE);
}
if (!logFile_) {
setError("SD.open log failed");
return false;
}
strlcpy(currentLogPath_, normalized, sizeof(currentLogPath_));
clearError();
return true;
}
size_t TBeamStorage::write(const uint8_t* data, size_t len) {
if (!data || len == 0 || !logFile_) {
return 0;
}
const size_t wrote = logFile_.write(data, len);
if (wrote != len) {
setError("SD log write short");
}
return wrote;
}
size_t TBeamStorage::print(const char* text) {
if (!text) {
return 0;
}
return write((const uint8_t*)text, strlen(text));
}
size_t TBeamStorage::println(const char* text) {
size_t wrote = print(text);
static const char newline[] = "\n";
wrote += write((const uint8_t*)newline, 1);
return wrote;
}
bool TBeamStorage::flush() {
if (!logFile_) {
return false;
}
logFile_.flush();
return true;
}
void TBeamStorage::closeLog() {
if (logFile_) {
logFile_.flush();
logFile_.close();
}
currentLogPath_[0] = '\0';
}
void TBeamStorage::listFiles(Print& out, const char* path, uint8_t depth) {
if (!ready()) {
out.println("SD not mounted");
return;
}
char normalized[128];
if (!normalizePath(path ? path : "/", normalized, sizeof(normalized))) {
out.println("path too long");
return;
}
File root = SD.open(normalized, FILE_READ);
if (!root) {
out.printf("open failed: %s\r\n", normalized);
return;
}
if (!root.isDirectory()) {
out.printf("%s %lu\r\n", normalized, (unsigned long)root.size());
root.close();
return;
}
listFilesRecursive(out, root, normalized, depth);
root.close();
}
bool TBeamStorage::removeFile(const char* path) {
if (!ready()) {
setError("SD not mounted");
return false;
}
char normalized[128];
if (!normalizePath(path, normalized, sizeof(normalized))) {
setError("remove path invalid");
return false;
}
if (!SD.exists(normalized)) {
setError("remove path missing");
return false;
}
if (!SD.remove(normalized)) {
setError("SD.remove failed");
return false;
}
clearError();
return true;
}
void TBeamStorage::logf(const char* fmt, ...) {
char msg[196];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
diagnostic_.printf("[%10lu][storage:%06lu] %s\r\n",
(unsigned long)millis(),
(unsigned long)logSeq_++,
msg);
}
void TBeamStorage::notify(SdEvent event, const char* message) {
if (callback_) {
callback_(event, message);
}
}
void TBeamStorage::setError(const char* message) {
strlcpy(lastError_, message ? message : "", sizeof(lastError_));
}
void TBeamStorage::clearError() {
lastError_[0] = '\0';
}
void TBeamStorage::forceSpiDeselected() {
pinMode(tbeam_supreme::sdCs(), OUTPUT);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
pinMode(tbeam_supreme::imuCs(), OUTPUT);
digitalWrite(tbeam_supreme::imuCs(), HIGH);
}
void TBeamStorage::dumpSdPins(const char* tag) {
if (!config_.enablePinDumps) {
return;
}
const gpio_num_t cs = (gpio_num_t)tbeam_supreme::sdCs();
const gpio_num_t sck = (gpio_num_t)tbeam_supreme::sdSck();
const gpio_num_t miso = (gpio_num_t)tbeam_supreme::sdMiso();
const gpio_num_t mosi = (gpio_num_t)tbeam_supreme::sdMosi();
logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d",
tag ? tag : "?",
gpio_get_level(cs),
gpio_get_level(sck),
gpio_get_level(miso),
gpio_get_level(mosi));
}
bool TBeamStorage::initPmuForSdPower() {
if (!tbeam_supreme::initPmuForPeripherals(pmu_, &diagnostic_)) {
setError("PMU init failed");
return false;
}
return true;
}
void TBeamStorage::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
if (!config_.enableSdRailCycle || !pmu_) {
return;
}
forceSpiDeselected();
pmu_->disablePowerOutput(XPOWERS_BLDO1);
delay(offMs);
pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
pmu_->enablePowerOutput(XPOWERS_BLDO1);
delay(onSettleMs);
}
bool TBeamStorage::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) {
SD.end();
bus.end();
delay(10);
forceSpiDeselected();
bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(2);
for (uint8_t i = 0; i < 10; ++i) {
bus.transfer(0xFF);
}
delay(2);
if (verbose) {
logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz);
}
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
return false;
}
if (SD.cardType() == CARD_NONE) {
SD.end();
return false;
}
sdSpi_ = &bus;
clearError();
return true;
}
bool TBeamStorage::mountPreferred(bool verbose) {
return tryMountWithBus(sdSpiH_, "HSPI", 400000, verbose);
}
bool TBeamStorage::mountCardFullScan() {
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
for (uint8_t i = 0; i < sizeof(freqs) / sizeof(freqs[0]); ++i) {
if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i], true)) {
logf("SD: mounted on HSPI");
return true;
}
}
for (uint8_t i = 0; i < sizeof(freqs) / sizeof(freqs[0]); ++i) {
if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i], true)) {
logf("SD: mounted on FSPI");
return true;
}
}
return false;
}
bool TBeamStorage::verifyMountedCard() {
if (SD.cardType() == CARD_NONE) {
return false;
}
File root = SD.open("/", FILE_READ);
const bool ok = root && root.isDirectory();
root.close();
return ok;
}
void TBeamStorage::setStateMounted() {
if (state_ != SdState::MOUNTED) {
logf("SD: mounted");
mountedEventPending_ = true;
notify(SdEvent::CARD_MOUNTED, "SD mounted");
}
state_ = SdState::MOUNTED;
}
void TBeamStorage::setStateAbsent() {
if (state_ == SdState::MOUNTED) {
removedEventPending_ = true;
notify(SdEvent::CARD_REMOVED, "SD removed");
} else if (state_ == SdState::UNKNOWN) {
notify(SdEvent::NO_CARD, "SD not mounted");
}
state_ = SdState::ABSENT;
}
void TBeamStorage::listFilesRecursive(Print& out, File& dir, const char* parentPath, uint8_t depth) {
File entry = dir.openNextFile();
while (entry) {
const char* name = entry.name();
if (entry.isDirectory()) {
out.printf("%s/\r\n", name);
if (depth > 0) {
listFilesRecursive(out, entry, name, depth - 1);
}
} else {
out.printf("%s %lu\r\n", name, (unsigned long)entry.size());
}
entry.close();
entry = dir.openNextFile();
}
(void)parentPath;
}
bool TBeamStorage::normalizePath(const char* input, char* out, size_t outSize) const {
if (!input || !out || outSize < 2) {
return false;
}
const int n = input[0] == '/' ? snprintf(out, outSize, "%s", input) : snprintf(out, outSize, "/%s", input);
return n > 0 && (size_t)n < outSize;
}
} // namespace tbeam

View file

@ -0,0 +1,114 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <XPowersLib.h>
#include "tbeam_supreme_adapter.h"
namespace tbeam {
enum class SdState : uint8_t {
UNKNOWN = 0,
ABSENT,
MOUNTED
};
enum class SdEvent : uint8_t {
NO_CARD = 0,
CARD_MOUNTED,
CARD_REMOVED
};
using SdEventCallback = void (*)(SdEvent event, const char* message);
struct StorageConfig {
const char* logDir = "/logs";
bool enableSdRailCycle = true;
bool enablePinDumps = false;
bool recoveryRailCycleOnFullScan = true;
uint32_t recoveryRailOffMs = 250;
uint32_t recoveryRailOnSettleMs = 700;
uint32_t startupWarmupMs = 1500;
uint32_t pollIntervalAbsentMs = 1000;
uint32_t pollIntervalMountedMs = 2000;
uint32_t fullScanIntervalMs = 10000;
uint32_t defaultFlushIntervalMs = 2000;
uint8_t votesToPresent = 2;
uint8_t votesToAbsent = 5;
};
class TBeamStorage {
public:
explicit TBeamStorage(Print& diagnostic = Serial);
bool begin(const StorageConfig& config = StorageConfig{}, SdEventCallback callback = nullptr);
void update();
bool ready() const { return state_ == SdState::MOUNTED; }
bool mounted() const { return ready(); }
SdState state() const { return state_; }
const char* lastError() const { return lastError_; }
const char* logDir() const { return config_.logDir; }
bool consumeMountedEvent();
bool consumeRemovedEvent();
bool forceRemount();
bool ensureDirRecursive(const char* path);
bool makeUniqueLogPath(const char* prefix, const char* extension, char* out, size_t outSize);
bool openLog(const char* path);
bool isLogOpen() const { return (bool)logFile_; }
const char* currentLogPath() const { return currentLogPath_; }
size_t write(const uint8_t* data, size_t len);
size_t print(const char* text);
size_t println(const char* text);
bool flush();
void closeLog();
void listFiles(Print& out, const char* path = "/", uint8_t depth = 4);
bool removeFile(const char* path);
private:
void logf(const char* fmt, ...);
void notify(SdEvent event, const char* message);
void setError(const char* message);
void clearError();
void forceSpiDeselected();
void dumpSdPins(const char* tag);
bool initPmuForSdPower();
void cycleSdRail(uint32_t offMs, uint32_t onSettleMs);
bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose);
bool mountPreferred(bool verbose);
bool mountCardFullScan();
bool verifyMountedCard();
void setStateMounted();
void setStateAbsent();
void listFilesRecursive(Print& out, File& dir, const char* parentPath, uint8_t depth);
bool normalizePath(const char* input, char* out, size_t outSize) const;
Print& diagnostic_;
StorageConfig config_{};
SdEventCallback callback_ = nullptr;
SPIClass sdSpiH_{HSPI};
SPIClass sdSpiF_{FSPI};
SPIClass* sdSpi_ = nullptr;
XPowersLibInterface* pmu_ = nullptr;
SdState state_ = SdState::UNKNOWN;
uint8_t presentVotes_ = 0;
uint8_t absentVotes_ = 0;
uint32_t lastPollMs_ = 0;
uint32_t lastFullScanMs_ = 0;
uint32_t logSeq_ = 0;
bool mountedEventPending_ = false;
bool removedEventPending_ = false;
File logFile_;
char currentLogPath_[128] = {};
char lastError_[160] = {};
};
} // namespace tbeam