411 lines
11 KiB
C++
411 lines
11 KiB
C++
|
|
// 20260213 ChatGPT
|
||
|
|
// $Id$
|
||
|
|
// $HeadURL$
|
||
|
|
|
||
|
|
#include <Arduino.h>
|
||
|
|
#include <stdarg.h>
|
||
|
|
#include <FS.h>
|
||
|
|
#include <SD.h>
|
||
|
|
#include <SPI.h>
|
||
|
|
#include <Wire.h>
|
||
|
|
#include <XPowersLib.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|