in sev state

This commit is contained in:
2026-08-14 08:24:33 -06:00
parent e06b429db9
commit 3d26b66490
27 changed files with 2578 additions and 108 deletions
+286 -43
View File
@@ -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
View File
@@ -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
+50 -2
View 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();
+4
View File
@@ -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;
+25 -11
View File
@@ -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.