feat: ESP32-S3 wake-word communicator firmware v1 #1

Merged
troed merged 34 commits from devel into main 2026-08-23 12:54:39 +02:00
Owner

Implements docs/superpowers/specs/2026-08-22-communicator-esp32-design.md per docs/superpowers/plans/2026-08-22-communicator-esp32-firmware.md.

What's here

  • Portable protocol layer (components/protocol, zero IDF deps, C99+cJSON) with host conformance suite vs computer fake_realtime_server — cd host/tests && python3 -m pytest -q = 23 passed; gate green before any on-device work
  • BSP: ES8311/ES7210 via esp_codec_dev (I2S pins 12/13/14/15/16, I2C SDA11/SCL10 bus-scan-corrected), TCA9555 keys K1/K2/K3 = vol-/vol+/mute, WS2812 ring on GPIO38 (RGB fmt per vendor driver)
  • esp-sr WakeNet + AEC feed/fetch tasks replacing raw mic streaming; AEC reference fed from playout path
  • Wire behavior: session.update sent on connect to arm server VAD; ~100 ms input_audio_buffer.append chunks; exactly-one output_audio.played ack after done+drain (guaranteed delivery, logged); unknown/malformed events never fatal; barge-in flushes promptly
  • Player: 256 KB PSRAM jitter ring (~8.2 s cap), Q8 volume gain, mono→stereo, local embedded here.wav wake chime (sox-resampled)
  • State machine IDLE→CONNECTING→LISTENING⇄SPEAKING→DISCONNECTING; 15 s idle disconnect so an idle device never holds a server pipeline slot

Verification

Field-tested against the real server: hands-free "Hi ESP" wake + chime, TTS + transcripts, follow-up turns without re-wake, barge-in, volume/mute keys, idle disconnect releasing the slot (server-journal verified), clean audio. Final whole-branch review approved; deferred minors recorded in ledger.

Custom "Computer" wake model is a later drop-in (menuconfig swap); stock wn9_hiesp ships for now.

Implements docs/superpowers/specs/2026-08-22-communicator-esp32-design.md per docs/superpowers/plans/2026-08-22-communicator-esp32-firmware.md. ## What's here - **Portable protocol layer** (`components/protocol`, zero IDF deps, C99+cJSON) with host conformance suite vs `computer` fake_realtime_server — `cd host/tests && python3 -m pytest -q` = 23 passed; gate green before any on-device work - **BSP**: ES8311/ES7210 via esp_codec_dev (I2S pins 12/13/14/15/16, I2C SDA11/SCL10 bus-scan-corrected), TCA9555 keys K1/K2/K3 = vol-/vol+/mute, WS2812 ring on GPIO38 (RGB fmt per vendor driver) - **esp-sr WakeNet + AEC** feed/fetch tasks replacing raw mic streaming; AEC reference fed from playout path - **Wire behavior**: session.update sent on connect to arm server VAD; ~100 ms input_audio_buffer.append chunks; exactly-one output_audio.played ack after done+drain (guaranteed delivery, logged); unknown/malformed events never fatal; barge-in flushes promptly - **Player**: 256 KB PSRAM jitter ring (~8.2 s cap), Q8 volume gain, mono→stereo, local embedded here.wav wake chime (sox-resampled) - **State machine** IDLE→CONNECTING→LISTENING⇄SPEAKING→DISCONNECTING; 15 s idle disconnect so an idle device never holds a server pipeline slot ## Verification Field-tested against the real server: hands-free "Hi ESP" wake + chime, TTS + transcripts, follow-up turns without re-wake, barge-in, volume/mute keys, idle disconnect releasing the slot (server-journal verified), clean audio. Final whole-branch review approved; deferred minors recorded in ledger. Custom "Computer" wake model is a later drop-in (menuconfig swap); stock wn9_hiesp ships for now.
esp-sr 2.5.1 AFE (AEC + WakeNet wn9_hiesp) now owns the microphone path:
a feed task drains I2S into interleaved [mic, ref] frames and a fetch task
raises app_state_wake_detected() on 'Hi ESP' and streams enhanced audio
while LISTENING/SPEAKING. player feeds afe_ref_write() with exactly what is
on air; the reference ring compensates output-chain latency
(CONFIG_AFE_REF_DELAY_MS) and bounds staleness. mic_stream.c deleted;
K3 stays mute-only. ws_client accepts multi-second inbound deltas
(RX_MAX 256 KB, oversize frames now logged when dropped).
Real backends push deltas larger than the whole old 400 ms ring
(observed: a single 22 KB delta vs 12.8 KB capacity, 135 KB responses).
Allocate 256 KB in PSRAM via heap_caps_malloc + xStreamBufferCreateStatic
so ring memory never lands in internal RAM; drop-newest stays the last
resort and the >=150 ms start-drain gate is unchanged.
The delta storm overflowed the 16-deep event queue and swallowed
EV_RESPONSE_DONE, so output_audio.played was never sent (contract
violation). Enlarge the queue to 64 and route control events (wake, WS
lifecycle, response.done, player-drained) through a bounded retry post
(100 x 20 ms) with a loud ESP_LOGE if it ever gives up; audio-delta and
other server events keep drop-newest semantics.
The idle-watchdog disconnect hit an already-stopped client and logged
"Client was not started" (double teardown). Mirror client liveness in
s_running: set on successful start, cleared on DISCONNECTED (server-side
loss) and at teardown entry, so close runs exactly once. On a graceful
close the component fully stops the task itself, so only fall back to
esp_websocket_client_stop() when close() fails. Also check start()'s
return value instead of ignoring it.
(s_prev, s_state) were written as a non-atomic pair from several tasks
and re-written by the animation task's ERROR auto-revert, so a torn or
resurrected state could wedge the wrong pattern on the ring. Guard both
the setters and the revert with a portMUX critical section and let the
animation task take an atomic snapshot each frame. Verified on device:
state sets arrive in order (IDLE/CONNECTING/LISTENING/SPEAKING/MUTED)
across 115 echo-server response cycles; temporary probe logging removed.
This reverts commit 38a36690d9.
Vendor rgb_led_driver.c (factory_01) configures this ring as RGB; the
previous GRB setting permuted logical colors on the wire.
- README/AGENTS: player ring is 256 KB PSRAM (~8.2 s cap), not 400 ms
- README: follow-up window bounded by IDLE_DISCONNECT_SEC=15 idle watchdog
  measured from last server event (was '~10 s after each response')
- player.h/afe_task.h: drop dead mic_stream references, describe current
  behavior; note runtime ws .buffer_size=8192 wins over CONFIG_WS_BUFFER_SIZE
  in sdkconfig.defaults
troed merged commit fd5200b2aa into main 2026-08-23 12:54:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
starfleet/communicator-esp32!1
No description provided.