spike: BLE+A2DP coexistence test on Board A (throwaway)

Proves dual-mode BLE+A2DP fit in RAM (~110KB free) but BLE radio activity
shreds the A2DP audio (clicks, no music) even when only advertising. Conclusion:
move BLE control to a dedicated ESP32-S3 hub; keep Board A a pure sink.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
blue — ESP32/PlatformIO firmware 2026-06-11 09:15:37 -04:00
parent 0b1c34074f
commit 55f7f22a01

View File

@ -1,96 +1,109 @@
/**
* BikeAudio Board A : A2DP SINK -> I2S MASTER
* BLE-COEXISTENCE SPIKE (throwaway) Board A : A2DP SINK + I2S master + BLE GATT
*
* Part of the 3-board relay. The iPhone connects to this board over Bluetooth
* (A2DP name "BikeAudio"). This board decodes the audio to PCM and clocks it
* out on a shared I2S bus as the MASTER. Boards B and C (A2DP sources) listen
* to this same bus as slaves and stream it to the JBL / Cardo speakers.
* Purpose: prove the classic ESP32 can run the A2DP sink (iPhone audio) AND a
* BLE GATT server at the same time, in dual mode (BTDM), with healthy heap.
* This is the gate for the phone-app control channel. NOT final firmware.
*
* iPhone ))BT)) [Board A: sink] ==I2S==> [Board B: src] ))BT)) JBL
* \====> [Board C: src] ))BT)) Cardo
*
* Why a separate board: one ESP32 cannot be an A2DP sink and source at once
* (single Bluedroid A2DP role), and an A2DP source can hold only one outgoing
* link so the sink and each speaker need their own chip. See README.
*
* I2S OUTPUT pins (this board DRIVES the bus wire these to B and C):
* BCK = GPIO5 WS/LRCK = GPIO25 DATA(out) = GPIO23 + common GND
*
* Build: pio run -e sink (compiled via build_src_filter in platformio.ini)
* Checks at runtime: BLE advertises ("BikeAudio-Ctl"), iPhone still connects to
* A2DP "BikeAudio", I2S master clocks, and heap stays sane.
*/
#include <Arduino.h>
#include "AudioTools.h"
#include "BluetoothA2DPSink.h"
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#define I2S_BCK_PIN 5
#define I2S_WS_PIN 25
#define I2S_DATA_PIN 23
#define SVC_UUID "a1000001-b10e-4c2a-9b00-000000000001"
#define CHR_UUID "a1000002-b10e-4c2a-9b00-000000000002"
I2SStream i2s;
BluetoothA2DPSink sink;
static uint16_t current_sample_rate = 0;
static BLECharacteristic *ctl_char = nullptr;
static volatile bool ble_connected = false;
// Configure / reconfigure the I2S bus as master TX at the given rate.
static void start_i2s(uint16_t rate) {
if (rate == 0) rate = 44100; // SBC default before negotiation
if (rate == 0) rate = 44100;
auto cfg = i2s.defaultConfig(TX_MODE);
cfg.pin_bck = I2S_BCK_PIN;
cfg.pin_ws = I2S_WS_PIN;
cfg.pin_data = I2S_DATA_PIN;
cfg.sample_rate = rate;
cfg.channels = 2;
cfg.bits_per_sample = 16;
cfg.is_master = true; // Board A clocks the whole bus
cfg.buffer_count = 8;
cfg.buffer_size = 512;
cfg.pin_bck = I2S_BCK_PIN; cfg.pin_ws = I2S_WS_PIN; cfg.pin_data = I2S_DATA_PIN;
cfg.sample_rate = rate; cfg.channels = 2; cfg.bits_per_sample = 16;
cfg.is_master = true; cfg.buffer_count = 8; cfg.buffer_size = 512;
i2s.begin(cfg);
current_sample_rate = rate;
Serial.printf("[SINK] I2S master @ %u Hz / 16-bit / stereo\n", rate);
Serial.printf("[SINK] I2S master @ %u Hz\n", rate);
}
// Called from the BT task with decoded PCM. Keep it cheap — just push to I2S.
void write_pcm_to_i2s(const uint8_t *data, uint32_t len) {
i2s.write(data, len);
}
void write_pcm_to_i2s(const uint8_t *data, uint32_t len) { i2s.write(data, len); }
void on_conn_state(esp_a2d_connection_state_t state, void *obj) {
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) Serial.println("[SINK] iPhone CONNECTED");
else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) Serial.println("[SINK] iPhone disconnected");
}
class SrvCB : public BLEServerCallbacks {
void onConnect(BLEServer *) override { ble_connected = true; Serial.println("[BLE] central connected"); }
void onDisconnect(BLEServer *s) override { ble_connected = false; Serial.println("[BLE] central disconnected"); s->getAdvertising()->start(); }
};
class ChrCB : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *c) override {
Serial.printf("[BLE] write: '%s'\n", c->getValue().c_str());
}
};
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("=== BikeAudio Board A — A2DP SINK -> I2S master ===");
Serial.println("=== BLE SPIKE — A2DP sink + BLE GATT (BTDM) ===");
Serial.printf("[boot] heap=%u\n", ESP.getFreeHeap());
// BLE first: btStart() brings the controller up in dual mode (BTDM), then
// bluedroid + BLE host. A2DP (also bluedroid) then registers on top.
BLEDevice::init("BikeAudio-Ctl");
Serial.printf("[BLE] init done heap=%u\n", ESP.getFreeHeap());
BLEServer *srv = BLEDevice::createServer();
srv->setCallbacks(new SrvCB());
BLEService *svc = srv->createService(SVC_UUID);
ctl_char = svc->createCharacteristic(
CHR_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
ctl_char->setValue("ready");
svc->start();
BLEAdvertising *adv = BLEDevice::getAdvertising();
adv->addServiceUUID(SVC_UUID);
adv->setScanResponse(true);
BLEDevice::startAdvertising();
Serial.printf("[BLE] advertising 'BikeAudio-Ctl' heap=%u\n", ESP.getFreeHeap());
// Now the A2DP sink in dual mode.
start_i2s(44100);
// false => the sink does NOT run its own I2S; we forward PCM ourselves.
sink.set_default_bt_mode(ESP_BT_MODE_BTDM);
sink.set_stream_reader(write_pcm_to_i2s, false);
sink.set_on_connection_state_changed(on_conn_state);
sink.set_auto_reconnect(true);
sink.start("BikeAudio");
Serial.println("[SINK] Advertising 'BikeAudio' — connect from iPhone");
Serial.printf("[SINK] A2DP started heap=%u\n", ESP.getFreeHeap());
Serial.println("[SINK] Advertising 'BikeAudio' — connect iPhone");
}
void loop() {
// Follow the negotiated sample rate (iPhone usually 44100; reconfigure if not).
uint16_t sr = sink.sample_rate();
if (sr != 0 && sr != current_sample_rate) {
Serial.printf("[SINK] sample rate changed %u -> %u, reconfiguring I2S\n",
current_sample_rate, sr);
start_i2s(sr);
}
if (sr != 0 && sr != current_sample_rate) start_i2s(sr);
static unsigned long last = 0;
if (millis() - last > 5000) {
Serial.printf("[SINK] iPhone=%s heap=%u\n",
sink.is_connected() ? "YES" : "no", ESP.getFreeHeap());
Serial.printf("[STATUS] iPhone=%s BLE=%s heap=%u\n",
sink.is_connected() ? "YES" : "no",
ble_connected ? "YES" : "no", ESP.getFreeHeap());
last = millis();
}
delay(100);
delay(50);
}