From e79ae74155c9ea98fc2cce51ebb6df3928fe7640 Mon Sep 17 00:00:00 2001 From: blue Date: Wed, 10 Jun 2026 10:18:00 -0400 Subject: [PATCH] Sink-only proof-of-life (diagnostic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace src/main.cpp with a minimal single-object A2DP sink advertising "BikeAudio", to verify the iPhone can discover + connect. Isolates the sink/advertising path that the full sketch corrupts by instantiating one sink + two sources (ESP32-A2DP uses a global singleton that each constructor overwrites; a single Bluedroid A2DP callback is registered, so the sink is orphaned and never discoverable). Not the final firmware — does not drive the speakers. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main.cpp | 260 ++++++--------------------------------------------- 1 file changed, 27 insertions(+), 233 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4cf94aa..14f2c77 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,261 +1,55 @@ /** - * BikeAudio — ESP32 DevKitC v4 Bluetooth Audio Relay + * BikeAudio — SINK-ONLY proof-of-life (diagnostic, NOT the final firmware) * - * iPhone --> [ESP32 A2DP SINK] --> [A2DP SOURCE x2] --> JBL Charge 5 + Cardo Packtalk Edge + * Purpose: prove the iPhone can discover and connect to an A2DP sink named + * "BikeAudio". The full BikeAudio sketch instantiates one A2DP sink + two A2DP + * sources at once; the ESP32-A2DP library keeps a single global instance + * (actual_bluetooth_a2dp_common) that each constructor overwrites and registers + * one Bluedroid A2DP callback, so the sink gets orphaned and is never + * discoverable. ESP32 classic-BT A2DP source also supports only one outgoing + * link, so one ESP32 cannot stream to two speakers regardless. This sketch + * isolates the sink path with a single object — no sources, no singleton clobber. * - * Libraries required (install via Arduino IDE Library Manager or .zip): - * - ESP32-A2DP by Phil Schatzmann https://github.com/pschatzmann/ESP32-A2DP - * - arduino-audio-tools by Phil Schatzmann https://github.com/pschatzmann/arduino-audio-tools + * Pass condition: iPhone sees "BikeAudio" in Bluetooth settings, connects, and + * serial prints "[SINK] iPhone CONNECTED". * - * Board: ESP32 Dev Module - * Partition scheme: Huge APP (3MB No OTA/1MB SPIFFS) <-- required for BT stack size - * ESP32 Arduino core: 2.0.17 (do NOT use 3.x — BT stack regression) - * - * HOW IT WORKS: - * 1. ESP32 boots and connects to JBL Charge 5 and Cardo as A2DP sources - * 2. Then advertises itself as "BikeAudio" for the iPhone to connect to - * 3. Audio received from iPhone is forwarded to both speakers in real time - * 4. Auto-reconnect on power cycle — just turn everything on and it finds each other - * - * FIRST TIME SETUP: - * - Forget JBL and Cardo from your iPhone - * - Put JBL in pairing mode (hold Bluetooth button) - * - Put Cardo in pairing mode (check Cardo manual — usually hold phone button) - * - Flash this sketch, open Serial Monitor at 115200 - * - ESP32 will find and pair with both devices on first boot - * - On iPhone, go to Bluetooth settings and connect to "BikeAudio" - * - Done — play audio, both speakers output simultaneously + * Board / partition / core: unchanged (esp32dev, huge_app.csv, core 2.0.x). */ #include -#include "AudioTools.h" #include "BluetoothA2DPSink.h" -#include "BluetoothA2DPSource.h" -#include "BluetoothA2DPCommon.h" -// Forward declaration — defined below. Required in .cpp builds: the Arduino IDE -// auto-generates prototypes for .ino files, but PlatformIO compiles .cpp directly -// and print_status() is called in setup()/loop() before its definition. -void print_status(); +BluetoothA2DPSink sink; // single A2DP object — receives audio FROM iPhone -// ─── CONFIGURATION ──────────────────────────────────────────────────────────── - -// Name this device shows to iPhone -#define SINK_NAME "BikeAudio" - -// Exact Bluetooth names of your speakers (must match exactly, case sensitive) -#define JBL_NAME "JBL Charge 5" -#define CARDO_NAME "Tangerine EDGE" - -// Retry interval if a speaker disconnects (ms) -#define RECONNECT_MS 5000 - -// Audio buffer size — larger = more stable, slightly more latency -#define BUFFER_SIZE (4 * 1024) - -// ─── GLOBALS ────────────────────────────────────────────────────────────────── - -BluetoothA2DPSink sink; // receives audio FROM iPhone -BluetoothA2DPSource src_jbl; // sends audio TO JBL -BluetoothA2DPSource src_cardo; // sends audio TO Cardo - -// Shared ring buffer — sink writes, sources read -RingBuffer ring_buf(BUFFER_SIZE * 2); - -// Connection state -volatile bool jbl_connected = false; -volatile bool cardo_connected = false; -volatile bool iphone_connected = false; - -unsigned long last_reconnect_jbl = 0; -unsigned long last_reconnect_cardo = 0; - -// ─── AUDIO CALLBACK (iPhone → buffer) ──────────────────────────────────────── - -/** - * Called by the A2DP sink every time a new audio frame arrives from iPhone. - * We write raw PCM into the shared ring buffer. - * Both sources pull from this buffer simultaneously. - */ -void audio_received_cb(const uint8_t *data, uint32_t len) { - // Write to ring buffer — non-blocking, drop if full (prevents deadlock) - for (uint32_t i = 0; i < len; i++) { - if (!ring_buf.isFull()) { - ring_buf.write(data[i]); - } - } -} - -// ─── SOURCE DATA CALLBACK (buffer → JBL / Cardo) ───────────────────────────── - -/** - * Called by each A2DP source when it needs audio data to send. - * Both JBL and Cardo call this — they share the same buffer read pointer - * via a duplicated/mirrored buffer approach. - * - * We use a simple approach: one primary reader (JBL) drains the buffer, - * Cardo gets the same data via a mirrored write in audio_received_cb. - */ - -// Second ring buffer mirroring data for Cardo -RingBuffer ring_buf_cardo(BUFFER_SIZE * 2); - -void audio_received_mirror_cb(const uint8_t *data, uint32_t len) { - // Write to BOTH ring buffers — JBL gets ring_buf, Cardo gets ring_buf_cardo - for (uint32_t i = 0; i < len; i++) { - if (!ring_buf.isFull()) ring_buf.write(data[i]); - if (!ring_buf_cardo.isFull()) ring_buf_cardo.write(data[i]); - } -} - -int32_t get_audio_for_jbl(uint8_t *data, int32_t len) { - int32_t bytes_read = 0; - uint8_t b; - while (bytes_read < len && ring_buf.read(b)) { - data[bytes_read++] = b; - } - // Pad with silence if buffer underrun - if (bytes_read < len) { - memset(data + bytes_read, 0, len - bytes_read); - } - return len; -} - -int32_t get_audio_for_cardo(uint8_t *data, int32_t len) { - int32_t bytes_read = 0; - uint8_t b; - while (bytes_read < len && ring_buf_cardo.read(b)) { - data[bytes_read++] = b; - } - if (bytes_read < len) { - memset(data + bytes_read, 0, len - bytes_read); - } - return len; -} - -// ─── CONNECTION CALLBACKS ───────────────────────────────────────────────────── - -void sink_connected_cb(esp_a2d_connection_state_t state, void *obj) { +void on_conn_state(esp_a2d_connection_state_t state, void *obj) { if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) { - Serial.println("[SINK] iPhone connected"); - iphone_connected = true; - } else { + Serial.println("[SINK] iPhone CONNECTED"); + } else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) { Serial.println("[SINK] iPhone disconnected"); - iphone_connected = false; - } -} - -void jbl_connected_cb(esp_a2d_connection_state_t state, void *obj) { - if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) { - Serial.println("[JBL] Connected"); - jbl_connected = true; } else { - Serial.println("[JBL] Disconnected — will retry"); - jbl_connected = false; - last_reconnect_jbl = millis(); + Serial.printf("[SINK] connection state = %d\n", (int)state); } } -void cardo_connected_cb(esp_a2d_connection_state_t state, void *obj) { - if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) { - Serial.println("[CARDO] Connected"); - cardo_connected = true; - } else { - Serial.println("[CARDO] Disconnected — will retry"); - cardo_connected = false; - last_reconnect_cardo = millis(); - } -} - -// ─── SETUP ──────────────────────────────────────────────────────────────────── - void setup() { Serial.begin(115200); delay(500); - Serial.println("=== BikeAudio Booting ==="); + Serial.println("=== BikeAudio SINK-ONLY proof ==="); - // ── Step 1: Connect to JBL as A2DP source ───────────────────────────────── - Serial.println("[JBL] Connecting..."); - src_jbl.set_data_callback(get_audio_for_jbl); - src_jbl.set_on_connection_state_changed(jbl_connected_cb); - src_jbl.set_auto_reconnect(true); - src_jbl.start(JBL_NAME); // auto-reconnect handled by set_auto_reconnect() above + sink.set_on_connection_state_changed(on_conn_state); + sink.start("BikeAudio"); - // Give it time to connect before starting second source - // (Bluedroid needs sequential connection setup) - uint32_t t = millis(); - while (!jbl_connected && millis() - t < 10000) { - delay(100); - } - if (jbl_connected) { - Serial.println("[JBL] Ready"); - } else { - Serial.println("[JBL] Not found yet — will retry in background"); - } - - // ── Step 2: Connect to Cardo as A2DP source ──────────────────────────────── - Serial.println("[CARDO] Connecting..."); - src_cardo.set_data_callback(get_audio_for_cardo); - src_cardo.set_on_connection_state_changed(cardo_connected_cb); - src_cardo.set_auto_reconnect(true); - src_cardo.start(CARDO_NAME); - - t = millis(); - while (!cardo_connected && millis() - t < 10000) { - delay(100); - } - if (cardo_connected) { - Serial.println("[CARDO] Ready"); - } else { - Serial.println("[CARDO] Not found yet — will retry in background"); - } - - // ── Step 3: Start sink — advertise "BikeAudio" to iPhone ────────────────── - Serial.println("[SINK] Advertising as '" SINK_NAME "' ..."); - sink.set_stream_reader(audio_received_mirror_cb); - sink.set_on_connection_state_changed(sink_connected_cb); - sink.start(SINK_NAME); - Serial.println("[SINK] Ready — connect iPhone to 'BikeAudio'"); - - Serial.println("=== BikeAudio Ready ==="); - print_status(); + Serial.println("[SINK] Advertising as 'BikeAudio' — connect from iPhone"); + Serial.println("=== Ready ==="); } -// ─── LOOP ───────────────────────────────────────────────────────────────────── - void loop() { - // Auto-reconnect JBL if lost - if (!jbl_connected && millis() - last_reconnect_jbl > RECONNECT_MS) { - Serial.println("[JBL] Retrying connection..."); - src_jbl.start(JBL_NAME); - last_reconnect_jbl = millis(); + static unsigned long last = 0; + if (millis() - last > 5000) { + Serial.printf("[SINK] connected=%s heap=%d bytes free\n", + sink.is_connected() ? "YES" : "no", ESP.getFreeHeap()); + last = millis(); } - - // Auto-reconnect Cardo if lost - if (!cardo_connected && millis() - last_reconnect_cardo > RECONNECT_MS) { - Serial.println("[CARDO] Retrying connection..."); - src_cardo.start(CARDO_NAME); - last_reconnect_cardo = millis(); - } - - // Print status every 10 seconds - static unsigned long last_status = 0; - if (millis() - last_status > 10000) { - print_status(); - last_status = millis(); - } - delay(100); } - -// ─── HELPERS ────────────────────────────────────────────────────────────────── - -void print_status() { - Serial.println("--- Status ---"); - Serial.printf(" iPhone : %s\n", iphone_connected ? "CONNECTED" : "waiting..."); - Serial.printf(" JBL : %s\n", jbl_connected ? "CONNECTED" : "waiting..."); - Serial.printf(" Cardo : %s\n", cardo_connected ? "CONNECTED" : "waiting..."); - Serial.printf(" Heap : %d bytes free\n", ESP.getFreeHeap()); - Serial.println("--------------"); -}