#include "StartupSdManager.h" #include #include "driver/gpio.h" StartupSdManager::StartupSdManager(Print& serial) : serial_(serial) {} bool StartupSdManager::begin(const SdWatcherConfig& cfg, SdStatusCallback callback) { cfg_ = cfg; callback_ = callback; forceSpiDeselected(); dumpSdPins("very-early"); if (!initPmuForSdPower()) { return false; } cycleSdRail(); delay(cfg_.startupWarmupMs); bool warmMounted = false; for (uint8_t i = 0; i < 3; ++i) { if (mountPreferred(false)) { warmMounted = true; break; } delay(200); } // Some cards need a longer power/settle window after cold boot. // Before declaring ABSENT, retry with extended settle and a full scan. if (!warmMounted) { logf("Watcher: startup preferred mount failed, retrying with extended settle"); cycleSdRail(400, 1200); delay(cfg_.startupWarmupMs + 1500); warmMounted = mountCardFullScan(); } if (warmMounted) { setStateMounted(); } else { setStateAbsent(); } return true; } void StartupSdManager::update() { const uint32_t now = millis(); const uint32_t pollInterval = (watchState_ == SdWatchState::MOUNTED) ? cfg_.pollIntervalMountedMs : cfg_.pollIntervalAbsentMs; if ((uint32_t)(now - lastPollMs_) < pollInterval) { return; } lastPollMs_ = now; if (watchState_ == SdWatchState::MOUNTED) { if (verifyMountedCard()) { presentVotes_ = 0; absentVotes_ = 0; return; } if (mountPreferred(false) && verifyMountedCard()) { presentVotes_ = 0; absentVotes_ = 0; return; } absentVotes_++; presentVotes_ = 0; if (absentVotes_ >= cfg_.votesToAbsent) { setStateAbsent(); absentVotes_ = 0; } return; } bool mounted = mountPreferred(false); if (!mounted && (uint32_t)(now - lastFullScanMs_) >= cfg_.fullScanIntervalMs) { lastFullScanMs_ = now; if (cfg_.recoveryRailCycleOnFullScan) { logf("Watcher: recovery rail cycle before full scan"); cycleSdRail(cfg_.recoveryRailOffMs, cfg_.recoveryRailOnSettleMs); delay(150); } logf("Watcher: preferred probe failed, running full scan"); mounted = mountCardFullScan(); } if (mounted) { presentVotes_++; absentVotes_ = 0; if (presentVotes_ >= cfg_.votesToPresent) { setStateMounted(); presentVotes_ = 0; } } else { absentVotes_++; presentVotes_ = 0; if (absentVotes_ >= cfg_.votesToAbsent) { setStateAbsent(); absentVotes_ = 0; } } } bool StartupSdManager::consumeMountedEvent() { bool out = mountedEventPending_; mountedEventPending_ = false; return out; } bool StartupSdManager::consumeRemovedEvent() { bool out = removedEventPending_; removedEventPending_ = false; return out; } void StartupSdManager::logf(const char* fmt, ...) { char msg[196]; va_list args; va_start(args, fmt); vsnprintf(msg, sizeof(msg), fmt, args); va_end(args); serial_.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)logSeq_++, msg); } void StartupSdManager::notify(SdEvent event, const char* message) { if (callback_ != nullptr) { callback_(event, message); } } void StartupSdManager::forceSpiDeselected() { pinMode(tbeam_supreme::sdCs(), OUTPUT); digitalWrite(tbeam_supreme::sdCs(), HIGH); pinMode(tbeam_supreme::imuCs(), OUTPUT); digitalWrite(tbeam_supreme::imuCs(), HIGH); } void StartupSdManager::dumpSdPins(const char* tag) { if (!cfg_.enablePinDumps) { (void)tag; 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, gpio_get_level(cs), gpio_get_level(sck), gpio_get_level(miso), gpio_get_level(mosi)); } bool StartupSdManager::initPmuForSdPower() { if (!tbeam_supreme::initPmuForPeripherals(pmu_, &serial_)) { logf("ERROR: PMU init failed"); return false; } return true; } void StartupSdManager::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) { if (!cfg_.enableSdRailCycle) { return; } if (!pmu_) { logf("SD rail cycle skipped: pmu=null"); return; } forceSpiDeselected(); pmu_->disablePowerOutput(XPOWERS_BLDO1); delay(offMs); pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); pmu_->enablePowerOutput(XPOWERS_BLDO1); delay(onSettleMs); } bool StartupSdManager::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 (int 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)) { if (verbose) { logf("SD: mount failed (possible non-FAT format, power, or bus issue)"); } return false; } if (SD.cardType() == CARD_NONE) { SD.end(); return false; } sdSpi_ = &bus; sdBusName_ = busName; sdFreq_ = hz; return true; } bool StartupSdManager::mountPreferred(bool verbose) { return tryMountWithBus(sdSpiH_, "HSPI", 400000, verbose); } bool StartupSdManager::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: card detected and mounted"); return true; } } for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) { if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i], true)) { logf("SD: card detected and mounted"); return true; } } logf("SD: begin() failed on all bus/frequency attempts"); return false; } bool StartupSdManager::verifyMountedCard() { File root = SD.open("/", FILE_READ); if (!root) { return false; } root.close(); return true; } const char* StartupSdManager::cardTypeToString(uint8_t type) { switch (type) { case CARD_MMC: return "MMC"; case CARD_SD: return "SDSC"; case CARD_SDHC: return "SDHC/SDXC"; default: return "UNKNOWN"; } } void StartupSdManager::printCardInfo() { uint8_t cardType = SD.cardType(); uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL); uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL); uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL); logf("SD type: %s", cardTypeToString(cardType)); logf("SD size: %llu MB", cardSizeMB); logf("FS total: %llu MB", totalMB); logf("FS used : %llu MB", usedMB); logf("SPI bus: %s @ %lu Hz", sdBusName_, (unsigned long)sdFreq_); } bool StartupSdManager::ensureDirRecursive(const char* path) { String full(path); if (!full.startsWith("/")) { full = "/" + full; } int start = 1; while (start > 0 && start < (int)full.length()) { int slash = full.indexOf('/', start); String partial = (slash < 0) ? full : full.substring(0, slash); if (!SD.exists(partial.c_str()) && !SD.mkdir(partial.c_str())) { logf("ERROR: mkdir failed for %s", partial.c_str()); return false; } if (slash < 0) { break; } start = slash + 1; } return true; } bool StartupSdManager::rewriteFile(const char* path, const char* payload) { if (SD.exists(path) && !SD.remove(path)) { logf("ERROR: failed to erase %s", path); return false; } File f = SD.open(path, FILE_WRITE); if (!f) { logf("ERROR: failed to create %s", path); return false; } size_t wrote = f.println(payload); f.close(); if (wrote == 0) { logf("ERROR: write failed for %s", path); return false; } return true; } void StartupSdManager::permissionsDemo(const char* path) { logf("Permissions demo: FAT has no Unix chmod/chown, use open mode only."); File r = SD.open(path, FILE_READ); if (!r) { logf("Could not open %s as FILE_READ", path); return; } size_t writeInReadMode = r.print("attempt write while opened read-only"); if (writeInReadMode == 0) { logf("As expected, FILE_READ write was blocked."); } else { logf("NOTE: FILE_READ write returned %u (unexpected)", (unsigned)writeInReadMode); } r.close(); } void StartupSdManager::setStateMounted() { if (watchState_ != SdWatchState::MOUNTED) { logf("EVENT: card inserted/mounted"); mountedEventPending_ = true; notify(SdEvent::CARD_MOUNTED, "SD card mounted"); } watchState_ = SdWatchState::MOUNTED; } void StartupSdManager::setStateAbsent() { if (watchState_ == SdWatchState::MOUNTED) { logf("EVENT: card removed/unavailable"); removedEventPending_ = true; notify(SdEvent::CARD_REMOVED, "SD card removed"); } else if (watchState_ != SdWatchState::ABSENT) { logf("EVENT: no card detected"); notify(SdEvent::NO_CARD, "Missing SD card or invalid FAT16/FAT32 format"); } SD.end(); watchState_ = SdWatchState::ABSENT; }