From 0014e8697f8de359f83ef11d3344804259a8b481 Mon Sep 17 00:00:00 2001 From: bionickatana Date: Sun, 9 Aug 2026 18:34:23 -0600 Subject: [PATCH] Basic working SD card.' --- src/core/storage_state.cpp | 67 +++++++++++++++++++++ src/core/storage_state.h | 35 +++++++++++ src/main.cpp | 6 +- src/services/web_service.cpp | 103 ++++++++++++++++++++++++++++----- src/services/web_service.h | 4 +- src/storage/sd_manager.cpp | 109 +++++++++++++++++++++++++++++++++++ src/storage/sd_manager.h | 11 ++++ src/storage/storage_config.h | 15 +++++ src/tasks/storage_task.cpp | 32 +++++++++- src/tasks/storage_task.h | 4 +- 10 files changed, 364 insertions(+), 22 deletions(-) create mode 100644 src/core/storage_state.cpp create mode 100644 src/core/storage_state.h diff --git a/src/core/storage_state.cpp b/src/core/storage_state.cpp new file mode 100644 index 0000000..b1da4f6 --- /dev/null +++ b/src/core/storage_state.cpp @@ -0,0 +1,67 @@ +#include "storage_state.h" + + +void StorageState::begin() +{ + current.mounted = false; + current.timestamp = 0; + current.totalMB = 0; + current.usedMB = 0; + current.freeMB = 0; + current.writeSpeedBps = 0; + current.cardType[0] = '\0'; +} + + +void StorageState::setMounted(bool mounted) +{ + current.mounted = mounted; + current.timestamp = millis(); +} + + +void StorageState::setCardType(const char* type) +{ + size_t i = 0; + for (i = 0; i < sizeof(current.cardType) - 1 && type[i] != '\0'; i++) + { + current.cardType[i] = type[i]; + } + current.cardType[i] = '\0'; +} + + +void StorageState::setCapacity(uint64_t totalBytes, uint64_t usedBytes) +{ + uint64_t freeBytes = (totalBytes > usedBytes) ? (totalBytes - usedBytes) : 0; + + current.totalMB = (uint32_t)(totalBytes >> 20); + current.usedMB = (uint32_t)(usedBytes >> 20); + current.freeMB = (uint32_t)(freeBytes >> 20); +} + + +void StorageState::setWriteSpeedBps(uint32_t bytesPerSecond) +{ + current.writeSpeedBps = bytesPerSecond; +} + + +StorageSnapshot StorageState::getCurrent() +{ + StorageSnapshot snapshot; + + snapshot.mounted = current.mounted; + snapshot.timestamp = current.timestamp; + snapshot.totalMB = current.totalMB; + snapshot.usedMB = current.usedMB; + snapshot.freeMB = current.freeMB; + snapshot.writeSpeedBps = current.writeSpeedBps; + + for (size_t i = 0; i < sizeof(snapshot.cardType); i++) + { + snapshot.cardType[i] = current.cardType[i]; + } + + return snapshot; +} diff --git a/src/core/storage_state.h b/src/core/storage_state.h new file mode 100644 index 0000000..326ad78 --- /dev/null +++ b/src/core/storage_state.h @@ -0,0 +1,35 @@ +#pragma once + +#include + + +// Cross-task snapshot of the SD card, filled by the StorageTask (core 0) and +// read by the WebService (core 1) for the dashboard broadcast. Values are +// stored as 32-bit quantities (MB / Bps) so each field reads atomically. +struct StorageSnapshot +{ + bool mounted; + uint32_t timestamp; + uint32_t totalMB; + uint32_t usedMB; + uint32_t freeMB; + uint32_t writeSpeedBps; + char cardType[16]; +}; + + +class StorageState +{ +public: + void begin(); + + void setMounted(bool mounted); + void setCardType(const char* type); + void setCapacity(uint64_t totalBytes, uint64_t usedBytes); + void setWriteSpeedBps(uint32_t bytesPerSecond); + + StorageSnapshot getCurrent(); + +private: + volatile StorageSnapshot current; +}; diff --git a/src/main.cpp b/src/main.cpp index 7bbd11d..a9e2f4e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "core/dashboard_state.h" #include "core/diagnostics_state.h" +#include "core/storage_state.h" #include "services/service_manager.h" #include "services/wifi_service.h" @@ -20,13 +21,14 @@ DashboardState dashboardState; DiagnosticsState diagnosticsState; +StorageState storageState; // Simple service scheduler nice for grouping tasks ServiceManager services; WiFiService wifi; OTAService ota; -WebService web(dashboardState, diagnosticsState); +WebService web(dashboardState, diagnosticsState, storageState); // Actual FreeRTOS tasks that are scheduled @@ -35,7 +37,7 @@ SystemTask systemTask(services); DiagnosticsTask diagnosticsTask(diagnosticsState); SDManager sdManager; -StorageTask storageTask(sdManager); +StorageTask storageTask(sdManager, storageState); void setup() { diff --git a/src/services/web_service.cpp b/src/services/web_service.cpp index c108139..7eea953 100644 --- a/src/services/web_service.cpp +++ b/src/services/web_service.cpp @@ -1,10 +1,11 @@ #include "web_service.h" -WebService::WebService(DashboardState& state, DiagnosticsState& diag_state) +WebService::WebService(DashboardState& state, DiagnosticsState& diag_state, StorageState& storage_state) : Service("Web", 10), dashboardState(state), diagnosticsState(diag_state), + storageState(storage_state), server(80), webSocket(81) { @@ -64,6 +65,11 @@ h1 { font-size: 28px; margin-top: 20px; } + +#sd_status { + font-size: 24px; + font-weight: bold; +} @@ -104,23 +143,27 @@ window.onload = connectWebSocket;

