diff --git a/apps/robot-hardware/platformio.ini b/apps/robot-hardware/platformio.ini index 9e62092..3af807a 100644 --- a/apps/robot-hardware/platformio.ini +++ b/apps/robot-hardware/platformio.ini @@ -30,11 +30,6 @@ build_flags = -DHW_SERIAL_BAUD=921600 ; Idle timeout before the eyes fall back to the default animation (ms) -DHW_HEARTBEAT_TIMEOUT_MS=5000 - ; Hardware UART2 pins used to talk to the Raspberry Pi. - ; The OLED eyes already claim GPIO 16/17 (UART2 default pins), - ; so Serial2 is remapped to these two free pins instead. - -DHW_UART_RX_PIN=27 - -DHW_UART_TX_PIN=13 build_unflags = -std=gnu++11 diff --git a/apps/robot-hardware/scripts/esp-play.ts b/apps/robot-hardware/scripts/esp-play.ts index f328d45..ff5c782 100644 --- a/apps/robot-hardware/scripts/esp-play.ts +++ b/apps/robot-hardware/scripts/esp-play.ts @@ -130,8 +130,7 @@ async function main(): Promise { port.open((err) => (err ? reject(err) : resolve())); }); - let ready = false; - const readyWaiters: Array<() => void> = []; + const pongWaiters: Array<() => void> = []; const finished = new Promise((resolve, reject) => { const timeout = setTimeout( @@ -151,9 +150,13 @@ async function main(): Promise { resolve(); return; } + if (line === 'PONG') { + while (pongWaiters.length) pongWaiters.shift()!(); + continue; + } if (line === 'READY') { - ready = true; - while (readyWaiters.length) readyWaiters.shift()!(); + // Firmware just booted (USB DTR reset). Ignore — the + // PING/PONG handshake is our real readiness signal. continue; } if (line.startsWith('ERR ')) { @@ -168,19 +171,26 @@ async function main(): Promise { port.on('error', reject); }); - // Wait for READY so we don't send PLAY into the bootloader. + // PING/PONG handshake — validates the link in both directions + // regardless of whether the firmware was recently rebooted or not. + console.log('→ PING'); + port.write('PING\n'); await new Promise((resolve, reject) => { - if (ready) return resolve(); const timer = setTimeout( - () => reject(new Error('timeout waiting for READY from firmware')), - 5000, + () => + reject( + new Error( + 'no PONG from firmware — check TX/RX wiring (cross!), baud rate, and that the ESP32 is powered', + ), + ), + 3000, ); - readyWaiters.push(() => { + pongWaiters.push(() => { clearTimeout(timer); resolve(); }); }); - await new Promise((r) => setTimeout(r, 50)); + console.log('→ PONG received'); console.log(`→ PLAY ${pcm.length} bytes`); port.write(`PLAY ${pcm.length}\n`); diff --git a/apps/robot-hardware/scripts/esp-record.ts b/apps/robot-hardware/scripts/esp-record.ts index 4a54218..26f7989 100644 --- a/apps/robot-hardware/scripts/esp-record.ts +++ b/apps/robot-hardware/scripts/esp-record.ts @@ -78,8 +78,7 @@ async function main(): Promise { let remaining = 0; const chunks: Buffer[] = []; let lineBuf = ''; - let ready = false; - const readyWaiters: Array<() => void> = []; + const pongWaiters: Array<() => void> = []; const finished = new Promise((resolve, reject) => { const timeout = setTimeout( @@ -122,9 +121,12 @@ async function main(): Promise { clearTimeout(timeout); const pcm = Buffer.concat(chunks); resolve(pcm); + } else if (line === 'PONG') { + while (pongWaiters.length) pongWaiters.shift()!(); } else if (line === 'READY') { - ready = true; - while (readyWaiters.length) readyWaiters.shift()!(); + // Firmware just booted. Ignore, the PING/PONG handshake + // below handles both "just booted" and "been running for + // hours" alike. } else if (line.startsWith('LOG ')) { console.log(`[esp] ${line.slice(4)}`); } else if (line.startsWith('ERR ')) { @@ -139,20 +141,28 @@ async function main(): Promise { port.on('error', reject); }); - // The ESP32 resets on port open (DTR/RTS). Wait until it prints - // READY so we don't send commands into the bootloader. + // PING/PONG handshake. Works whether the ESP32 just booted + // (USB/DTR reset) or has been running for hours (UART wiring + // never resets anything). Also validates that TX and RX are + // correctly crossed — a one-way wiring error would time out here. + console.log('→ PING'); + port.write('PING\n'); await new Promise((resolve, reject) => { - if (ready) return resolve(); const timer = setTimeout( - () => reject(new Error('timeout waiting for READY from firmware')), - 5000, + () => + reject( + new Error( + 'no PONG from firmware — check TX/RX wiring (cross!), baud rate, and that the ESP32 is powered', + ), + ), + 3000, ); - readyWaiters.push(() => { + pongWaiters.push(() => { clearTimeout(timer); resolve(); }); }); - await new Promise((r) => setTimeout(r, 50)); + console.log('→ PONG received'); console.log(`→ REC ${durationMs} ms — speak now!`); port.write(`REC ${durationMs}\n`); diff --git a/apps/robot-hardware/src/main.cpp b/apps/robot-hardware/src/main.cpp index c7cdb9e..14426e5 100644 --- a/apps/robot-hardware/src/main.cpp +++ b/apps/robot-hardware/src/main.cpp @@ -1,15 +1,19 @@ // Ti-Pote — Minimal audio bring-up firmware (ESP32-WROOM-32) // -// GOAL: prove the I2S audio chain (INMP441 + MAX98357A) end to end -// with nothing else in the loop — no Pi, no OLED, no protocol frames. -// The ESP32 is plugged into a computer via USB and the host runs -// two tiny scripts: +// GOAL: prove the I2S audio chain (INMP441 + MAX98357A) end to end. +// The command stream lives on Serial2 (hardware UART2, pins RX=27 +// TX=13) which is wired to the Raspberry Pi's UART0 (/dev/serial0). +// The USB Serial port is kept only for boot-time diagnostics — all +// the real traffic goes over the UART to the Pi. // -// scripts/esp-record.mjs -// scripts/esp-play.mjs +// On the host side, the same two scripts we used with the USB link +// work unchanged — just pass `ESP_PORT=/dev/serial0`: // -// Protocol over USB Serial (921600 baud, line-based for commands, -// raw bytes for audio): +// ESP_PORT=/dev/serial0 pnpm esp:record out.wav 3000 +// ESP_PORT=/dev/serial0 pnpm esp:play out.wav +// +// Protocol (same as before, 921600 baud, line-based for commands, +// raw bytes for audio payload): // // host → esp32 // "PING\n" ping @@ -37,6 +41,21 @@ #include #include +// ────────────────────────────────────────────────────────── +// Comms config — UART2 to the Raspberry Pi +// ────────────────────────────────────────────────────────── + +// Hardware UART2 remapped to pins that don't clash with anything +// else on the devkit. TX = GPIO 13, RX = GPIO 27. +static constexpr int HW_UART_RX_PIN = 27; +static constexpr int HW_UART_TX_PIN = 13; +static constexpr long HW_UART_BAUD = 921600; + +// HW_COMM is the Stream that carries the command/audio protocol. +// Changing this single #define lets us swap between USB (Serial) +// and the Pi-facing UART (Serial2). +#define HW_COMM Serial2 + // ────────────────────────────────────────────────────────── // Audio config // ────────────────────────────────────────────────────────── @@ -66,13 +85,13 @@ static char g_line[64]; static size_t g_lineLen = 0; static void sendLog(const char* msg) { - Serial.print("LOG "); - Serial.println(msg); + HW_COMM.print("LOG "); + HW_COMM.println(msg); } static void sendErr(const char* msg) { - Serial.print("ERR "); - Serial.println(msg); + HW_COMM.print("ERR "); + HW_COMM.println(msg); } // ────────────────────────────────────────────────────────── @@ -162,8 +181,8 @@ static void handleRec(uint32_t durationMs) { const uint32_t totalSamples = (SAMPLE_RATE * durationMs) / 1000; const uint32_t totalBytes = totalSamples * sizeof(int16_t); - Serial.print("BEGIN "); - Serial.println(totalBytes); + HW_COMM.print("BEGIN "); + HW_COMM.println(totalBytes); // Flush whatever old noise is in the mic DMA first. i2s_zero_dma_buffer(I2S_NUM_0); @@ -174,13 +193,13 @@ static void handleRec(uint32_t durationMs) { if (want > OUT_S16_SAMPLES) want = OUT_S16_SAMPLES; const size_t got = micReadMono(g_micMono, want); if (got == 0) continue; - Serial.write(reinterpret_cast(g_micMono), - got * sizeof(int16_t)); + HW_COMM.write(reinterpret_cast(g_micMono), + got * sizeof(int16_t)); sent += got; } - Serial.println(); - Serial.println("END"); + HW_COMM.println(); + HW_COMM.println("END"); } static void handlePlay(uint32_t totalBytes) { @@ -188,9 +207,9 @@ static void handlePlay(uint32_t totalBytes) { // with a pop. i2s_zero_dma_buffer(I2S_NUM_0); - // Give Serial.readBytes a generous timeout so a jittery host - // doesn't abort us mid-playback. - Serial.setTimeout(2000); + // Give readBytes a generous timeout so a jittery host doesn't + // abort us mid-playback. + HW_COMM.setTimeout(2000); uint32_t remaining = totalBytes; while (remaining > 0) { @@ -200,7 +219,7 @@ static void handlePlay(uint32_t totalBytes) { if (want & 1) want -= 1; if (want == 0) want = 2; - const size_t got = Serial.readBytes(g_spkInBuf, want); + const size_t got = HW_COMM.readBytes(g_spkInBuf, want); if (got == 0) { sendErr("PLAY read timeout"); return; @@ -213,12 +232,12 @@ static void handlePlay(uint32_t totalBytes) { // Let the last frames actually reach the speaker, then clear. delay(50); i2s_zero_dma_buffer(I2S_NUM_0); - Serial.println("OK"); + HW_COMM.println("OK"); } static void handleLine(const char* line) { if (strcmp(line, "PING") == 0) { - Serial.println("PONG"); + HW_COMM.println("PONG"); return; } if (strncmp(line, "REC ", 4) == 0) { @@ -244,25 +263,35 @@ static void handleLine(const char* line) { // ────────────────────────────────────────────────────────── void setup() { - // Bump the UART RX buffer WAY above the 256-byte default so we + // USB Serial is kept as a *boot-time logger only*. It gives + // you something to look at via `pio device monitor` when the + // board is plugged into a laptop, without interfering with + // the Pi link on Serial2. + Serial.begin(115200); + Serial.println("[boot] USB logger up, real comms on Serial2"); + + // Bump the UART2 RX buffer WAY above the 256-byte default so we // can absorb a full PLAY payload (up to a few tens of KB) without // losing bytes if the host floods us. - Serial.setRxBufferSize(16 * 1024); - Serial.begin(921600); + HW_COMM.setRxBufferSize(16 * 1024); + HW_COMM.begin(HW_UART_BAUD, SERIAL_8N1, HW_UART_RX_PIN, HW_UART_TX_PIN); delay(50); if (!audioBegin()) { sendErr("I2S init failed"); + Serial.println("[boot] I2S init FAILED"); } else { sendLog("I2S ready"); + Serial.println("[boot] I2S ready"); } - Serial.println("READY"); + HW_COMM.println("READY"); + Serial.println("[boot] READY sent on Serial2"); } void loop() { - while (Serial.available() > 0) { - const int c = Serial.read(); + while (HW_COMM.available() > 0) { + const int c = HW_COMM.read(); if (c < 0) break; if (c == '\r') continue; if (c == '\n') {