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:
parent
0b1c34074f
commit
55f7f22a01
@ -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;
|
||||
|
||||
static uint16_t current_sample_rate = 0;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user