feat(firmware): M1 recording core - mic capture to WAV on SD
Implements the first working feature: the device records audio to the microSD
card, toggled by the button, with LED status and per-recording JSON metadata.
What changed:
- firmware/src/audio.{h,cpp}: mic capture via the core-bundled ESP_I2S. Dev board
uses I2S standard mode (INMP441/ICS-43434); XIAO uses PDM mode (onboard mic).
Presents 16-bit PCM mono to callers regardless of board.
- firmware/src/storage.{h,cpp}: microSD on a dedicated HSPI bus + a streaming
WavWriter that writes a 44-byte PCM header and patches RIFF/data sizes on close;
plus sidecar JSON metadata writer.
- firmware/src/recorder.{h,cpp}: idle/recording state machine; creates
/recordings/<id>.wav, pumps mic chunks in on update(), finalises + writes
<id>.json (Recording schema from api/openapi.yaml) on stop.
- firmware/src/ux.{h,cpp}: debounced button (short press toggles) + status LED
patterns (idle/recording/error), active-low aware.
- firmware/src/main.cpp: wires ux + recorder; loop toggles on button and drains
the mic while recording.
- firmware/include/audio_config.h: 16 kHz mono 16-bit, chunk size, rec dir.
- firmware/include/pins.h: added XIAO PDM mic + onboard SD pins, LED active-low flag.
- state/: TODO, ARCHITECTURE, NOTES updated for M1 and the deferred follow-ups.
Why:
- Recording is the foundation every later milestone (transfer, upload, transcription)
builds on. Kept dependency-free (only core-bundled ESP_I2S + SD) for simple CI builds.
Notes:
- Not compiled locally (no PlatformIO on the dev host) or hardware-verified; Forgejo
Actions CI builds both board profiles. Follow-ups tracked in state/TODO.md:
INMP441 16-bit level calibration, and real NTP timestamps (ids are uptime-based
until M2 brings WiFi/NTP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
031074c9a9
commit
054b2f6981
14 changed files with 508 additions and 43 deletions
|
|
@ -2,50 +2,73 @@
|
|||
//
|
||||
// OpenScribe firmware entry point.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// 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)
|
||||
// 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)
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
static const char *FIRMWARE_VERSION = "0.1.0-scaffold";
|
||||
#include "recorder.h"
|
||||
#include "ux.h"
|
||||
|
||||
static const char *FIRMWARE_VERSION = "0.1.0";
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
Serial.println();
|
||||
Serial.printf("OpenScribe firmware %s booting\n", FIRMWARE_VERSION);
|
||||
Serial.printf("\nOpenScribe firmware %s booting\n", FIRMWARE_VERSION);
|
||||
|
||||
#if defined(OPENSCRIBE_BOARD_XIAO)
|
||||
Serial.println("Board profile: XIAO ESP32-S3 (Build A, PDM mic + onboard SD)");
|
||||
Serial.println("Board: XIAO ESP32-S3 (PDM mic + onboard SD)");
|
||||
#else
|
||||
Serial.println("Board profile: ESP32-S3 dev (Build B, I2S mic + SD module)");
|
||||
Serial.println("Board: ESP32-S3 dev (I2S mic + SD module)");
|
||||
#endif
|
||||
|
||||
if (psramFound()) {
|
||||
Serial.printf("PSRAM: %u bytes free\n", (unsigned)ESP.getFreePsram());
|
||||
} else {
|
||||
if (!psramFound()) {
|
||||
Serial.println("WARNING: no PSRAM detected - audio buffering needs a PSRAM board");
|
||||
}
|
||||
|
||||
// M1 onwards wires the modules here:
|
||||
// config::begin(); storage::begin(); audio::begin(); ux::begin(); ...
|
||||
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);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Placeholder heartbeat until the recorder state machine lands (M1).
|
||||
delay(1000);
|
||||
}
|
||||
// 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<unsigned long>(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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue