Working on SD and netowrking

This commit is contained in:
2026-08-09 20:34:44 -06:00
parent 0014e8697f
commit e06b429db9
4 changed files with 730 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
#include "data_logger.h"
#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.
// ============================================================================
// --- WAV metadata helpers (complete) -----------------------------------------
void buildWavHeader(WavHeader& header,
uint16_t numChannels,
uint32_t sampleRate,
uint16_t bitsPerSample,
uint32_t dataSize)
{
uint16_t blockAlign = numChannels * (bitsPerSample / 8);
memset(&header, 0, sizeof(header));
memcpy(header.riff, "RIFF", 4);
header.riffSize = sizeof(header) + dataSize - 8;
memcpy(header.wave, "WAVE", 4);
memcpy(header.fmt, "fmt ", 4);
header.fmtChunkSize = 16;
header.audioFormat = 1; // PCM
header.numChannels = numChannels;
header.sampleRate = sampleRate;
header.byteRate = sampleRate * blockAlign;
header.blockAlign = blockAlign;
header.bitsPerSample = bitsPerSample;
memcpy(header.data, "data", 4);
header.dataSize = dataSize;
}
void finalizeWavHeader(uint32_t fileSize,
uint32_t& riffSizeOut,
uint32_t& dataSizeOut)
{
riffSizeOut = fileSize - 8;
dataSizeOut = fileSize - sizeof(WavHeader);
}
// --- DataLogger ---------------------------------------------------------------
DataLogger::DataLogger()
:
files(nullptr),
pool(nullptr),
freeQ(nullptr),
filledQ(nullptr),
sessionSeq(0),
bytesThisFile(0),
totalBytes(0),
totalChunks(0),
totalDropped(0),
lastDropPrintMs(0),
windowBytes(0),
windowStartMs(0),
bps(0),
active(false),
warning(false)
{
}
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;
}
void DataLogger::end()
{
// TODO: closeSession(), free the pool (heap_caps_free), delete the queues.
}
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;
}
bool DataLogger::closeSession()
{
// TODO: finalizeWavHeader(file.size(), ...) -> patch offsets 4 and 40,
// file.flush(), file.close().
return false;
}
bool DataLogger::rotateIfNeeded()
{
// TODO: if active && bytesThisFile >= STORAGE_LOG_ROTATE_BYTES:
// closeSession(); sessionSeq++; openSession().
return false;
}
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;
}
void DataLogger::submitChunk(AudioChunk* chunk)
{
// TODO: xQueueSend(filledQ, &chunk, ...). Must preserve stream order.
}
AudioChunk* DataLogger::nextChunk(TickType_t timeout)
{
// TODO: xQueueReceive(filledQ, &chunk, timeout).
return nullptr;
}
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;
}
void DataLogger::releaseChunk(AudioChunk* chunk)
{
// TODO: chunk->length = 0; xQueueSend(freeQ, &chunk, ...).
}
AudioChunk* DataLogger::dropOldestChunk()
{
// TODO: xQueueReceive from the BACK of filledQ without writing, count it,
// setOverflowWarning(true). Called by acquireChunk on timeout.
return nullptr;
}
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;
}
uint32_t DataLogger::bytesWritten() const { return (uint32_t)totalBytes; }
uint32_t DataLogger::chunksWritten() const { return totalChunks; }
uint32_t DataLogger::droppedChunks() const { return totalDropped; }
uint32_t DataLogger::writeSpeedBps() const { return bps; }
bool DataLogger::overflowing() const { return warning; }
+174
View File
@@ -0,0 +1,174 @@
#pragma once
// ============================================================================
// Audio logging interface (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.
// ============================================================================
#include <Arduino.h>
#include <FS.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "storage_config.h"
// ----------------------------------------------------------------------------
// WAV (RIFF) metadata helpers - fully implemented in data_logger.cpp.
// ----------------------------------------------------------------------------
// 44-byte PCM WAVE header. Layout and field meaning are documented in
// docs/audio_logging.md section 3.
struct WavHeader
{
uint8_t riff[4]; // "RIFF"
uint32_t riffSize; // file size - 8
uint8_t wave[4]; // "WAVE"
uint8_t fmt[4]; // "fmt "
uint32_t fmtChunkSize; // 16 (PCM)
uint16_t audioFormat; // 1 (PCM)
uint16_t numChannels; // STORAGE_AUDIO_CHANNELS
uint32_t sampleRate; // STORAGE_AUDIO_SAMPLE_RATE_HZ
uint32_t byteRate; // sampleRate * blockAlign
uint16_t blockAlign; // numChannels * (bitsPerSample / 8)
uint16_t bitsPerSample; // STORAGE_AUDIO_BITS
uint8_t data[4]; // "data"
uint32_t dataSize; // file size - 44
} __attribute__((packed));
static_assert(sizeof(WavHeader) == 44, "WAV header must be exactly 44 bytes");
// Fills the header for a PCM stream of the given channels/rate/depth.
// dataSize is typically 0 at file open and corrected on finalize.
void buildWavHeader(WavHeader& header,
uint16_t numChannels,
uint32_t sampleRate,
uint16_t bitsPerSample,
uint32_t dataSize);
// Returns the two little-endian values to patch at offsets 4 and 40 when a
// file is closed/rotated: riffSize = fileSize - 8, dataSize = fileSize - 44.
void finalizeWavHeader(uint32_t fileSize,
uint32_t& riffSizeOut,
uint32_t& dataSizeOut);
// ----------------------------------------------------------------------------
// Chunk pool
// ----------------------------------------------------------------------------
// One slot of the write pool. Producers borrow a chunk, fill `data[0..length)`
// with interleaved PCM (40 channels, frame order, 80 bytes/frame), and submit
// it. Chunks must be submitted in strict stream order.
struct AudioChunk
{
uint8_t* data; // DMA-capable buffer owned by the pool
uint32_t capacity; // STORAGE_LOG_CHUNK_SIZE
uint32_t length; // valid bytes, multiple of the WAV frame size
uint32_t sequence; // monotonic order (diagnostics only)
};
// ----------------------------------------------------------------------------
// 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:
DataLogger();
// Lifecycle --------------------------------------------------------------
// Allocates the pool (MALLOC_CAP_DMA), creates the queues, creates
// STORAGE_LOG_DIR. Does not open a session file.
bool begin(fs::FS& files);
// Finalizes and closes the current session file, frees the pool/queues.
void end();
// 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.
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.
bool rotateIfNeeded();
// 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).
AudioChunk* acquireChunk(TickType_t timeout);
// Returns a filled chunk to the write queue. Must be called in order.
void submitChunk(AudioChunk* chunk);
// 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).
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).
bool writeChunk(AudioChunk* chunk);
// Returns a chunk to the free pool after it has been written or dropped.
void releaseChunk(AudioChunk* chunk);
// Stats -------------------------------------------------------------------
uint32_t bytesWritten() const;
uint32_t chunksWritten() const;
uint32_t droppedChunks() const;
uint32_t writeSpeedBps() const;
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).
AudioChunk* dropOldestChunk();
// Serial (rate-limited) + LED (STORAGE_WARN_LED_GPIO) overflow indication.
void setOverflowWarning(bool overflowing);
fs::FS* files; // SD backend, injected by begin()
AudioChunk* pool; // STORAGE_LOG_POOL_SIZE chunks
QueueHandle_t freeQ; // empty chunks
QueueHandle_t filledQ; // chunks waiting to be written
File file; // open session file
char filePath[96];
uint32_t sessionSeq; // rotation counter within this boot
uint64_t bytesThisFile;
uint64_t totalBytes;
uint32_t totalChunks;
uint32_t totalDropped;
uint32_t lastDropPrintMs;
// bps metering window (writeChunk is the only writer)
uint32_t windowBytes;
uint32_t windowStartMs;
uint32_t bps;
bool active;
bool warning;
};
+31
View File
@@ -112,3 +112,34 @@
// 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
// --- 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
// the pool must grow (see docs/audio_logging.md section 7).
// WAV parameters (must match what the Nodes produce).
#define STORAGE_AUDIO_SAMPLE_RATE_HZ 48000
#define STORAGE_AUDIO_CHANNELS 40 // 10 nodes x 4 mics
#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)
#define STORAGE_LOG_POOL_SIZE 8
// Recording directory and rotation policy.
#define STORAGE_LOG_DIR STORAGE_MOUNT_POINT "/audio"
#define STORAGE_LOG_ROTATE_BYTES (1024LL * 1024 * 1024) // 1 GiB / file
#define STORAGE_LOG_FLUSH_BYTES (16LL * 1024 * 1024) // f_sync cadence
// Overflow warning LED: lights while chunks are being dropped because the SD
// card cannot keep up. GPIO 4 is unused by the SD card lines (SPI 5/18/19/23,
// SDMMC 6-11). Change both values if a different LED is wired.
#define STORAGE_WARN_LED_GPIO 4
#define STORAGE_WARN_LED_ACTIVE_HIGH true