Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0014e8697f | |||
| 8a44887b6f | |||
| 243419b401 | |||
| 7ad00e452c | |||
| e411c934d5 |
+4
-2
@@ -14,7 +14,9 @@ board = esp32dev
|
|||||||
framework = arduino
|
framework = arduino
|
||||||
lib_deps =
|
lib_deps =
|
||||||
WebSockets
|
WebSockets
|
||||||
|
;board_build.f_cpu = 160000000L
|
||||||
|
;build_type = debug
|
||||||
|
|
||||||
; upload via OTA
|
; upload via OTA
|
||||||
upload_protocol = espota
|
;upload_protocol = espota
|
||||||
upload_port = 192.168.4.1
|
;upload_port = 192.168.4.1
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// Contains configuration constants:
|
// Contains configuration constants:
|
||||||
|
|
||||||
#define FIRMWARE_VERSION "1.0.3"
|
#define FIRMWARE_VERSION "1.0.6"
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include "diagnostics_state.h"
|
||||||
|
|
||||||
|
|
||||||
|
void DiagnosticsState::update()
|
||||||
|
{
|
||||||
|
current.timestamp = millis();
|
||||||
|
current.freeHeap =
|
||||||
|
ESP.getFreeHeap();
|
||||||
|
|
||||||
|
current.minimumFreeHeap =
|
||||||
|
ESP.getMinFreeHeap();
|
||||||
|
|
||||||
|
current.cpuFrequency =
|
||||||
|
ESP.getCpuFreqMHz();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DiagnosticSample DiagnosticsState::getCurrent()
|
||||||
|
{
|
||||||
|
DiagnosticSample sample;
|
||||||
|
sample.timestamp = current.timestamp;
|
||||||
|
sample.freeHeap = current.freeHeap;
|
||||||
|
sample.minimumFreeHeap = current.minimumFreeHeap;
|
||||||
|
sample.cpuFrequency = current.cpuFrequency;
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
|
||||||
|
#define DIAGNOSTIC_HISTORY_SIZE 60
|
||||||
|
|
||||||
|
|
||||||
|
struct DiagnosticSample
|
||||||
|
{
|
||||||
|
uint32_t timestamp;
|
||||||
|
uint32_t freeHeap;
|
||||||
|
uint32_t minimumFreeHeap;
|
||||||
|
uint32_t cpuFrequency;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticsState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
void update();
|
||||||
|
|
||||||
|
|
||||||
|
DiagnosticSample getCurrent();
|
||||||
|
//DiagnosticSample getHistory(uint8_t index);
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
volatile DiagnosticSample current;
|
||||||
|
//DiagnosticSample history[DIAGNOSTIC_HISTORY_SIZE];
|
||||||
|
//uint8_t historyIndex = 0;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
+16
-3
@@ -4,29 +4,40 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#include "core/dashboard_state.h"
|
#include "core/dashboard_state.h"
|
||||||
|
#include "core/diagnostics_state.h"
|
||||||
|
#include "core/storage_state.h"
|
||||||
|
|
||||||
#include "services/service_manager.h"
|
#include "services/service_manager.h"
|
||||||
#include "services/wifi_service.h"
|
#include "services/wifi_service.h"
|
||||||
#include "services/ota_service.h"
|
#include "services/ota_service.h"
|
||||||
#include "services/web_service.h"
|
#include "services/web_service.h"
|
||||||
|
|
||||||
|
#include "storage/sd_manager.h"
|
||||||
|
|
||||||
#include "tasks/system_task.h"
|
#include "tasks/system_task.h"
|
||||||
|
#include "tasks/diagnostics_task.h"
|
||||||
|
#include "tasks/storage_task.h"
|
||||||
|
|
||||||
|
|
||||||
DashboardState dashboardState;
|
DashboardState dashboardState;
|
||||||
|
DiagnosticsState diagnosticsState;
|
||||||
|
StorageState storageState;
|
||||||
|
|
||||||
|
// Simple service scheduler nice for grouping tasks
|
||||||
ServiceManager services;
|
ServiceManager services;
|
||||||
|
|
||||||
|
|
||||||
WiFiService wifi;
|
WiFiService wifi;
|
||||||
OTAService ota;
|
OTAService ota;
|
||||||
WebService web(dashboardState);
|
WebService web(dashboardState, diagnosticsState, storageState);
|
||||||
|
|
||||||
|
|
||||||
|
// Actual FreeRTOS tasks that are scheduled
|
||||||
SystemTask systemTask(services);
|
SystemTask systemTask(services);
|
||||||
|
|
||||||
|
DiagnosticsTask diagnosticsTask(diagnosticsState);
|
||||||
|
|
||||||
|
SDManager sdManager;
|
||||||
|
StorageTask storageTask(sdManager, storageState);
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
@@ -38,6 +49,8 @@ void setup()
|
|||||||
|
|
||||||
|
|
||||||
systemTask.start();
|
systemTask.start();
|
||||||
|
diagnosticsTask.start();
|
||||||
|
storageTask.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#include "web_service.h"
|
#include "web_service.h"
|
||||||
|
|
||||||
WebService::WebService(DashboardState& state)
|
WebService::WebService(DashboardState& state, DiagnosticsState& diag_state, StorageState& storage_state)
|
||||||
:
|
:
|
||||||
Service("Web", 10),
|
Service("Web", 10),
|
||||||
dashboardState(state),
|
dashboardState(state),
|
||||||
|
diagnosticsState(diag_state),
|
||||||
|
storageState(storage_state),
|
||||||
server(80),
|
server(80),
|
||||||
webSocket(81)
|
webSocket(81)
|
||||||
{
|
{
|
||||||
@@ -63,6 +65,11 @@ h1 {
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sd_status {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -81,8 +88,13 @@ function connectWebSocket() {
|
|||||||
socket.onmessage = function(event) {
|
socket.onmessage = function(event) {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
document.getElementById("uptime").innerHTML = data.uptime;
|
document.getElementById("uptime").innerHTML = "Uptime: " + data.system.uptime;
|
||||||
document.getElementById("firmware_version").innerHTML = data.version;
|
document.getElementById("firmware_version").innerHTML = "Firmware Version: " + data.system.version;
|
||||||
|
document.getElementById("free_heap").innerHTML = "Free Heap: " + data.diagnostics.free_heap;
|
||||||
|
document.getElementById("minimum_free_heap").innerHTML = "Minimum Free Heap: " + data.diagnostics.minimum_free_heap;
|
||||||
|
document.getElementById("cpu_frequency").innerHTML = "CPU Frequency: " + data.diagnostics.cpu_frequency + "MHz";
|
||||||
|
|
||||||
|
updateStorage(data.storage);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onclose = function() {
|
socket.onclose = function() {
|
||||||
@@ -91,6 +103,37 @@ function connectWebSocket() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytesMB(mb) {
|
||||||
|
const value = Number(mb);
|
||||||
|
if (value >= 1024) {
|
||||||
|
return (value / 1024).toFixed(2) + " GB";
|
||||||
|
}
|
||||||
|
return value.toFixed(0) + " MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStorage(storage) {
|
||||||
|
const sdStatus = document.getElementById("sd_status");
|
||||||
|
|
||||||
|
if (storage && storage.mounted === "true") {
|
||||||
|
sdStatus.innerHTML = "SD card found";
|
||||||
|
sdStatus.style.color = "#00ff99";
|
||||||
|
|
||||||
|
document.getElementById("sd_type").innerHTML = "Card Type: " + storage.card_type;
|
||||||
|
document.getElementById("sd_free").innerHTML = "Space left: " +
|
||||||
|
formatBytesMB(storage.free_mb) + " free of " + formatBytesMB(storage.total_mb) +
|
||||||
|
" (" + Math.round(storage.used_mb / storage.total_mb * 100) + "% used)";
|
||||||
|
document.getElementById("sd_speed").innerHTML = "Estimated write speed: " +
|
||||||
|
(storage.write_speed_bps / 1048576).toFixed(2) + " MB/s";
|
||||||
|
} else {
|
||||||
|
sdStatus.innerHTML = "SD not found";
|
||||||
|
sdStatus.style.color = "red";
|
||||||
|
|
||||||
|
document.getElementById("sd_type").innerHTML = "-";
|
||||||
|
document.getElementById("sd_free").innerHTML = "-";
|
||||||
|
document.getElementById("sd_speed").innerHTML = "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.onload = connectWebSocket;
|
window.onload = connectWebSocket;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -100,20 +143,40 @@ window.onload = connectWebSocket;
|
|||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>ESP32 Dashboard OTA</h1>
|
<h1>ESP32 Dashboard OTA</h1>
|
||||||
<p>Device uptime:</p>
|
<div id="uptime">Device uptime: Loading...</div>
|
||||||
<div id="uptime">Loading...</div>
|
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
|
||||||
Firmware Version: <div id="firmware_version">Loading...</div>
|
<div class="card">
|
||||||
</footer>
|
<h1>Hub Diagnostics:</h1>
|
||||||
|
<div id=free_heap>Free heap: Loading...</div>
|
||||||
|
<div id=minimum_free_heap>Minimum free heap: Loading...</div>
|
||||||
|
<div id=cpu_frequency>CPU Frequency: Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h1>SD Storage:</h1>
|
||||||
|
<div id=sd_status>Status: Loading...</div>
|
||||||
|
<div id=sd_type>Card type: Loading...</div>
|
||||||
|
<div id=sd_free>Space left: Loading...</div>
|
||||||
|
<div id=sd_speed>Estimated write speed: Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
<footer>
|
||||||
|
<div id="firmware_version">Firmware Version: Loading...</div>
|
||||||
|
</footer>
|
||||||
</html>
|
</html>
|
||||||
)rawliteral";
|
)rawliteral";
|
||||||
|
|
||||||
void WebService::broadcastState()
|
void WebService::broadcastState()
|
||||||
{
|
{
|
||||||
|
DiagnosticSample diagnostics = diagnosticsState.getCurrent();
|
||||||
|
StorageSnapshot storage = storageState.getCurrent();
|
||||||
String json = "{";
|
String json = "{";
|
||||||
|
|
||||||
|
// Opening system tag:
|
||||||
|
json += "\"system\":{";
|
||||||
|
|
||||||
json += "\"uptime\":\"";
|
json += "\"uptime\":\"";
|
||||||
json += dashboardState.uptime;
|
json += dashboardState.uptime;
|
||||||
json += "\",";
|
json += "\",";
|
||||||
@@ -122,8 +185,61 @@ void WebService::broadcastState()
|
|||||||
json += dashboardState.firmwareVersion;
|
json += dashboardState.firmwareVersion;
|
||||||
json += "\"";
|
json += "\"";
|
||||||
|
|
||||||
|
json += "},"; // Close system tag
|
||||||
|
|
||||||
|
// Opening diagnostic tag:
|
||||||
|
json += "\"diagnostics\":{";
|
||||||
|
|
||||||
|
json += "\"free_heap\":\"";
|
||||||
|
json += diagnostics.freeHeap;
|
||||||
|
//json += ESP.getFreeHeap();
|
||||||
|
json += "\",";
|
||||||
|
|
||||||
|
json += "\"minimum_free_heap\":\"";
|
||||||
|
json += diagnostics.minimumFreeHeap;
|
||||||
|
json += "\",";
|
||||||
|
|
||||||
|
json += "\"cpu_frequency\":\"";
|
||||||
|
json += diagnostics.cpuFrequency;
|
||||||
|
json += "\"";
|
||||||
|
|
||||||
|
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 += "}";
|
json += "}";
|
||||||
|
|
||||||
|
|
||||||
|
//Serial.println(json);
|
||||||
webSocket.broadcastTXT(json);
|
webSocket.broadcastTXT(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +253,16 @@ void WebService::begin() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#ifdef DEBUGGING
|
||||||
|
// Prints out paths that are requested but not found.
|
||||||
|
server.onNotFound([this]() {
|
||||||
|
Serial.print("HTTP not found: ");
|
||||||
|
Serial.println(server.uri());
|
||||||
|
|
||||||
|
server.send(404, "text/plain", "Not found");
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
|
||||||
server.begin();
|
server.begin();
|
||||||
|
|
||||||
webSocket.begin();
|
webSocket.begin();
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
#include "service.h"
|
#include "service.h"
|
||||||
|
|
||||||
#include "../core/dashboard_state.h"
|
#include "../core/dashboard_state.h"
|
||||||
|
#include "../core/diagnostics_state.h"
|
||||||
|
#include "../core/storage_state.h"
|
||||||
|
|
||||||
class WebService : public Service {
|
class WebService : public Service {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
WebService(DashboardState& state);
|
WebService(DashboardState& state, DiagnosticsState& diag_state, StorageState& storage_state);
|
||||||
|
|
||||||
void begin() override;
|
void begin() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -20,7 +22,10 @@ private:
|
|||||||
|
|
||||||
WebServer server;
|
WebServer server;
|
||||||
WebSocketsServer webSocket;
|
WebSocketsServer webSocket;
|
||||||
DashboardState dashboardState;
|
|
||||||
|
DashboardState& dashboardState;
|
||||||
|
DiagnosticsState& diagnosticsState;
|
||||||
|
StorageState& storageState;
|
||||||
|
|
||||||
void handleWebSocketMessage(uint8_t clientNum, uint8_t *payload, size_t length);
|
void handleWebSocketMessage(uint8_t clientNum, uint8_t *payload, size_t length);
|
||||||
void broadcastState();
|
void broadcastState();
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
#include "sd_manager.h"
|
||||||
|
|
||||||
|
#include "storage_config.h"
|
||||||
|
|
||||||
|
#include <driver/gpio.h>
|
||||||
|
|
||||||
|
// 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 <SD.h>
|
||||||
|
#include <SD_MMC.h>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
private:
|
||||||
|
StorageBackend* backend;
|
||||||
|
bool mounted;
|
||||||
|
|
||||||
|
void listFilesRecursive(fs::FS& files, char* path, size_t pathSize, uint8_t depth);
|
||||||
|
static const char* cardTypeName(StorageCardType type);
|
||||||
|
};
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#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
|
||||||
|
|
||||||
|
// --- 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
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#include "diagnostics_task.h"
|
||||||
|
|
||||||
|
DiagnosticsTask::DiagnosticsTask(
|
||||||
|
DiagnosticsState& state
|
||||||
|
)
|
||||||
|
:
|
||||||
|
diagnostics(state)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DiagnosticsTask::start()
|
||||||
|
{
|
||||||
|
|
||||||
|
xTaskCreatePinnedToCore(
|
||||||
|
taskEntry,
|
||||||
|
"DiagnosticsTask",
|
||||||
|
4096,
|
||||||
|
this,
|
||||||
|
1,
|
||||||
|
&taskHandle,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DiagnosticsTask::taskEntry(void* parameter)
|
||||||
|
{
|
||||||
|
|
||||||
|
DiagnosticsTask* task =
|
||||||
|
static_cast<DiagnosticsTask*>(parameter);
|
||||||
|
|
||||||
|
|
||||||
|
task->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DiagnosticsTask::run()
|
||||||
|
{
|
||||||
|
|
||||||
|
TickType_t lastWake =
|
||||||
|
xTaskGetTickCount();
|
||||||
|
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
diagnostics.update();
|
||||||
|
|
||||||
|
vTaskDelayUntil(
|
||||||
|
&lastWake,
|
||||||
|
pdMS_TO_TICKS(1000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "../core/diagnostics_state.h"
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticsTask
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
DiagnosticsTask(
|
||||||
|
DiagnosticsState& state
|
||||||
|
);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
static void taskEntry(void* parameter);
|
||||||
|
void run();
|
||||||
|
|
||||||
|
DiagnosticsState& diagnostics;
|
||||||
|
|
||||||
|
TaskHandle_t taskHandle = nullptr;
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#include "storage_task.h"
|
||||||
|
|
||||||
|
#include "../storage/storage_config.h"
|
||||||
|
|
||||||
|
|
||||||
|
StorageTask::StorageTask(SDManager& manager, StorageState& state)
|
||||||
|
:
|
||||||
|
storage(manager),
|
||||||
|
storageState(state),
|
||||||
|
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<StorageTask*>(parameter);
|
||||||
|
|
||||||
|
task->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
vTaskDelayUntil(
|
||||||
|
&lastWake,
|
||||||
|
pdMS_TO_TICKS(1000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "../core/storage_state.h"
|
||||||
|
#include "../storage/sd_manager.h"
|
||||||
|
|
||||||
|
|
||||||
|
class StorageTask
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
StorageTask(SDManager& manager, StorageState& state);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void taskEntry(void* parameter);
|
||||||
|
void run();
|
||||||
|
|
||||||
|
SDManager& storage;
|
||||||
|
StorageState& storageState;
|
||||||
|
|
||||||
|
TaskHandle_t taskHandle = nullptr;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user