in sev state
This commit is contained in:
+286
-43
@@ -2,14 +2,10 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
// ============================================================================
|
||||
// Skeleton implementation. The WAV metadata helpers below are complete;
|
||||
// everything else is stubbed with a TODO and the work is tracked in
|
||||
// docs/audio_logging.md section 14.
|
||||
// ============================================================================
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
|
||||
// --- WAV metadata helpers (complete) -----------------------------------------
|
||||
// --- WAV metadata helpers ----------------------------------------------------
|
||||
|
||||
void buildWavHeader(WavHeader& header,
|
||||
uint16_t numChannels,
|
||||
@@ -53,7 +49,7 @@ void finalizeWavHeader(uint32_t fileSize,
|
||||
DataLogger::DataLogger()
|
||||
:
|
||||
files(nullptr),
|
||||
pool(nullptr),
|
||||
poolData(nullptr),
|
||||
freeQ(nullptr),
|
||||
filledQ(nullptr),
|
||||
sessionSeq(0),
|
||||
@@ -68,103 +64,350 @@ bps(0),
|
||||
active(false),
|
||||
warning(false)
|
||||
{
|
||||
memset(chunks, 0, sizeof(chunks));
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::begin(fs::FS& files)
|
||||
{
|
||||
// TODO: allocate the chunk pool with heap_caps_malloc(MALLOC_CAP_DMA)
|
||||
// (STORAGE_LOG_POOL_SIZE x STORAGE_LOG_CHUNK_SIZE), create freeQ with all
|
||||
// chunks and filledQ empty (xQueueCreate), mkdir STORAGE_LOG_DIR.
|
||||
this->files = &files;
|
||||
return false;
|
||||
|
||||
// One contiguous DMA-capable block, sliced into STORAGE_LOG_POOL_SIZE
|
||||
// chunks so the SDMMC IDMA engine can read each chunk directly.
|
||||
poolData = (uint8_t*)heap_caps_malloc(
|
||||
STORAGE_LOG_POOL_SIZE * STORAGE_LOG_CHUNK_SIZE,
|
||||
MALLOC_CAP_DMA);
|
||||
|
||||
Serial.print("Allocating: ");
|
||||
Serial.print(STORAGE_LOG_POOL_SIZE * STORAGE_LOG_CHUNK_SIZE);
|
||||
Serial.println(" bytes of data");
|
||||
if (poolData == nullptr)
|
||||
{
|
||||
Serial.println("[Logger] Failed to allocate DMA chunk pool");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < STORAGE_LOG_POOL_SIZE; i++)
|
||||
{
|
||||
chunks[i].data = poolData + (uint32_t)i * STORAGE_LOG_CHUNK_SIZE;
|
||||
chunks[i].capacity = STORAGE_LOG_CHUNK_SIZE;
|
||||
chunks[i].length = 0;
|
||||
chunks[i].sequence = 0;
|
||||
}
|
||||
|
||||
freeQ = xQueueCreate(STORAGE_LOG_POOL_SIZE, sizeof(AudioChunk*));
|
||||
filledQ = xQueueCreate(STORAGE_LOG_POOL_SIZE, sizeof(AudioChunk*));
|
||||
|
||||
if (freeQ == nullptr || filledQ == nullptr)
|
||||
{
|
||||
Serial.println("[Logger] Failed to create chunk queues");
|
||||
heap_caps_free(poolData);
|
||||
poolData = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < STORAGE_LOG_POOL_SIZE; i++)
|
||||
{
|
||||
AudioChunk* c = &chunks[i];
|
||||
xQueueSend(freeQ, &c, 0);
|
||||
}
|
||||
|
||||
files.mkdir(STORAGE_LOG_DIR);
|
||||
|
||||
pinMode(STORAGE_WARN_LED_GPIO, OUTPUT);
|
||||
digitalWrite(STORAGE_WARN_LED_GPIO,
|
||||
STORAGE_WARN_LED_ACTIVE_HIGH ? LOW : HIGH);
|
||||
|
||||
Serial.printf("[Logger] Pool ready: %d chunks x %u bytes\n",
|
||||
STORAGE_LOG_POOL_SIZE, STORAGE_LOG_CHUNK_SIZE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void DataLogger::end()
|
||||
{
|
||||
// TODO: closeSession(), free the pool (heap_caps_free), delete the queues.
|
||||
closeSession();
|
||||
|
||||
if (freeQ) { vQueueDelete(freeQ); freeQ = nullptr; }
|
||||
if (filledQ) { vQueueDelete(filledQ); filledQ = nullptr; }
|
||||
|
||||
if (poolData)
|
||||
{
|
||||
heap_caps_free(poolData);
|
||||
poolData = nullptr;
|
||||
}
|
||||
|
||||
active = false;
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::openSession()
|
||||
{
|
||||
// TODO: build "rec_<uptimeSeconds>_<sessionSeq>.wav" under
|
||||
// STORAGE_LOG_DIR (skip forward if the name exists), open FILE_WRITE,
|
||||
// write the 44-byte header with buildWavHeader(dataSize=0).
|
||||
return false;
|
||||
if (files == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build a fresh name, skipping any that already exist so we never
|
||||
// truncate an earlier recording from a same-second boot.
|
||||
char path[sizeof(filePath)];
|
||||
do
|
||||
{
|
||||
snprintf(path, sizeof(path), "%s/rec_%lu_%lu.wav",
|
||||
STORAGE_LOG_DIR,
|
||||
(unsigned long)(millis() / 1000),
|
||||
(unsigned long)sessionSeq++);
|
||||
}
|
||||
while (files->exists(path));
|
||||
|
||||
File f = files->open(path, FILE_WRITE);
|
||||
if (!f)
|
||||
{
|
||||
Serial.println("[Logger] Failed to open session file (card full?)");
|
||||
return false;
|
||||
}
|
||||
|
||||
WavHeader header;
|
||||
buildWavHeader(header,
|
||||
STORAGE_AUDIO_CHANNELS,
|
||||
STORAGE_AUDIO_SAMPLE_RATE_HZ,
|
||||
STORAGE_AUDIO_BITS,
|
||||
0);
|
||||
|
||||
if (f.write((const uint8_t*)&header, sizeof(header)) != sizeof(header))
|
||||
{
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
strncpy(filePath, path, sizeof(filePath));
|
||||
filePath[sizeof(filePath) - 1] = '\0';
|
||||
|
||||
file = f;
|
||||
active = true;
|
||||
bytesThisFile = sizeof(header);
|
||||
|
||||
Serial.printf("[Logger] Session open: %s\n", filePath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::closeSession()
|
||||
{
|
||||
// TODO: finalizeWavHeader(file.size(), ...) -> patch offsets 4 and 40,
|
||||
// file.flush(), file.close().
|
||||
return false;
|
||||
if (!active || !file)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Patch the RIFF/data sizes so the file is valid even if the run was
|
||||
// short. On unclean power-off the stale sizes are ignored by EOF-reading
|
||||
// decoders (see docs/audio_logging.md).
|
||||
uint32_t riffSize, dataSize;
|
||||
finalizeWavHeader(file.size(), riffSize, dataSize);
|
||||
|
||||
file.seek(4);
|
||||
file.write((const uint8_t*)&riffSize, 4);
|
||||
file.seek(40);
|
||||
file.write((const uint8_t*)&dataSize, 4);
|
||||
|
||||
file.flush();
|
||||
file.close();
|
||||
|
||||
active = false;
|
||||
|
||||
Serial.printf("[Logger] Session closed: %s (%llu bytes)\n",
|
||||
filePath, (unsigned long long)bytesThisFile);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::rotateIfNeeded()
|
||||
{
|
||||
// TODO: if active && bytesThisFile >= STORAGE_LOG_ROTATE_BYTES:
|
||||
// closeSession(); sessionSeq++; openSession().
|
||||
return false;
|
||||
if (active && bytesThisFile >= STORAGE_LOG_ROTATE_BYTES)
|
||||
{
|
||||
closeSession();
|
||||
return openSession();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::hasSession() const
|
||||
{
|
||||
return active;
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::isEmpty() const
|
||||
{
|
||||
return filledQ == nullptr || uxQueueMessagesWaiting(filledQ) == 0;
|
||||
}
|
||||
|
||||
|
||||
AudioChunk* DataLogger::acquireChunk(TickType_t timeout)
|
||||
{
|
||||
// TODO: xQueueReceive(freeQ, &chunk, timeout). On timeout, call
|
||||
// dropOldestChunk() and return its chunk so the producer keeps streaming.
|
||||
return nullptr;
|
||||
if (freeQ == nullptr)
|
||||
{
|
||||
// Logger pool not created yet (recording pressed during boot).
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AudioChunk* c = nullptr;
|
||||
|
||||
if (xQueueReceive(freeQ, &c, timeout) != pdTRUE)
|
||||
{
|
||||
// Pool exhausted: drop the oldest queued chunk and hand its buffer to
|
||||
// the producer so recording never stalls.
|
||||
c = dropOldestChunk();
|
||||
}
|
||||
|
||||
if (c != nullptr)
|
||||
{
|
||||
c->length = 0;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
void DataLogger::submitChunk(AudioChunk* chunk)
|
||||
{
|
||||
// TODO: xQueueSend(filledQ, &chunk, ...). Must preserve stream order.
|
||||
if (chunk == nullptr || filledQ == nullptr) return;
|
||||
xQueueSend(filledQ, &chunk, portMAX_DELAY);
|
||||
}
|
||||
|
||||
|
||||
AudioChunk* DataLogger::nextChunk(TickType_t timeout)
|
||||
{
|
||||
// TODO: xQueueReceive(filledQ, &chunk, timeout).
|
||||
return nullptr;
|
||||
if (filledQ == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AudioChunk* c = nullptr;
|
||||
xQueueReceive(filledQ, &c, timeout);
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
bool DataLogger::writeChunk(AudioChunk* chunk)
|
||||
{
|
||||
// TODO: if (!active) return false;
|
||||
// n = file.write(chunk->data, chunk->length);
|
||||
// bytesThisFile += n; totalBytes += n; totalChunks++;
|
||||
// feed the writeSpeedBps window (windowBytes/windowStartMs);
|
||||
// file.flush() every STORAGE_LOG_FLUSH_BYTES;
|
||||
// return n == chunk->length; (card failure -> fatal)
|
||||
return false;
|
||||
if (chunk == nullptr || chunk->length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!active && !openSession())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t n = file.write(chunk->data, chunk->length);
|
||||
|
||||
if (n != chunk->length)
|
||||
{
|
||||
Serial.println("[Logger] Card write FAILED (partial write)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesThisFile += n;
|
||||
totalBytes += n;
|
||||
totalChunks++;
|
||||
|
||||
updateBpsWindow(n, millis());
|
||||
|
||||
// Periodic f_sync so an unclean power-off loses at most this window.
|
||||
if (bytesThisFile - sizeof(WavHeader) >= STORAGE_LOG_FLUSH_BYTES &&
|
||||
(bytesThisFile - sizeof(WavHeader)) % STORAGE_LOG_FLUSH_BYTES < n)
|
||||
{
|
||||
file.flush();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void DataLogger::releaseChunk(AudioChunk* chunk)
|
||||
{
|
||||
// TODO: chunk->length = 0; xQueueSend(freeQ, &chunk, ...).
|
||||
if (chunk == nullptr || freeQ == nullptr) return;
|
||||
|
||||
chunk->length = 0;
|
||||
|
||||
if (warning && uxQueueSpacesAvailable(freeQ) >= STORAGE_LOG_POOL_SIZE / 2)
|
||||
{
|
||||
setOverflowWarning(false);
|
||||
}
|
||||
|
||||
xQueueSend(freeQ, &chunk, portMAX_DELAY);
|
||||
}
|
||||
|
||||
|
||||
AudioChunk* DataLogger::dropOldestChunk()
|
||||
{
|
||||
// TODO: xQueueReceive from the BACK of filledQ without writing, count it,
|
||||
// setOverflowWarning(true). Called by acquireChunk on timeout.
|
||||
return nullptr;
|
||||
AudioChunk* c = nullptr;
|
||||
|
||||
if (filledQ == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (xQueueReceive(filledQ, &c, 0) == pdTRUE)
|
||||
{
|
||||
totalDropped++;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - lastDropPrintMs >= 5000)
|
||||
{
|
||||
lastDropPrintMs = now;
|
||||
Serial.printf("[Logger] WARNING: dropping oldest chunk "
|
||||
"(SD behind), %lu dropped total\n",
|
||||
(unsigned long)totalDropped);
|
||||
}
|
||||
|
||||
setOverflowWarning(true);
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
void DataLogger::setOverflowWarning(bool overflowing)
|
||||
{
|
||||
// TODO: rate-limited Serial line with totalDropped + bytes behind;
|
||||
// digitalWrite(STORAGE_WARN_LED_GPIO, ...) using
|
||||
// STORAGE_WARN_LED_ACTIVE_HIGH.
|
||||
(void)overflowing;
|
||||
if (overflowing == warning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
warning = overflowing;
|
||||
|
||||
int level = overflowing
|
||||
? (STORAGE_WARN_LED_ACTIVE_HIGH ? HIGH : LOW)
|
||||
: (STORAGE_WARN_LED_ACTIVE_HIGH ? LOW : HIGH);
|
||||
|
||||
digitalWrite(STORAGE_WARN_LED_GPIO, level);
|
||||
|
||||
if (overflowing)
|
||||
{
|
||||
Serial.println("[Logger] Overflow: SD cannot keep up");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DataLogger::updateBpsWindow(uint32_t bytes, uint32_t nowMs)
|
||||
{
|
||||
windowBytes += bytes;
|
||||
|
||||
uint32_t elapsed = nowMs - windowStartMs;
|
||||
|
||||
if (elapsed >= 1000)
|
||||
{
|
||||
bps = (uint32_t)(((uint64_t)windowBytes * 1000ULL) / elapsed);
|
||||
windowBytes = 0;
|
||||
windowStartMs = nowMs;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+24
-24
@@ -1,13 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// Audio logging interface (see docs/audio_logging.md for the full design).
|
||||
// Audio logging (see docs/audio_logging.md for the full design).
|
||||
//
|
||||
// THIS FILE IS A SKELETON. The chunk-pool / queue / file logic in
|
||||
// data_logger.cpp is stubbed with TODOs; only the WAV metadata helpers are
|
||||
// implemented. The interface below is the contract the future Node
|
||||
// collection task (producer) and the StorageTask (consumer) are written
|
||||
// against.
|
||||
// Producer/consumer bridge between the Node collection task (core 1) and the
|
||||
// StorageTask (core 0). Owns the chunk pool, the two FreeRTOS queues, and the
|
||||
// open WAV file. Only the consumer touches the card.
|
||||
// ============================================================================
|
||||
|
||||
#include <Arduino.h>
|
||||
@@ -20,7 +18,7 @@
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// WAV (RIFF) metadata helpers - fully implemented in data_logger.cpp.
|
||||
// WAV (RIFF) metadata helpers.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// 44-byte PCM WAVE header. Layout and field meaning are documented in
|
||||
@@ -79,9 +77,6 @@ struct AudioChunk
|
||||
// DataLogger
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Producer/consumer bridge between the Node collection task (core 1) and the
|
||||
// StorageTask (core 0). Owns the chunk pool, the two FreeRTOS queues, and the
|
||||
// open WAV file. Only the consumer touches the card.
|
||||
class DataLogger
|
||||
{
|
||||
public:
|
||||
@@ -98,22 +93,26 @@ public:
|
||||
|
||||
// Session control --------------------------------------------------------
|
||||
|
||||
// Creates the next rec_<uptimeSeconds>_<n>.wav under STORAGE_LOG_DIR,
|
||||
// writes the 44-byte WAV header. false if the card is full or unwritable.
|
||||
// Creates the next rec_<uptimeSeconds>_<n>.wav under STORAGE_LOG_DIR
|
||||
// (skipping forward if the name exists), writes the 44-byte WAV header.
|
||||
// false if the card is full or unwritable.
|
||||
bool openSession();
|
||||
|
||||
// Finalizes (patches RIFF/data sizes) and closes the current file.
|
||||
bool closeSession();
|
||||
|
||||
// Closes/opens when bytesThisFile >= STORAGE_LOG_ROTATE_BYTES. Called by
|
||||
// the consumer on every loop; must be a no-op when idle.
|
||||
// the consumer on every loop; a no-op when idle or below the threshold.
|
||||
bool rotateIfNeeded();
|
||||
|
||||
bool hasSession() const;
|
||||
bool isEmpty() const; // no chunks waiting to be written
|
||||
|
||||
// Producer API (Node collection task, core 1) ----------------------------
|
||||
|
||||
// Pops a free chunk, or on timeout drops the oldest queued chunk (Section
|
||||
// 8) and returns it so the producer can keep streaming. nullptr only if
|
||||
// there is nothing to drop (empty pool + empty queue).
|
||||
// Pops a free chunk, or on timeout drops the oldest queued chunk and
|
||||
// returns it so the producer can keep streaming. nullptr only if there is
|
||||
// nothing to drop (empty pool + empty queue).
|
||||
AudioChunk* acquireChunk(TickType_t timeout);
|
||||
|
||||
// Returns a filled chunk to the write queue. Must be called in order.
|
||||
@@ -121,13 +120,12 @@ public:
|
||||
|
||||
// Consumer API (StorageTask, core 0) --------------------------------------
|
||||
|
||||
// Blocks up to `timeout` for the next filled chunk. nullptr on timeout
|
||||
// (lets the consumer pulse for stats even when idle).
|
||||
// Blocks up to `timeout` for the next filled chunk. nullptr on timeout.
|
||||
AudioChunk* nextChunk(TickType_t timeout);
|
||||
|
||||
// Appends chunk->data[0..length) to the WAV data section with one
|
||||
// file.write(), updates the byte/chunk counters and the bps window.
|
||||
// false on card failure (fatal per design.md).
|
||||
// file.write(), updating counters and the bps window. Opens a session
|
||||
// lazily if none is open. false on card failure (fatal per design.md).
|
||||
bool writeChunk(AudioChunk* chunk);
|
||||
|
||||
// Returns a chunk to the free pool after it has been written or dropped.
|
||||
@@ -142,16 +140,18 @@ public:
|
||||
bool overflowing() const;
|
||||
|
||||
private:
|
||||
// Pops the oldest unwritten chunk off filledQ without writing it,
|
||||
// increments droppedChunks, raises the warning. Returns it to the caller
|
||||
// (used by acquireChunk on timeout).
|
||||
// Pops the oldest unwritten chunk off filledQ without writing it (FIFO
|
||||
// front == oldest), increments droppedChunks, raises the warning.
|
||||
AudioChunk* dropOldestChunk();
|
||||
|
||||
// Serial (rate-limited) + LED (STORAGE_WARN_LED_GPIO) overflow indication.
|
||||
void setOverflowWarning(bool overflowing);
|
||||
|
||||
void updateBpsWindow(uint32_t bytes, uint32_t nowMs);
|
||||
|
||||
fs::FS* files; // SD backend, injected by begin()
|
||||
AudioChunk* pool; // STORAGE_LOG_POOL_SIZE chunks
|
||||
AudioChunk chunks[STORAGE_LOG_POOL_SIZE];
|
||||
uint8_t* poolData; // one DMA-capable block, sliced into chunks
|
||||
QueueHandle_t freeQ; // empty chunks
|
||||
QueueHandle_t filledQ; // chunks waiting to be written
|
||||
File file; // open session file
|
||||
|
||||
@@ -285,7 +285,10 @@ uint32_t SDManager::measureWriteSpeed()
|
||||
const size_t bufferSize = 16 * 1024;
|
||||
|
||||
// Remove any leftover from a previous crashed run.
|
||||
files.remove(scratchPath);
|
||||
if (files.exists(scratchPath))
|
||||
{
|
||||
files.remove(scratchPath);
|
||||
}
|
||||
|
||||
uint8_t* buffer = (uint8_t*)malloc(bufferSize);
|
||||
if (buffer == nullptr)
|
||||
@@ -324,7 +327,10 @@ uint32_t SDManager::measureWriteSpeed()
|
||||
Serial.print("Elapsed microseconds: ");
|
||||
Serial.println(elapsedUs);
|
||||
|
||||
files.remove(scratchPath);
|
||||
if (files.exists(scratchPath))
|
||||
{
|
||||
files.remove(scratchPath);
|
||||
}
|
||||
free(buffer);
|
||||
|
||||
if (remaining != 0 || elapsedUs == 0)
|
||||
@@ -351,6 +357,47 @@ const char* SDManager::cardTypeName(StorageCardType type)
|
||||
}
|
||||
|
||||
|
||||
bool SDManager::writeTestFile()
|
||||
{
|
||||
if (!mounted || backend == nullptr)
|
||||
{
|
||||
Serial.println("[Storage] Test file: not mounted, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* path = STORAGE_TEST_FILE_PATH;
|
||||
const char* text = "the dog ate the moon";
|
||||
|
||||
fs::FS& files = backend->fs();
|
||||
|
||||
if (files.exists(path))
|
||||
{
|
||||
files.remove(path);
|
||||
}
|
||||
|
||||
File file = files.open(path, FILE_WRITE);
|
||||
if (!file)
|
||||
{
|
||||
Serial.println("[Storage] Test file: could not open for write");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t written = file.print(text);
|
||||
file.close();
|
||||
|
||||
if (written != strlen(text))
|
||||
{
|
||||
Serial.printf("[Storage] Test file: wrote %u of %u bytes\n",
|
||||
(unsigned)written, (unsigned)strlen(text));
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[Storage] Test file: wrote '%s' (%u bytes)\n",
|
||||
text, (unsigned)written);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void SDManager::printCardInfo()
|
||||
{
|
||||
if (!mounted)
|
||||
@@ -434,6 +481,7 @@ void SDManager::listFilesRecursive(fs::FS& files, char* path, size_t pathSize, u
|
||||
{
|
||||
snprintf(path + base, pathSize - base, "/%s", entry.name());
|
||||
Serial.printf(" %s (%llu bytes)\n", path, (unsigned long long)entry.size());
|
||||
path[base] = '\0';
|
||||
}
|
||||
|
||||
entry.close();
|
||||
|
||||
@@ -63,6 +63,10 @@ public:
|
||||
void printCardInfo();
|
||||
void listFiles();
|
||||
|
||||
// Small sanity write used during bring-up: creates happy_file.txt with
|
||||
// a fixed message and returns true when the file lands on the card.
|
||||
bool writeTestFile();
|
||||
|
||||
private:
|
||||
StorageBackend* backend;
|
||||
bool mounted;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
// SPI until the SDIO-capable board arrives.
|
||||
#ifndef STORAGE_IFACE
|
||||
#define STORAGE_IFACE STORAGE_IFACE_SPI
|
||||
#define STORAGE_IFACE STORAGE_IFACE_SDMMC
|
||||
#endif
|
||||
|
||||
// --- Common -----------------------------------------------------------------
|
||||
@@ -75,12 +75,19 @@
|
||||
// --- 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
|
||||
// Interface 1: #define STORAGE_SDMMC_CLK 6
|
||||
// Interface 1: #define STORAGE_SDMMC_CMD 11
|
||||
// Interface 1: #define STORAGE_SDMMC_D0 7
|
||||
// Interface 1: #define STORAGE_SDMMC_D1 8
|
||||
// Interface 1: #define STORAGE_SDMMC_D2 9
|
||||
// Interface 1: #define STORAGE_SDMMC_D3 10
|
||||
|
||||
#define STORAGE_SDMMC_CLK 14
|
||||
#define STORAGE_SDMMC_CMD 15
|
||||
#define STORAGE_SDMMC_D0 2
|
||||
#define STORAGE_SDMMC_D1 4
|
||||
#define STORAGE_SDMMC_D2 12
|
||||
#define STORAGE_SDMMC_D3 13
|
||||
|
||||
// false = 4-bit wide bus (required for >8 MB/s). Do NOT enable 1-bit mode.
|
||||
#define STORAGE_SDMMC_MODE_1BIT false
|
||||
@@ -113,14 +120,20 @@
|
||||
// this benchmark disabled by setting the interval to 0 (measure once at boot).
|
||||
#define STORAGE_SPEED_MEASURE_INTERVAL_MS 0
|
||||
|
||||
// --- Bring-up sanity test ----------------------------------------------------
|
||||
|
||||
// Small fixed-content file written at boot to prove the card's write path
|
||||
// works end to end (create/open/write/close on the live mount).
|
||||
#define STORAGE_TEST_FILE_PATH STORAGE_MOUNT_POINT "/happy_file.txt"
|
||||
|
||||
// --- Audio logging ----------------------------------------------------------
|
||||
//
|
||||
// The DataLogger (docs/audio_logging.md) writes one multichannel WAV file
|
||||
// per rotation. The Node collection task feeds it interleaved 40-channel PCM
|
||||
// as chunks; the logger appends them and only the consumer touches the card.
|
||||
//
|
||||
// Stream rate: 40 ch x 48 kHz x 2 B = 3,840,000 B/s. The 128 KB pool holds
|
||||
// ~34 ms of audio, so collection rounds must stay <= ~25 ms (<= 96 KB) or
|
||||
// Stream rate: 40 ch x 48 kHz x 2 B = 3,840,000 B/s. The 64 KB pool holds
|
||||
// ~17 ms of audio, so collection rounds must stay <= ~12 ms (<= 48 KB) or
|
||||
// the pool must grow (see docs/audio_logging.md section 7).
|
||||
|
||||
// WAV parameters (must match what the Nodes produce).
|
||||
@@ -129,8 +142,9 @@
|
||||
#define STORAGE_AUDIO_BITS 16
|
||||
|
||||
// Chunk pool: STORAGE_LOG_POOL_SIZE chunks of STORAGE_LOG_CHUNK_SIZE bytes,
|
||||
// allocated with MALLOC_CAP_DMA. 128 KB total.
|
||||
#define STORAGE_LOG_CHUNK_SIZE (16 * 1024)
|
||||
// allocated with MALLOC_CAP_DMA. 64 KB total. Each interleave round (see
|
||||
// NET_ROUND_FRAMES) must fit inside one chunk.
|
||||
#define STORAGE_LOG_CHUNK_SIZE (8 * 1024)
|
||||
#define STORAGE_LOG_POOL_SIZE 8
|
||||
|
||||
// Recording directory and rotation policy.
|
||||
|
||||
Reference in New Issue
Block a user