ESP32 Dashboard OTA

-

Device uptime:

-
Loading...
+
Device uptime: Loading...

Hub Diagnostics:

-

Free heap:

-
Loading...
-

Minimum free heap:

-
Loading...
-

CPU Frequency:

-
Loading...
+
Free heap: Loading...
+
Minimum free heap: Loading...
+
CPU Frequency: Loading...
+
+ +
+

SD Storage:

+
Status: Loading...
+
Card type: Loading...
+
Space left: Loading...
+
Estimated write speed: Loading...
-Firmware Version:
Loading...
+
Firmware Version: Loading...
)rawliteral"; @@ -128,6 +171,7 @@ Firmware Version:
Loading...
void WebService::broadcastState() { DiagnosticSample diagnostics = diagnosticsState.getCurrent(); + StorageSnapshot storage = storageState.getCurrent(); String json = "{"; // Opening system tag: @@ -161,6 +205,35 @@ void WebService::broadcastState() json += "}"; // Clost diagnostic tag + // Opening storage tag: + json += ",\"storage\":{"; + + json += "\"mounted\":\""; + json += storage.mounted ? "true" : "false"; + json += "\","; + + json += "\"card_type\":\""; + json += storage.cardType; + json += "\","; + + json += "\"total_mb\":\""; + json += storage.totalMB; + json += "\","; + + json += "\"free_mb\":\""; + json += storage.freeMB; + json += "\","; + + json += "\"used_mb\":\""; + json += storage.usedMB; + json += "\","; + + json += "\"write_speed_bps\":\""; + json += storage.writeSpeedBps; + json += "\""; + + json += "}"; // Close storage tag + // Final close bracket json += "}"; diff --git a/src/services/web_service.h b/src/services/web_service.h index 8182614..e2a08b1 100644 --- a/src/services/web_service.h +++ b/src/services/web_service.h @@ -7,12 +7,13 @@ #include "../core/dashboard_state.h" #include "../core/diagnostics_state.h" +#include "../core/storage_state.h" class WebService : public Service { public: - WebService(DashboardState& state, DiagnosticsState& diag_state); + WebService(DashboardState& state, DiagnosticsState& diag_state, StorageState& storage_state); void begin() override; void update() override; @@ -24,6 +25,7 @@ private: DashboardState& dashboardState; DiagnosticsState& diagnosticsState; + StorageState& storageState; void handleWebSocketMessage(uint8_t clientNum, uint8_t *payload, size_t length); void broadcastState(); diff --git a/src/storage/sd_manager.cpp b/src/storage/sd_manager.cpp index f8579fb..416e5ca 100644 --- a/src/storage/sd_manager.cpp +++ b/src/storage/sd_manager.cpp @@ -228,6 +228,115 @@ fs::FS& SDManager::fs() } +StorageCardType SDManager::cardType() const +{ + if (mounted && backend != nullptr) + { + return backend->cardType(); + } + return StorageCardType::None; +} + + +const char* SDManager::cardTypeName() const +{ + return cardTypeName(cardType()); +} + + +uint64_t SDManager::totalBytes() const +{ + if (mounted && backend != nullptr) + { + return backend->totalBytes(); + } + return 0; +} + + +uint64_t SDManager::usedBytes() const +{ + if (mounted && backend != nullptr) + { + return backend->usedBytes(); + } + return 0; +} + + +uint64_t SDManager::freeBytes() const +{ + uint64_t total = totalBytes(); + uint64_t used = usedBytes(); + return (total > used) ? (total - used) : 0; +} + + +uint32_t SDManager::measureWriteSpeed() +{ + if (!mounted || backend == nullptr) + { + return 0; + } + + fs::FS& files = backend->fs(); + + const char* scratchPath = STORAGE_SPEED_MEASURE_PATH; + const size_t bufferSize = 16 * 1024; + + // Remove any leftover from a previous crashed run. + files.remove(scratchPath); + + uint8_t* buffer = (uint8_t*)malloc(bufferSize); + if (buffer == nullptr) + { + return 0; + } + + memset(buffer, 0xA5, bufferSize); + + File file = files.open(scratchPath, FILE_WRITE); + if (!file) + { + free(buffer); + return 0; + } + + uint32_t remaining = STORAGE_SPEED_MEASURE_BYTES; + uint32_t startUs = micros(); + + while (remaining > 0) + { + size_t toWrite = (remaining < bufferSize) ? remaining : bufferSize; + size_t written = file.write(buffer, toWrite); + + if (written == 0) + { + break; + } + + remaining -= written; + } + + file.close(); + + uint32_t elapsedUs = micros() - startUs; + Serial.print("Elapsed microseconds: "); + Serial.println(elapsedUs); + + files.remove(scratchPath); + free(buffer); + + if (remaining != 0 || elapsedUs == 0) + { + return 0; + } + + uint64_t writtenBytes = (uint64_t)STORAGE_SPEED_MEASURE_BYTES - remaining; + return (uint32_t)((writtenBytes * 1000000ULL) / elapsedUs); +} + + const char* SDManager::cardTypeName(StorageCardType type) { switch (type) diff --git a/src/storage/sd_manager.h b/src/storage/sd_manager.h index 5a9c5b8..dd51d9d 100644 --- a/src/storage/sd_manager.h +++ b/src/storage/sd_manager.h @@ -49,6 +49,17 @@ public: fs::FS& fs(); + StorageCardType cardType() const; + const char* cardTypeName() const; + uint64_t totalBytes() const; + uint64_t usedBytes() const; + uint64_t freeBytes() const; + + // Writes a scratch file of STORAGE_SPEED_MEASURE_BYTES and returns the + // achieved write throughput in bytes/second (0 on failure). The scratch + // file is deleted before returning. + uint32_t measureWriteSpeed(); + void printCardInfo(); void listFiles(); diff --git a/src/storage/storage_config.h b/src/storage/storage_config.h index aabb929..c488828 100644 --- a/src/storage/storage_config.h +++ b/src/storage/storage_config.h @@ -97,3 +97,18 @@ // Maximum directory depth printed during the recursive boot listing. Guards // the StorageTask stack against pathological directory nesting. #define STORAGE_LIST_MAX_DEPTH 10 + +// --- Write-speed estimation ------------------------------------------------- + +// Path of the temporary scratch file used to measure write throughput. It is +// created, written, measured, then deleted, so it never appears in listings. +#define STORAGE_SPEED_MEASURE_PATH STORAGE_MOUNT_POINT "/.writespeed.tmp" + +// Size of the scratch file written per measurement (bytes). +#define STORAGE_SPEED_MEASURE_BYTES (0.5 * 1024 * 1024) + +// How often to re-measure write speed (ms). This is a bring-up ESTIMATE only: +// it writes STORAGE_SPEED_MEASURE_BYTES to the card on every tick. Once real +// logging exists, throughput should be derived from actual logging writes and +// this benchmark disabled by setting the interval to 0 (measure once at boot). +#define STORAGE_SPEED_MEASURE_INTERVAL_MS 0 diff --git a/src/tasks/storage_task.cpp b/src/tasks/storage_task.cpp index 2946d60..fa6623c 100644 --- a/src/tasks/storage_task.cpp +++ b/src/tasks/storage_task.cpp @@ -1,9 +1,12 @@ #include "storage_task.h" +#include "../storage/storage_config.h" -StorageTask::StorageTask(SDManager& manager) + +StorageTask::StorageTask(SDManager& manager, StorageState& state) : storage(manager), +storageState(state), taskHandle(nullptr) { } @@ -40,6 +43,11 @@ void StorageTask::run() { Serial.println("[Storage] Initializing SD card..."); + storageState.begin(); + + // While the mount has not succeeded the dashboard must show "SD not found". + storageState.setMounted(false); + // 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 @@ -53,17 +61,35 @@ void StorageTask::run() vTaskDelay(pdMS_TO_TICKS(5000)); } + storageState.setMounted(true); + storageState.setCardType(storage.cardTypeName()); + storage.printCardInfo(); storage.listFiles(); + // Initial write-speed estimate for the dashboard. + storageState.setWriteSpeedBps(storage.measureWriteSpeed()); + storageState.setCapacity(storage.totalBytes(), storage.usedBytes()); + TickType_t lastWake = xTaskGetTickCount(); + uint32_t lastSpeedMeasure = millis(); + while (true) { + // Periodically re-estimate write speed and refresh capacity so the + // dashboard stays current. An interval of 0 disables re-measuring. + if (STORAGE_SPEED_MEASURE_INTERVAL_MS != 0 && + millis() - lastSpeedMeasure >= STORAGE_SPEED_MEASURE_INTERVAL_MS) + { + lastSpeedMeasure = millis(); + storageState.setWriteSpeedBps(storage.measureWriteSpeed()); + storageState.setCapacity(storage.totalBytes(), storage.usedBytes()); + } + // 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. + // audio data here to be flushed to the card. vTaskDelayUntil( &lastWake, pdMS_TO_TICKS(1000) diff --git a/src/tasks/storage_task.h b/src/tasks/storage_task.h index 4b65e2f..485e255 100644 --- a/src/tasks/storage_task.h +++ b/src/tasks/storage_task.h @@ -2,13 +2,14 @@ #include +#include "../core/storage_state.h" #include "../storage/sd_manager.h" class StorageTask { public: - StorageTask(SDManager& manager); + StorageTask(SDManager& manager, StorageState& state); void start(); @@ -17,6 +18,7 @@ private: void run(); SDManager& storage; + StorageState& storageState; TaskHandle_t taskHandle = nullptr; };