add command rasp to esp32

This commit is contained in:
ordinarthur 2026-04-09 02:58:29 +02:00
parent c19d9a7cf4
commit 28d5bd44e0
4 changed files with 100 additions and 56 deletions

View File

@ -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

View File

@ -130,8 +130,7 @@ async function main(): Promise<void> {
port.open((err) => (err ? reject(err) : resolve()));
});
let ready = false;
const readyWaiters: Array<() => void> = [];
const pongWaiters: Array<() => void> = [];
const finished = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
@ -151,9 +150,13 @@ async function main(): Promise<void> {
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<void> {
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<void>((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`);

View File

@ -78,8 +78,7 @@ async function main(): Promise<void> {
let remaining = 0;
const chunks: Buffer[] = [];
let lineBuf = '';
let ready = false;
const readyWaiters: Array<() => void> = [];
const pongWaiters: Array<() => void> = [];
const finished = new Promise<Buffer>((resolve, reject) => {
const timeout = setTimeout(
@ -122,9 +121,12 @@ async function main(): Promise<void> {
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<void> {
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<void>((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`);

View File

@ -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 <file.raw> <duration_ms>
// scripts/esp-play.mjs <file.raw>
// 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 <driver/i2s.h>
#include <string.h>
// ──────────────────────────────────────────────────────────
// 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<const uint8_t*>(g_micMono),
got * sizeof(int16_t));
HW_COMM.write(reinterpret_cast<const uint8_t*>(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') {