Working on SD and netowrking
This commit is contained in:
@@ -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; }
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user