diff --git a/firmware/include/audio_config.h b/firmware/include/audio_config.h deleted file mode 100644 index c6c7853..0000000 --- a/firmware/include/audio_config.h +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -// Recording parameters. Defaults: 16 kHz mono 16-bit PCM (see state/DECISIONS.md). -#pragma once - -#define OSC_SAMPLE_RATE 16000 -#define OSC_BITS_PER_SAMPLE 16 -#define OSC_CHANNELS 1 - -// Directory on the SD card for recordings + sidecar metadata. -#define OSC_REC_DIR "/recordings" - -// Capture chunk size (bytes) pumped from the mic to the WAV file each update(). -// 4096 bytes = 2048 samples = 128 ms at 16 kHz mono 16-bit. Sits in a stack buffer. -#define OSC_CHUNK_BYTES 4096 \ No newline at end of file diff --git a/firmware/include/pins.h b/firmware/include/pins.h index 24277a3..3e882f7 100644 --- a/firmware/include/pins.h +++ b/firmware/include/pins.h @@ -5,17 +5,10 @@ #if defined(OPENSCRIBE_BOARD_XIAO) // Seeed XIAO ESP32-S3 Sense: onboard PDM mic + microSD use the Sense board's fixed pins. -// The audio driver selects the PDM path for this board. -#define PIN_PDM_CLK 42 // XIAO ESP32-S3 Sense onboard PDM mic clock -#define PIN_PDM_DIN 41 // XIAO ESP32-S3 Sense onboard PDM mic data -// XIAO Sense onboard microSD (SDMMC-on-SPI fixed pins) -#define PIN_SD_CS 21 -#define PIN_SD_SCK 7 -#define PIN_SD_MOSI 9 -#define PIN_SD_MISO 8 +// The audio driver selects the PDM path for this board; these are placeholders for the +// user-facing controls only. #define PIN_BUTTON 2 // external tactile button to GND -#define PIN_LED LED_BUILTIN // XIAO onboard user LED (active-low) -#define PIN_LED_ACTIVE_LOW 1 +#define PIN_LED 21 // XIAO onboard user LED #define PIN_VBUS_SENSE -1 // use USB-serial/VBUS API on XIAO #define PIN_CHARGE_STAT -1 @@ -33,8 +26,7 @@ // Controls / status #define PIN_BUTTON 7 // active-low, internal pull-up -#define PIN_LED 48 // plain LED via 330R (WS2812 support added later) -#define PIN_LED_ACTIVE_LOW 0 +#define PIN_LED 48 // WS2812 data (or plain LED via 330R) #define PIN_CHARGE_STAT 14 // TP4056 STAT (open-drain) #define PIN_VBUS_SENSE 15 // USB 5V present -> on-charge / powered mode #define PIN_MOTOR 16 // optional haptic, via transistor diff --git a/firmware/src/audio.cpp b/firmware/src/audio.cpp deleted file mode 100644 index 4187348..0000000 --- a/firmware/src/audio.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -#include "audio.h" - -#include // bundled with Arduino-ESP32 3.x - -#include "audio_config.h" -#include "pins.h" - -namespace { -I2SClass i2s; -} - -namespace audio { - -bool begin() { -#if defined(OPENSCRIBE_BOARD_XIAO) - // XIAO ESP32-S3 Sense onboard PDM microphone. - i2s.setPinsPdmRx(PIN_PDM_CLK, PIN_PDM_DIN); - return i2s.begin(I2S_MODE_PDM_RX, OSC_SAMPLE_RATE, - I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO); -#else - // I2S standard mode from an external MEMS mic (INMP441 / ICS-43434). - // Args: bclk, ws, dout(unused for RX = -1), din, mclk(unused = -1). - i2s.setPins(PIN_I2S_BCLK, PIN_I2S_WS, -1, PIN_I2S_DIN, -1); - return i2s.begin(I2S_MODE_STD, OSC_SAMPLE_RATE, - I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO); -#endif -} - -size_t audio::read(uint8_t *dst, size_t maxBytes) { - // I2SClass::readBytes blocks until data is available or the DMA times out. - int n = i2s.readBytes(reinterpret_cast(dst), maxBytes); - return n < 0 ? 0 : static_cast(n); -} - -void end() { i2s.end(); } - -} // namespace audio - -// NOTE (calibration, revisit with hardware): INMP441 emits 24-bit samples in a 32-bit -// slot. Reading in 16-bit mode captures the top bits and is usable for speech but may be -// quiet. If levels are low on the dev build, switch to 32-bit capture and right-shift into -// int16 here. Tracked in state/TODO.md. \ No newline at end of file diff --git a/firmware/src/audio.h b/firmware/src/audio.h deleted file mode 100644 index 4c03352..0000000 --- a/firmware/src/audio.h +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -// Microphone capture. Presents 16-bit PCM mono regardless of board: -// - Build B (dev): I2S standard mode from an INMP441 / ICS-43434. -// - Build A (XIAO): PDM mode from the onboard mic. -#pragma once - -#include -#include - -namespace audio { - -// Bring the mic up at OSC_SAMPLE_RATE. Returns false if the I2S peripheral fails to init. -bool begin(); - -// Read up to maxBytes of 16-bit little-endian PCM mono into dst. -// Returns the number of bytes actually read (may be less than requested). -size_t read(uint8_t *dst, size_t maxBytes); - -// Release the I2S peripheral. -void end(); - -} // namespace audio \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 7b91684..e3fa988 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -2,73 +2,50 @@ // // OpenScribe firmware entry point. // -// M1 (recording core): boots audio + storage, then records to a WAV file on the microSD -// card, toggled by the button, with the status LED showing idle/recording/error. Each -// recording gets a sidecar JSON metadata file matching the Recording schema in -// api/openapi.yaml. +// This is the M0 scaffold: it boots, reports the board profile and PSRAM, and defines the +// module seams the later milestones fill in (see state/TODO.md and state/ARCHITECTURE.md). +// No audio/WiFi/BLE yet - those land in M1-M4 so each is a reviewable feature branch. // -// Later milestones add the remaining module seams: -// power - battery ADC, charge/VBUS detect -> mode switch (M3) -// config - NVS settings (WiFi, upload target, codec, token) (M2/M3) -// net_wifi - WiFi join/reconnect + mDNS (M2) -// api_http - on-device REST server (api/openapi.yaml) (M2) -// uploader - S3/WebDAV upload when powered (M3) -// ble - GATT control + WiFi provisioning (M4) -// ota - firmware update over HTTP (M9) +// Module seams (added incrementally): +// audio - I2S/PDM mic capture -> PSRAM ring buffer -> WAV encoder (M1) +// storage - microSD FAT files + sidecar JSON metadata (M1) +// recorder - record session state machine, file naming (M1) +// ux - button + LED/haptic (M1) +// power - battery ADC, charge/VBUS detect -> mode switch (M3) +// config - NVS settings (WiFi, upload target, codec, token) (M2/M3) +// net_wifi - WiFi join/reconnect + mDNS (M2) +// api_http - on-device REST server (api/openapi.yaml device paths) (M2) +// uploader - S3/WebDAV upload when powered (M3) +// ble - GATT control + WiFi provisioning (M4) +// ota - firmware update over HTTP (M9) #include -#include "recorder.h" -#include "ux.h" - -static const char *FIRMWARE_VERSION = "0.1.0"; +static const char *FIRMWARE_VERSION = "0.1.0-scaffold"; void setup() { Serial.begin(115200); delay(300); - Serial.printf("\nOpenScribe firmware %s booting\n", FIRMWARE_VERSION); + Serial.println(); + Serial.printf("OpenScribe firmware %s booting\n", FIRMWARE_VERSION); #if defined(OPENSCRIBE_BOARD_XIAO) - Serial.println("Board: XIAO ESP32-S3 (PDM mic + onboard SD)"); + Serial.println("Board profile: XIAO ESP32-S3 (Build A, PDM mic + onboard SD)"); #else - Serial.println("Board: ESP32-S3 dev (I2S mic + SD module)"); + Serial.println("Board profile: ESP32-S3 dev (Build B, I2S mic + SD module)"); #endif - if (!psramFound()) { + if (psramFound()) { + Serial.printf("PSRAM: %u bytes free\n", (unsigned)ESP.getFreePsram()); + } else { Serial.println("WARNING: no PSRAM detected - audio buffering needs a PSRAM board"); } - ux::begin(); - - if (!recorder::begin()) { - Serial.println("ERROR: audio or SD init failed - check the mic wiring and SD card"); - ux::setLed(ux::Led::Error); - return; // stay in error state; loop() still services the LED - } - - Serial.println("Ready. Short-press the button to start/stop recording."); - ux::setLed(ux::Led::Idle); + // M1 onwards wires the modules here: + // config::begin(); storage::begin(); audio::begin(); ux::begin(); ... } void loop() { - // Button: a short press toggles recording. - if (ux::poll()) { - if (recorder::toggle()) { - if (recorder::state() == recorder::State::Recording) { - Serial.printf("Recording started: %s\n", recorder::currentId().c_str()); - ux::setLed(ux::Led::Recording); - } else { - Serial.printf("Recording stopped: %s (%lu PCM bytes)\n", - recorder::currentId().c_str(), - static_cast(recorder::recordedBytes())); - ux::setLed(ux::Led::Idle); - } - } else { - Serial.println("ERROR: could not open recording file on SD"); - ux::setLed(ux::Led::Error); - } - } - - // While recording, drain the mic into the file. - recorder::update(); -} + // Placeholder heartbeat until the recorder state machine lands (M1). + delay(1000); +} \ No newline at end of file diff --git a/firmware/src/recorder.cpp b/firmware/src/recorder.cpp deleted file mode 100644 index 89f8401..0000000 --- a/firmware/src/recorder.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -#include "recorder.h" - -#include "audio.h" -#include "audio_config.h" -#include "storage.h" - -namespace { - -recorder::State state_ = recorder::State::Idle; -storage::WavWriter wav; -String currentId_; -uint32_t startMillis_ = 0; - -// Generate a recording id. Without an RTC/NTP the device has no wall-clock time yet, so -// M1 uses an uptime-milliseconds stamp; M2 (WiFi/NTP) upgrades this to a real UTC time in -// the id and the started_at field. Kept sortable and filename-safe either way. -String makeId() { - char buf[40]; - snprintf(buf, sizeof(buf), "rec_up%010lu", static_cast(millis())); - return String(buf); -} - -String wavPath(const String &id) { return String(OSC_REC_DIR) + "/" + id + ".wav"; } -String metaPath(const String &id) { return String(OSC_REC_DIR) + "/" + id + ".json"; } - -// Minimal metadata JSON matching the Recording schema in api/openapi.yaml. Hand-written to -// avoid pulling in a JSON library on the device for a handful of fields. -String metadataJson(const String &id, uint32_t dataBytes, float durationS) { - String s = "{"; - s += "\"id\":\"" + id + "\","; - s += "\"started_at\":null,"; // set once the clock is known (M2/NTP) - s += "\"duration_s\":" + String(durationS, 2) + ","; - s += "\"sample_rate\":" + String(OSC_SAMPLE_RATE) + ","; - s += "\"channels\":" + String(OSC_CHANNELS) + ","; - s += "\"codec\":\"wav_pcm_s16le\","; - s += "\"size_bytes\":" + String(dataBytes + 44) + ","; - s += "\"source\":\"device\","; - s += "\"sync_state\":\"local\""; - s += "}"; - return s; -} - -} // namespace - -namespace recorder { - -bool begin() { - bool ok = storage::begin(); - ok = audio::begin() && ok; - return ok; -} - -bool startRecording() { - if (state_ == State::Recording) return false; - currentId_ = makeId(); - if (!wav.open(wavPath(currentId_), OSC_SAMPLE_RATE, OSC_BITS_PER_SAMPLE, OSC_CHANNELS)) { - return false; - } - startMillis_ = millis(); - state_ = State::Recording; - return true; -} - -bool stopRecording() { - if (state_ != State::Recording) return false; - state_ = State::Idle; - wav.close(); - const float durationS = (millis() - startMillis_) / 1000.0f; - storage::writeMetadata(metaPath(currentId_), - metadataJson(currentId_, wav.dataBytes(), durationS)); - return true; -} - -bool toggle() { return state_ == State::Recording ? stopRecording() : startRecording(); } - -void update() { - if (state_ != State::Recording) return; - static uint8_t buf[OSC_CHUNK_BYTES]; - size_t n = audio::read(buf, sizeof(buf)); - if (n > 0) { - wav.write(buf, n); - } -} - -State state() { return state_; } -const String ¤tId() { return currentId_; } -uint32_t recordedBytes() { return wav.dataBytes(); } - -} // namespace recorder diff --git a/firmware/src/recorder.h b/firmware/src/recorder.h deleted file mode 100644 index 5f1fdc9..0000000 --- a/firmware/src/recorder.h +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -// Recording session state machine: pumps mic audio into a WAV file on the SD card and -// writes sidecar JSON metadata when a session ends. -#pragma once - -#include - -namespace recorder { - -enum class State { Idle, Recording }; - -// Bring up audio + storage. Returns false if either fails (caller shows an error state). -bool begin(); - -// Call frequently from loop(): when recording, drains the mic into the file. -void update(); - -bool startRecording(); -bool stopRecording(); -bool toggle(); // start if idle, stop if recording - -State state(); -const String ¤tId(); // id of the in-progress/last recording -uint32_t recordedBytes(); // PCM bytes written in the current session - -} // namespace recorder diff --git a/firmware/src/storage.cpp b/firmware/src/storage.cpp deleted file mode 100644 index 0f0cbd4..0000000 --- a/firmware/src/storage.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -#include "storage.h" - -#include - -#include "audio_config.h" -#include "pins.h" - -namespace { - -// A dedicated SPI bus for the SD card so it does not clash with other peripherals. -SPIClass sdSpi(HSPI); - -// Write a 44-byte canonical PCM WAV header. dataBytes may be 0 at open (patched on close). -void writeWavHeader(File &f, uint32_t sampleRate, uint16_t bits, uint16_t channels, - uint32_t dataBytes) { - const uint32_t byteRate = sampleRate * channels * (bits / 8); - const uint16_t blockAlign = channels * (bits / 8); - const uint32_t riffSize = 36 + dataBytes; - - auto u32 = [&](uint32_t v) { f.write(reinterpret_cast(&v), 4); }; - auto u16 = [&](uint16_t v) { f.write(reinterpret_cast(&v), 2); }; - - f.write(reinterpret_cast("RIFF"), 4); - u32(riffSize); - f.write(reinterpret_cast("WAVE"), 4); - f.write(reinterpret_cast("fmt "), 4); - u32(16); // PCM fmt chunk size - u16(1); // audio format = PCM - u16(channels); - u32(sampleRate); - u32(byteRate); - u16(blockAlign); - u16(bits); - f.write(reinterpret_cast("data"), 4); - u32(dataBytes); -} - -} // namespace - -namespace storage { - -bool begin() { - sdSpi.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS); - if (!SD.begin(PIN_SD_CS, sdSpi)) { - return false; - } - if (!SD.exists(OSC_REC_DIR)) { - SD.mkdir(OSC_REC_DIR); - } - return true; -} - -uint64_t totalBytes() { return SD.totalBytes(); } -uint64_t freeBytes() { return SD.totalBytes() - SD.usedBytes(); } - -bool WavWriter::open(const String &path, uint32_t sampleRate, uint16_t bitsPerSample, - uint16_t channels) { - file_ = SD.open(path, FILE_WRITE); - if (!file_) return false; - sampleRate_ = sampleRate; - bits_ = bitsPerSample; - channels_ = channels; - dataBytes_ = 0; - writeWavHeader(file_, sampleRate_, bits_, channels_, 0); // placeholder sizes - return true; -} - -size_t WavWriter::write(const uint8_t *data, size_t len) { - if (!file_) return 0; - size_t n = file_.write(data, len); - dataBytes_ += n; - return n; -} - -bool WavWriter::close() { - if (!file_) return false; - // Patch RIFF size (offset 4) and data size (offset 40) now that we know the length. - file_.flush(); - file_.seek(4); - uint32_t riffSize = 36 + dataBytes_; - file_.write(reinterpret_cast(&riffSize), 4); - file_.seek(40); - file_.write(reinterpret_cast(&dataBytes_), 4); - file_.close(); - return true; -} - -bool writeMetadata(const String &path, const String &json) { - File f = SD.open(path, FILE_WRITE); - if (!f) return false; - f.print(json); - f.close(); - return true; -} - -} // namespace storage diff --git a/firmware/src/storage.h b/firmware/src/storage.h deleted file mode 100644 index af65659..0000000 --- a/firmware/src/storage.h +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -// microSD storage: a streaming WAV writer plus sidecar JSON metadata. -#pragma once - -#include -#include -#include - -namespace storage { - -// Mount the microSD card over SPI using the pins in pins.h. Creates OSC_REC_DIR. -bool begin(); - -uint64_t totalBytes(); -uint64_t freeBytes(); - -// Streaming writer for a canonical 16-bit PCM WAV file. Writes a placeholder header on -// open(), appends PCM with write(), and patches the RIFF/data sizes on close(). -class WavWriter { - public: - bool open(const String &path, uint32_t sampleRate, uint16_t bitsPerSample, - uint16_t channels); - size_t write(const uint8_t *data, size_t len); - bool close(); - uint32_t dataBytes() const { return dataBytes_; } - bool isOpen() const { return (bool)file_; } - - private: - File file_; - uint32_t dataBytes_ = 0; - uint32_t sampleRate_ = 0; - uint16_t bits_ = 0; - uint16_t channels_ = 0; -}; - -// Write a sidecar JSON metadata file next to a recording. -bool writeMetadata(const String &path, const String &json); - -} // namespace storage diff --git a/firmware/src/ux.cpp b/firmware/src/ux.cpp deleted file mode 100644 index 8b8629f..0000000 --- a/firmware/src/ux.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -#include "ux.h" - -#include "pins.h" - -namespace { - -ux::Led ledState = ux::Led::Off; - -// Button debounce. -const uint32_t DEBOUNCE_MS = 40; -int lastReading = HIGH; // active-low: released reads HIGH -int stableState = HIGH; -uint32_t lastChangeMs = 0; - -// Drive the status LED. A plain LED is on/off; recording blinks, error fast-blinks. -void applyLed() { - bool on = false; - switch (ledState) { - case ux::Led::Off: on = false; break; - case ux::Led::Idle: on = (millis() % 2000) < 60; break; // brief heartbeat - case ux::Led::Recording: on = (millis() % 1000) < 500; break; // slow blink - case ux::Led::Error: on = (millis() % 200) < 100; break; // fast blink - } -#if defined(PIN_LED_ACTIVE_LOW) && PIN_LED_ACTIVE_LOW - digitalWrite(PIN_LED, on ? LOW : HIGH); -#else - digitalWrite(PIN_LED, on ? HIGH : LOW); -#endif -} - -} // namespace - -namespace ux { - -void begin() { - pinMode(PIN_BUTTON, INPUT_PULLUP); - pinMode(PIN_LED, OUTPUT); - setLed(Led::Idle); -} - -bool poll() { - bool shortPress = false; - - int reading = digitalRead(PIN_BUTTON); - if (reading != lastReading) { - lastChangeMs = millis(); - lastReading = reading; - } - if ((millis() - lastChangeMs) > DEBOUNCE_MS && reading != stableState) { - stableState = reading; - // Fire on release (LOW -> HIGH) of a debounced press. - if (stableState == HIGH) { - shortPress = true; - } - } - - applyLed(); - return shortPress; -} - -void setLed(Led state) { ledState = state; } - -} // namespace ux diff --git a/firmware/src/ux.h b/firmware/src/ux.h deleted file mode 100644 index f6c79b7..0000000 --- a/firmware/src/ux.h +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -// User controls: a single button (short press toggles recording) and a status LED. -#pragma once - -#include - -namespace ux { - -// LED patterns for the device states M1 cares about. -enum class Led { Off, Idle, Recording, Error }; - -void begin(); - -// Poll the button (debounced) and refresh the LED. Returns true on a short-press event. -bool poll(); - -void setLed(Led state); - -} // namespace ux diff --git a/state/ARCHITECTURE.md b/state/ARCHITECTURE.md index 5fc97b4..98df99a 100644 --- a/state/ARCHITECTURE.md +++ b/state/ARCHITECTURE.md @@ -33,11 +33,11 @@ Three sync paths, exactly as specified: - Responsibility: capture audio, store it, manage power/controls, expose control + data over BLE and WiFi, and upload autonomously when powered. - Location: `firmware/` (PlatformIO, Arduino-ESP32, target ESP32-S3). -- Modules (M1 landed: audio, storage, recorder, ux; rest planned per milestone): - - `audio` - I2S/PDM mic capture presenting 16-bit PCM mono (ESP_I2S). [M1] - - `storage`- microSD (FAT) streaming WAV writer + sidecar JSON metadata. [M1] - - `recorder` - session state machine (idle/recording), file naming, metadata. [M1] - - `ux` - button (short-press start/stop) + status LED (haptic later). [M1] +- Modules (planned): + - `audio` - I2S mic driver, DMA capture, PSRAM ring buffer, WAV encoder. + - `storage`- microSD (FAT) recording files + sidecar JSON metadata. + - `recorder` - session state machine (idle/recording), file naming, metadata. + - `ux` - button (start/stop, long-press pair), LED/haptic status. - `power` - battery read (ADC), charge/VBUS detect -> mode switch. - `config` - NVS-stored settings (WiFi creds, upload target + keys, codec). - `net_wifi` - WiFi manager (join, reconnect), mDNS. diff --git a/state/NOTES.md b/state/NOTES.md index 843bde3..323f03f 100644 --- a/state/NOTES.md +++ b/state/NOTES.md @@ -42,16 +42,6 @@ Per component (see each component's README for detail): - Do not put object-store credentials or WiFi passwords on the microSD in clear; they live in ESP32 NVS. -## Firmware specifics (M1) - -- I2S/PDM via the core-bundled `ESP_I2S` (`I2SClass`) - no extra lib_deps. Dev board uses - `I2S_MODE_STD` (INMP441/ICS-43434); XIAO uses `I2S_MODE_PDM_RX` (onboard mic). -- SD uses a dedicated HSPI bus (`SPIClass(HSPI)`) so it does not clash with other SPI use. -- WAV writer streams a 44-byte header then patches RIFF/data sizes on close by seeking. -- Recording ids are uptime-based until NTP lands (M2); metadata `started_at` is null until - then. See the follow-ups in TODO.md. -- Not yet compiled locally (no PlatformIO on the dev host) - CI builds both profiles. - ## Dead ends - (none yet) diff --git a/state/TODO.md b/state/TODO.md index 6823f2d..1145792 100644 --- a/state/TODO.md +++ b/state/TODO.md @@ -5,13 +5,13 @@ ## In progress -- [ ] M1 Firmware recording core - code complete on `feature/fw-recorder-core`, in PR. - I2S/PDM mic capture -> WAV on microSD, button start/stop, LED status, sidecar JSON - metadata. Not yet compiled locally (no PlatformIO here) or hardware-verified; relies - on Forgejo Actions CI to build both board profiles. +- [ ] M0 Scaffold - repo, licences, state docs, BOM, OpenAPI, component skeletons, CI + (branch: bootstrap on `main`; this is the setup commit) ## Pending (roughly in build order) +- [ ] M1 Firmware recording core: I2S mic capture -> PSRAM ring buffer -> WAV on microSD, + button start/stop, LED status, sidecar JSON metadata. (branch `feature/fw-recorder-core`) - [ ] M2 On-device WiFi + REST API: WiFi manager, mDNS, list/get/download(range)/delete recordings, device status. Implements `api/openapi.yaml` device paths. (branch `feature/fw-rest-api`) @@ -34,22 +34,12 @@ ## Done -- [x] M0 Scaffold - repo, licences, state docs, BOM, OpenAPI, skeletons, CI - (merged to `main`, 2026-07-03) - [x] Create Forgejo repo `laurence/openscribe` (public, main) - 2026-07-03 - [x] Decide hardware, sync model, AI stack, app platform, storage, licensing (see DECISIONS.md) - 2026-07-03 -## Follow-ups (deferred, do when convenient) - -- [ ] Audio calibration: INMP441 emits 24-bit samples in a 32-bit slot. M1 reads in - 16-bit mode (usable for speech, may be quiet). If levels are low on the dev build, - capture 32-bit and right-shift into int16 in `audio.cpp`. -- [ ] Real timestamps: M1 ids/`started_at` are uptime-based (no RTC/NTP yet). M2 adds NTP - over WiFi; switch ids and `started_at` to real UTC then. - ## Blocked / waiting - [ ] Physical build + on-device verification - waiting on parts from `hardware/BOM.md`. - Until parts arrive, firmware is validated by CI build (Forgejo Actions) of both - board profiles, not on real hardware. + Until parts arrive, firmware is developed against the ESP32-S3 target and validated + by build + (where possible) native/host unit tests, not on real hardware.