#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 #include #include #include #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__.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; };