diff --git a/src/core/diagnostics_state.cpp b/src/core/diagnostics_state.cpp
index bed3b76..2717bf1 100644
--- a/src/core/diagnostics_state.cpp
+++ b/src/core/diagnostics_state.cpp
@@ -11,13 +11,7 @@ void DiagnosticsState::update()
ESP.getMinFreeHeap();
current.cpuFrequency =
- ESP.getCpuFreqMHz();
-
- Serial.print("#update: ");
- Serial.println(current.freeHeap);
- Serial.println("Calling current from update");
- getCurrent();
-
+ ESP.getCpuFreqMHz();
}
@@ -29,9 +23,5 @@ DiagnosticSample DiagnosticsState::getCurrent()
sample.freeHeap = current.freeHeap;
sample.minimumFreeHeap = current.minimumFreeHeap;
sample.cpuFrequency = current.cpuFrequency;
-
- Serial.print("#getCurrent: ");
- Serial.println(sample.freeHeap);
- Serial.println(sample.timestamp);
return sample;
}
diff --git a/src/main.cpp b/src/main.cpp
index 111dee2..7bbd11d 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -11,8 +11,11 @@
#include "services/ota_service.h"
#include "services/web_service.h"
+#include "storage/sd_manager.h"
+
#include "tasks/system_task.h"
#include "tasks/diagnostics_task.h"
+#include "tasks/storage_task.h"
DashboardState dashboardState;
@@ -31,6 +34,9 @@ SystemTask systemTask(services);
DiagnosticsTask diagnosticsTask(diagnosticsState);
+SDManager sdManager;
+StorageTask storageTask(sdManager);
+
void setup()
{
Serial.begin(115200);
@@ -42,6 +48,7 @@ void setup()
systemTask.start();
diagnosticsTask.start();
+ storageTask.start();
}
diff --git a/src/services/web_service.cpp b/src/services/web_service.cpp
index defc279..c108139 100644
--- a/src/services/web_service.cpp
+++ b/src/services/web_service.cpp
@@ -128,7 +128,6 @@ Firmware Version:
Loading...
void WebService::broadcastState()
{
DiagnosticSample diagnostics = diagnosticsState.getCurrent();
- Serial.println(diagnostics.freeHeap);
String json = "{";
// Opening system tag:
diff --git a/src/storage/sd_manager.cpp b/src/storage/sd_manager.cpp
new file mode 100644
index 0000000..f8579fb
--- /dev/null
+++ b/src/storage/sd_manager.cpp
@@ -0,0 +1,334 @@
+#include "sd_manager.h"
+
+#include "storage_config.h"
+
+#include
+
+// Both backends are compiled into the image so that the SDIO path is
+// compile-checked even while the system still runs SPI for bring-up.
+#include
+#include
+
+namespace
+{
+
+class SpiBackend : public StorageBackend
+{
+public:
+ bool begin() override
+ {
+ // Explicit SPI instance so the pin wiring is driven from
+ // storage_config.h rather than the board defaults. The second
+ // spi.begin() call made inside SDFS::begin() is a no-op because the
+ // bus is already started with these pins.
+ static SPIClass spi;
+
+ spi.begin(
+ STORAGE_SPI_SCK,
+ STORAGE_SPI_MISO,
+ STORAGE_SPI_MOSI,
+ STORAGE_SPI_CS
+ );
+
+ // Cheap breakout modules often omit the pull-up resistors the SD
+ // spec expects on the idle-high lines. The ESP32 SPI HAL clears the
+ // internal pull-ups when it attaches a pin (esp32-hal-spi.c), so a
+ // floating MISO/CS means the card never answers CMD0 during init
+ // ("Card Failed! cmd: 0x00"). Re-enable the pull-ups directly so we
+ // do not disturb the pin's peripheral function.
+ gpio_pullup_en((gpio_num_t)STORAGE_SPI_MISO);
+ gpio_pullup_en((gpio_num_t)STORAGE_SPI_CS);
+
+ // Give the card a moment to stabilize after power-on before the init
+ // handshake starts (the framework sends 74+ dummy clocks, but some
+ // cards need a little more settling time on a fresh mount attempt).
+ delay(20);
+
+ return SD.begin(
+ STORAGE_SPI_CS,
+ spi,
+ STORAGE_SPI_FREQ,
+ STORAGE_MOUNT_POINT,
+ STORAGE_MAX_OPEN_FILES,
+ false // format_if_empty: never auto-format
+ );
+ }
+
+ void end() override
+ {
+ SD.end();
+ }
+
+ fs::FS& fs() override
+ {
+ return SD;
+ }
+
+ StorageCardType cardType() override
+ {
+ return mapType(SD.cardType());
+ }
+
+ uint64_t totalBytes() override
+ {
+ return SD.totalBytes();
+ }
+
+ uint64_t usedBytes() override
+ {
+ return SD.usedBytes();
+ }
+
+private:
+ static StorageCardType mapType(sdcard_type_t type)
+ {
+ switch (type)
+ {
+ case CARD_MMC: return StorageCardType::MMC;
+ case CARD_SD: return StorageCardType::SD;
+ case CARD_SDHC: return StorageCardType::SDHC;
+ case CARD_NONE: return StorageCardType::None;
+ default: return StorageCardType::Unknown;
+ }
+ }
+};
+
+class SdmmcBackend : public StorageBackend
+{
+public:
+ bool begin() override
+ {
+ // The plain esp32dev variant does not pre-wire the SDMMC pins, so
+ // they must always be set explicitly. The classic ESP32 routes the
+ // SDMMC peripheral through the GPIO matrix, so the pins defined in
+ // storage_config.h are fully re-routable.
+ if (!SD_MMC.setPins(
+ STORAGE_SDMMC_CLK,
+ STORAGE_SDMMC_CMD,
+ STORAGE_SDMMC_D0,
+ STORAGE_SDMMC_D1,
+ STORAGE_SDMMC_D2,
+ STORAGE_SDMMC_D3))
+ {
+ return false;
+ }
+
+ return SD_MMC.begin(
+ STORAGE_MOUNT_POINT,
+ STORAGE_SDMMC_MODE_1BIT, // false == 4-bit bus
+ STORAGE_SDMMC_FORMAT_IF_FAILED, // never auto-format
+ STORAGE_SDMMC_FREQ_HZ, // 40 MHz == SDMMC_FREQ_HIGHSPEED
+ STORAGE_MAX_OPEN_FILES
+ );
+ }
+
+ void end() override
+ {
+ SD_MMC.end();
+ }
+
+ fs::FS& fs() override
+ {
+ return SD_MMC;
+ }
+
+ StorageCardType cardType() override
+ {
+ return mapType(SD_MMC.cardType());
+ }
+
+ uint64_t totalBytes() override
+ {
+ return SD_MMC.totalBytes();
+ }
+
+ uint64_t usedBytes() override
+ {
+ return SD_MMC.usedBytes();
+ }
+
+private:
+ static StorageCardType mapType(sdcard_type_t type)
+ {
+ switch (type)
+ {
+ case CARD_MMC: return StorageCardType::MMC;
+ case CARD_SD: return StorageCardType::SD;
+ case CARD_SDHC: return StorageCardType::SDHC;
+ case CARD_NONE: return StorageCardType::None;
+ default: return StorageCardType::Unknown;
+ }
+ }
+};
+
+} // namespace
+
+
+SDManager::SDManager()
+:
+backend(nullptr),
+mounted(false)
+{
+}
+
+
+bool SDManager::begin()
+{
+ if (mounted)
+ {
+ return true;
+ }
+
+#if STORAGE_IFACE == STORAGE_IFACE_SDMMC
+ backend = new SdmmcBackend();
+#else
+ backend = new SpiBackend();
+#endif
+
+ if (backend == nullptr)
+ {
+ return false;
+ }
+
+ mounted = backend->begin();
+
+ if (!mounted)
+ {
+ Serial.println("[Storage] SD card mount FAILED");
+ delete backend;
+ backend = nullptr;
+ }
+
+ return mounted;
+}
+
+
+void SDManager::end()
+{
+ if (backend != nullptr)
+ {
+ backend->end();
+ delete backend;
+ backend = nullptr;
+ }
+
+ mounted = false;
+}
+
+
+bool SDManager::isMounted() const
+{
+ return mounted;
+}
+
+
+fs::FS& SDManager::fs()
+{
+ return backend->fs();
+}
+
+
+const char* SDManager::cardTypeName(StorageCardType type)
+{
+ switch (type)
+ {
+ case StorageCardType::MMC: return "MMC";
+ case StorageCardType::SD: return "SD";
+ case StorageCardType::SDHC: return "SDHC";
+ case StorageCardType::Unknown: return "Unknown";
+ case StorageCardType::None:
+ default: return "None";
+ }
+}
+
+
+void SDManager::printCardInfo()
+{
+ if (!mounted)
+ {
+ Serial.println("[Storage] Card info unavailable (not mounted)");
+ return;
+ }
+
+ uint64_t total = backend->totalBytes();
+ uint64_t used = backend->usedBytes();
+ uint64_t free = (total > used) ? (total - used) : 0;
+
+ Serial.println("----------------------------------------");
+ Serial.println("SD card information");
+ Serial.println("----------------------------------------");
+ Serial.printf(" Type : %s\n", cardTypeName(backend->cardType()));
+ Serial.printf(" Total : %llu bytes\n", (unsigned long long)total);
+ Serial.printf(" Used : %llu bytes\n", (unsigned long long)used);
+ Serial.printf(" Free : %llu bytes\n", (unsigned long long)free);
+ Serial.println("----------------------------------------");
+}
+
+
+void SDManager::listFiles()
+{
+ if (!mounted)
+ {
+ Serial.println("[Storage] Cannot list files (not mounted)");
+ return;
+ }
+
+ Serial.println("Files on SD card:");
+ Serial.println("----------------------------------------");
+
+ char path[256];
+ snprintf(path, sizeof(path), "/");
+
+ listFilesRecursive(backend->fs(), path, sizeof(path), 0);
+
+ Serial.println("----------------------------------------");
+ Serial.println("End of listing");
+}
+
+
+void SDManager::listFilesRecursive(fs::FS& files, char* path, size_t pathSize, uint8_t depth)
+{
+ if (depth > STORAGE_LIST_MAX_DEPTH)
+ {
+ Serial.printf(" ... (max depth %u reached)\n", STORAGE_LIST_MAX_DEPTH);
+ return;
+ }
+
+ File dir = files.open(path);
+
+ if (!dir)
+ {
+ Serial.printf(" [error] cannot open: %s\n", path);
+ return;
+ }
+
+ if (!dir.isDirectory())
+ {
+ Serial.printf(" %s (%llu bytes)\n", path, (unsigned long long)dir.size());
+ dir.close();
+ return;
+ }
+
+ File entry;
+ while ((entry = dir.openNextFile()))
+ {
+ size_t base = strlen(path);
+
+ if (entry.isDirectory())
+ {
+ snprintf(path + base, pathSize - base, "/%s", entry.name());
+ Serial.printf(" %s/\n", path);
+ listFilesRecursive(files, path, pathSize, depth + 1);
+ path[base] = '\0';
+ }
+ else
+ {
+ snprintf(path + base, pathSize - base, "/%s", entry.name());
+ Serial.printf(" %s (%llu bytes)\n", path, (unsigned long long)entry.size());
+ }
+
+ entry.close();
+ }
+
+ dir.close();
+}
diff --git a/src/storage/sd_manager.h b/src/storage/sd_manager.h
new file mode 100644
index 0000000..5a9c5b8
--- /dev/null
+++ b/src/storage/sd_manager.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include
+#include
+
+// StorageCardType decouples the rest of the system from the SD/SD_MMC
+// library enum so nothing outside sd_manager.cpp needs to know which
+// hardware interface is in use.
+enum class StorageCardType
+{
+ None,
+ MMC,
+ SD,
+ SDHC,
+ Unknown
+};
+
+// Hardware abstraction for the SD card transport. The concrete backend
+// (SPI today, SDIO later) is selected at build time in storage_config.h.
+// Everything downstream (StorageTask, future DataLogger) talks only to
+// the `fs::FS` reference, so swapping SPI for SDIO touches no other code.
+class StorageBackend
+{
+public:
+ virtual ~StorageBackend() {}
+
+ virtual bool begin() = 0;
+ virtual void end() = 0;
+
+ virtual fs::FS& fs() = 0;
+
+ virtual StorageCardType cardType() = 0;
+ virtual uint64_t totalBytes() = 0;
+ virtual uint64_t usedBytes() = 0;
+};
+
+// Owns the selected backend, mounts the card, and provides the boot-time
+// info/listing report. The mount and any file access are blocking and are
+// expected to be driven from the dedicated StorageTask on core 0.
+class SDManager
+{
+public:
+ SDManager();
+
+ bool begin();
+ void end();
+
+ bool isMounted() const;
+
+ fs::FS& fs();
+
+ void printCardInfo();
+ void listFiles();
+
+private:
+ StorageBackend* backend;
+ bool mounted;
+
+ void listFilesRecursive(fs::FS& files, char* path, size_t pathSize, uint8_t depth);
+ static const char* cardTypeName(StorageCardType type);
+};
diff --git a/src/storage/storage_config.h b/src/storage/storage_config.h
new file mode 100644
index 0000000..aabb929
--- /dev/null
+++ b/src/storage/storage_config.h
@@ -0,0 +1,99 @@
+#pragma once
+
+// ============================================================================
+// Storage configuration
+// ============================================================================
+//
+// This header is the single place to configure how the Hub talks to the
+// SD card. Two hardware interfaces are supported by the Arduino-ESP32
+// framework, and both are implemented behind the StorageBackend interface
+// in sd_manager.cpp:
+//
+// STORAGE_IFACE_SPI - Uses the `SD` library (SPI protocol). This is the
+// bring-up path for the current breakout module,
+// which only breaks out the 4 SPI lines.
+// Max practical throughput: ~1-2 MB/s. NOT enough
+// for the 6-8 MB/s audio logging target.
+//
+// STORAGE_IFACE_SDMMC - Uses the `SD_MMC` library (SDIO protocol, the
+// SDMMC peripheral). This is the production path.
+// 4-bit mode @ 40 MHz (SDMMC_FREQ_HIGHSPEED) gives
+// roughly 8-12 MB/s, which comfortably meets the
+// target. REQUIRED for the final design.
+//
+// TO MIGRATE TO SDIO (the new SDIO-capable board that is being shipped):
+//
+// 1. Change STORAGE_IFACE below from STORAGE_IFACE_SPI to
+// STORAGE_IFACE_SDMMC.
+//
+// 2. Wire the SD card to the SDIO lines listed in the STORAGE_SDMMC_*
+// defines below. On the classic ESP32 the SDMMC peripheral is routed
+// through the GPIO matrix, so these pins can be changed to any free
+// GPIO simply by editing the defines.
+//
+// 3. Keep STORAGE_SDMMC_MODE_1BIT as `false` (4-bit bus is required for
+// the write throughput) and keep STORAGE_SDMMC_FORMAT_IF_FAILED as
+// `false` (a foreign/unformatted card must never be auto-formatted).
+//
+// 4. Everything downstream (StorageTask, SDManager, and the future
+// DataLogger) talks only through the `fs::FS` interface, so no other
+// code changes are needed.
+// ============================================================================
+
+// --- Backend selection ------------------------------------------------------
+
+#define STORAGE_IFACE_SPI 1
+#define STORAGE_IFACE_SDMMC 2
+
+// SPI until the SDIO-capable board arrives.
+#ifndef STORAGE_IFACE
+#define STORAGE_IFACE STORAGE_IFACE_SPI
+#endif
+
+// --- Common -----------------------------------------------------------------
+
+// Single canonical mount point so file paths never change when the backend
+// is switched (the SD lib defaults to "/sd", SD_MMC to "/sdcard").
+#define STORAGE_MOUNT_POINT "/sdcard"
+
+#define STORAGE_MAX_OPEN_FILES 5
+
+// --- SPI (bring-up only) ----------------------------------------------------
+
+// Default VSPI pins on the classic ESP32 DevKitC (variant/pins_arduino.h).
+#define STORAGE_SPI_CS 5
+#define STORAGE_SPI_SCK 18
+#define STORAGE_SPI_MOSI 23
+#define STORAGE_SPI_MISO 19
+
+// 10 MHz is a reliable default for breadboard/jumper-wire bring-up. The SD
+// init handshake always runs at 400 kHz regardless (see sd_diskio.cpp), so a
+// mount failure is NOT a frequency problem - check power, wiring, pull-ups
+// and card seating first. Raise to 20 MHz once the wiring is proven.
+#define STORAGE_SPI_FREQ 10000000UL
+
+// --- SDIO / SD_MMC (production, 4-bit) --------------------------------------
+
+// Default ESP32 SDMMC slot-1 pins (GPIO matrix, freely re-routable).
+#define STORAGE_SDMMC_CLK 6
+#define STORAGE_SDMMC_CMD 11
+#define STORAGE_SDMMC_D0 7
+#define STORAGE_SDMMC_D1 8
+#define STORAGE_SDMMC_D2 9
+#define STORAGE_SDMMC_D3 10
+
+// false = 4-bit wide bus (required for >8 MB/s). Do NOT enable 1-bit mode.
+#define STORAGE_SDMMC_MODE_1BIT false
+
+// NEVER auto-format: an unformatted/foreign card must never be destroyed.
+#define STORAGE_SDMMC_FORMAT_IF_FAILED false
+
+// 40 MHz == SDMMC_FREQ_HIGHSPEED. Written as a literal to keep this header
+// free of driver includes.
+#define STORAGE_SDMMC_FREQ_HZ 40000000
+
+// --- Boot-time file listing -------------------------------------------------
+
+// Maximum directory depth printed during the recursive boot listing. Guards
+// the StorageTask stack against pathological directory nesting.
+#define STORAGE_LIST_MAX_DEPTH 10
diff --git a/src/tasks/storage_task.cpp b/src/tasks/storage_task.cpp
new file mode 100644
index 0000000..2946d60
--- /dev/null
+++ b/src/tasks/storage_task.cpp
@@ -0,0 +1,72 @@
+#include "storage_task.h"
+
+
+StorageTask::StorageTask(SDManager& manager)
+:
+storage(manager),
+taskHandle(nullptr)
+{
+}
+
+
+void StorageTask::start()
+{
+ // Dedicated task on core 0 so SD card work never blocks the services
+ // running on core 1 (SystemTask / DiagnosticsTask). The 8 KB stack is
+ // sized for the recursive boot-time file listing; the eventual
+ // producer/consumer logging loop will also live here.
+ xTaskCreatePinnedToCore(
+ taskEntry,
+ "StorageTask",
+ 8192,
+ this,
+ 3,
+ &taskHandle,
+ 0
+ );
+}
+
+
+void StorageTask::taskEntry(void* parameter)
+{
+ StorageTask* task =
+ static_cast(parameter);
+
+ task->run();
+}
+
+
+void StorageTask::run()
+{
+ Serial.println("[Storage] Initializing SD card...");
+
+ // Retry the mount until it succeeds. This keeps the card dead-pin-capable:
+ // reseating the card, fixing a wire, or powering the module will bring it
+ // up without rebooting the Hub. (The SD init handshake runs at 400 kHz, so
+ // a failure here is wiring/power/seating/pull-up related, not speed.)
+ while (!storage.begin())
+ {
+ Serial.println("[Storage] SD card mount FAILED.");
+ Serial.println("[Storage] Check: 3.3V power + common ground, CS/SCK/MOSI/MISO wiring,");
+ Serial.println("[Storage] card fully seated (click), and module pull-ups.");
+ Serial.println("[Storage] Retrying in 5 s...");
+ vTaskDelay(pdMS_TO_TICKS(5000));
+ }
+
+ storage.printCardInfo();
+ storage.listFiles();
+
+ TickType_t lastWake =
+ xTaskGetTickCount();
+
+ while (true)
+ {
+ // Future integration point: a producer/consumer queue will feed
+ // audio data here to be flushed to the card. For now the task
+ // simply idles while the system runs.
+ vTaskDelayUntil(
+ &lastWake,
+ pdMS_TO_TICKS(1000)
+ );
+ }
+}
diff --git a/src/tasks/storage_task.h b/src/tasks/storage_task.h
new file mode 100644
index 0000000..4b65e2f
--- /dev/null
+++ b/src/tasks/storage_task.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include
+
+#include "../storage/sd_manager.h"
+
+
+class StorageTask
+{
+public:
+ StorageTask(SDManager& manager);
+
+ void start();
+
+private:
+ static void taskEntry(void* parameter);
+ void run();
+
+ SDManager& storage;
+
+ TaskHandle_t taskHandle = nullptr;
+};