// 20260213 ChatGPT // $Id$ // $HeadURL$ #include #include #include #include #include #include #include #ifndef SD_SCK #define SD_SCK 36 #endif #ifndef SD_MISO #define SD_MISO 37 #endif #ifndef SD_MOSI #define SD_MOSI 35 #endif #ifndef SD_CS #define SD_CS 47 #endif #ifndef IMU_CS #define IMU_CS 34 #endif #ifndef I2C_SDA1 #define I2C_SDA1 42 #endif #ifndef I2C_SCL1 #define I2C_SCL1 41 #endif static SPIClass sdSpiH(HSPI); static SPIClass sdSpiF(FSPI); static SPIClass* g_sdSpi = nullptr; static const char* g_sdBusName = "none"; static uint32_t g_sdFreq = 0; static XPowersLibInterface* g_pmu = nullptr; static const char* kRootTestFile = "/Exercise_05_test.txt"; static const char* kNestedDir = "/test/testsub1/testsubsub1"; static const char* kNestedTestFile = "/test/testsub1/testsubsub1/Exercise_05_test.txt"; static const char* kPayload = "This is a test"; enum class WatchState : uint8_t { UNKNOWN = 0, ABSENT, MOUNTED }; static WatchState g_watchState = WatchState::UNKNOWN; static uint8_t g_presentVotes = 0; static uint8_t g_absentVotes = 0; static uint32_t g_lastPollMs = 0; static uint32_t g_lastFullScanMs = 0; static uint32_t g_lastPeriodicActionMs = 0; static const uint32_t kPollIntervalAbsentMs = 1000; static const uint32_t kPollIntervalMountedMs = 2000; static const uint32_t kFullScanIntervalMs = 10000; static const uint32_t kPeriodicActionMs = 15000; static const uint8_t kVotesToPresent = 2; //static const uint8_t kVotesToAbsent = 4; static const uint8_t kVotesToAbsent = 5; // More votes needed to declare absent to prevent false removes on transient errors. //static const uint32_t kStartupWarmupMs = 800; static const uint32_t kStartupWarmupMs = 1500; // Longer warmup to allow PMU and card stabilization after power-on. static uint32_t g_logSeq = 0; static void logf(const char* fmt, ...) { char msg[192]; 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)g_logSeq++, msg); } static bool initPmuForSdPower() { Wire1.begin(I2C_SDA1, I2C_SCL1); if (!g_pmu) { g_pmu = new XPowersAXP2101(Wire1); } if (!g_pmu->init()) { logf("PMU: AXP2101 init failed (SD power rail may be off)"); return false; } // Mirror Meshtastic tbeam-s3-core power setup needed for peripherals. g_pmu->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); // GNSS g_pmu->enablePowerOutput(XPOWERS_ALDO4); g_pmu->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // LoRa g_pmu->enablePowerOutput(XPOWERS_ALDO3); g_pmu->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // sensor/rtc path g_pmu->enablePowerOutput(XPOWERS_ALDO2); g_pmu->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); // IMU/OLED path g_pmu->enablePowerOutput(XPOWERS_ALDO1); g_pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); // SD card rail g_pmu->enablePowerOutput(XPOWERS_BLDO1); logf("PMU: AXP2101 ready, BLDO1(SD)=%s", g_pmu->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF"); return g_pmu->isPowerChannelEnable(XPOWERS_BLDO1); } static const char* 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"; } } static bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) { SD.end(); bus.end(); delay(10); // Keep inactive devices deselected on shared bus lines. pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH); pinMode(IMU_CS, OUTPUT); digitalWrite(IMU_CS, HIGH); bus.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS); delay(2); if (verbose) { logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz); } if (!SD.begin(SD_CS, bus, hz)) { if (verbose) { logf("SD: mount failed (possible non-FAT format, power, or bus issue)"); } return false; } uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { SD.end(); return false; } g_sdSpi = &bus; g_sdBusName = busName; g_sdFreq = hz; return true; } static bool mountPreferred(bool verbose) { return tryMountWithBus(sdSpiH, "HSPI", 400000, verbose); } static bool mountCard() { 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"); logf(" likely card absent, bad format, pin mismatch, or hardware issue"); return false; } static void 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", g_sdBusName, (unsigned long)g_sdFreq); } static bool 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())) { logf("Creating directory: %s", partial.c_str()); if (!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; } static bool rewriteFile(const char* path, const char* payload) { if (SD.exists(path)) { logf("WARNING: %s exists ... erasing", path); if (!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; } logf("Wrote file: %s", path); return true; } static void permissionsDemo(const char* path) { logf("Permissions demo:"); logf(" SD/FAT does not support Unix permissions (chmod/chown)."); logf(" Access control is by open mode (FILE_READ/FILE_WRITE)."); File r = SD.open(path, FILE_READ); if (!r) { logf(" Could not open %s as FILE_READ", path); return; } logf(" FILE_READ open succeeded, size=%u bytes", (unsigned)r.size()); size_t writeInReadMode = r.print("attempt write while opened read-only"); if (writeInReadMode == 0) { logf(" As expected, write via FILE_READ handle was blocked."); } else { logf(" NOTE: write via FILE_READ returned %u (unexpected)", (unsigned)writeInReadMode); } r.close(); } static bool verifyMountedCard() { File root = SD.open("/", FILE_READ); if (!root) { return false; } root.close(); return true; } static void runCardWorkflow() { printCardInfo(); if (!rewriteFile(kRootTestFile, kPayload)) { logf("Watcher action: root file write failed"); return; } if (!ensureDirRecursive(kNestedDir)) { logf("Watcher action: directory creation failed"); return; } if (!rewriteFile(kNestedTestFile, kPayload)) { logf("Watcher action: nested file write failed"); return; } permissionsDemo(kRootTestFile); } static void setStateMounted() { if (g_watchState != WatchState::MOUNTED) { logf("EVENT: card inserted/mounted"); runCardWorkflow(); g_lastPeriodicActionMs = millis(); } g_watchState = WatchState::MOUNTED; } static void setStateAbsent() { if (g_watchState == WatchState::MOUNTED) { logf("EVENT: card removed/unavailable"); } else if (g_watchState != WatchState::ABSENT) { logf("EVENT: no card detected"); } SD.end(); g_watchState = WatchState::ABSENT; } void setup() { Serial.begin(115200); Serial.println("[WATCHER: startup]"); Serial.println("Sleeping for 5 seconds to allow Serial Monitor connection..."); delay(5000); // Time to open Serial Monitor after reset Serial.println("\r\n=================================================="); Serial.println("Exercise 05: SD Card Watcher"); Serial.println("=================================================="); Serial.printf("Pins: CS=%d SCK=%d MISO=%d MOSI=%d\r\n", SD_CS, SD_SCK, SD_MISO, SD_MOSI); Serial.printf("PMU I2C: SDA1=%d SCL1=%d\r\n", I2C_SDA1, I2C_SCL1); Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library.\r\n"); initPmuForSdPower(); logf("Watcher: waiting %lu ms for SD rail/card stabilization", (unsigned long)kStartupWarmupMs); delay(kStartupWarmupMs); // Warm-up attempts before first status decision. bool warmMounted = false; for (uint8_t i = 0; i < 3; ++i) { if (mountPreferred(false)) { warmMounted = true; break; } delay(200); } if (warmMounted) { logf("Watcher: startup warmup mount succeeded"); setStateMounted(); } else { logf("Watcher: startup warmup did not mount card"); setStateAbsent(); } } void loop() { const uint32_t now = millis(); const uint32_t pollInterval = (g_watchState == WatchState::MOUNTED) ? kPollIntervalMountedMs : kPollIntervalAbsentMs; if ((uint32_t)(now - g_lastPollMs) < pollInterval) { delay(10); return; } g_lastPollMs = now; if (g_watchState == WatchState::MOUNTED) { if (verifyMountedCard()) { if ((uint32_t)(now - g_lastPeriodicActionMs) >= kPeriodicActionMs) { logf("Watcher: periodic mounted check action"); runCardWorkflow(); g_lastPeriodicActionMs = now; } g_presentVotes = 0; g_absentVotes = 0; return; } // One immediate remount attempt prevents false remove events on transient SPI errors. if (mountPreferred(false) && verifyMountedCard()) { g_presentVotes = 0; g_absentVotes = 0; return; } g_absentVotes++; g_presentVotes = 0; if (g_absentVotes >= kVotesToAbsent) { setStateAbsent(); g_absentVotes = 0; } return; } bool mounted = mountPreferred(false); if (!mounted && (uint32_t)(now - g_lastFullScanMs) >= kFullScanIntervalMs) { g_lastFullScanMs = now; logf("Watcher: preferred probe failed, running full scan"); mounted = mountCard(); } if (mounted) { g_presentVotes++; g_absentVotes = 0; if (g_presentVotes >= kVotesToPresent) { setStateMounted(); g_presentVotes = 0; } } else { g_absentVotes++; g_presentVotes = 0; if (g_absentVotes >= kVotesToAbsent) { setStateAbsent(); g_absentVotes = 0; } } }