Basic working SD card.'
This commit is contained in:
@@ -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;
|
||||
};
|
||||
+4
-2
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -82,11 +88,13 @@ function connectWebSocket() {
|
||||
socket.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
document.getElementById("uptime").innerHTML = data.system.uptime;
|
||||
document.getElementById("firmware_version").innerHTML = data.system.version;
|
||||
document.getElementById("free_heap").innerHTML = data.diagnostics.free_heap;
|
||||
document.getElementById("minimum_free_heap").innerHTML = data.diagnostics.minimum_free_heap;
|
||||
document.getElementById("cpu_frequency").innerHTML = data.diagnostics.cpu_frequency;
|
||||
document.getElementById("uptime").innerHTML = "Uptime: " + data.system.uptime;
|
||||
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() {
|
||||
@@ -95,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;
|
||||
|
||||
</script>
|
||||
@@ -104,23 +143,27 @@ window.onload = connectWebSocket;
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>ESP32 Dashboard OTA</h1>
|
||||
<p>Device uptime:</p>
|
||||
<div id="uptime">Loading...</div>
|
||||
<div id="uptime">Device uptime: Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h1>Hub Diagnostics:</h1>
|
||||
<p>Free heap:</p>
|
||||
<div id=free_heap>Loading...</div>
|
||||
<p>Minimum free heap:</p>
|
||||
<div id=minimum_free_heap>Loading...</div>
|
||||
<p>CPU Frequency:</p>
|
||||
<div id=cpu_frequency>Loading...</div>
|
||||
<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>
|
||||
<footer>
|
||||
Firmware Version: <div id="firmware_version">Loading...</div>
|
||||
<div id="firmware_version">Firmware Version: Loading...</div>
|
||||
</footer>
|
||||
</html>
|
||||
)rawliteral";
|
||||
@@ -128,6 +171,7 @@ Firmware Version: <div id="firmware_version">Loading...</div>
|
||||
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 += "}";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user