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
|
* Purpose: prove the classic ESP32 can run the A2DP sink (iPhone audio) AND a
|
||||||
* (A2DP name "BikeAudio"). This board decodes the audio to PCM and clocks it
|
* BLE GATT server at the same time, in dual mode (BTDM), with healthy heap.
|
||||||
* out on a shared I2S bus as the MASTER. Boards B and C (A2DP sources) listen
|
* This is the gate for the phone-app control channel. NOT final firmware.
|
||||||
* to this same bus as slaves and stream it to the JBL / Cardo speakers.
|
|
||||||
*
|
*
|
||||||
* iPhone ))BT)) [Board A: sink] ==I2S==> [Board B: src] ))BT)) JBL
|
* Checks at runtime: BLE advertises ("BikeAudio-Ctl"), iPhone still connects to
|
||||||
* \====> [Board C: src] ))BT)) Cardo
|
* A2DP "BikeAudio", I2S master clocks, and heap stays sane.
|
||||||
*
|
|
||||||
* 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)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#include "AudioTools.h"
|
#include "AudioTools.h"
|
||||||
#include "BluetoothA2DPSink.h"
|
#include "BluetoothA2DPSink.h"
|
||||||
|
#include <BLEDevice.h>
|
||||||
|
#include <BLEServer.h>
|
||||||
|
#include <BLEUtils.h>
|
||||||
|
|
||||||
#define I2S_BCK_PIN 5
|
#define I2S_BCK_PIN 5
|
||||||
#define I2S_WS_PIN 25
|
#define I2S_WS_PIN 25
|
||||||
#define I2S_DATA_PIN 23
|
#define I2S_DATA_PIN 23
|
||||||
|
|
||||||
|
#define SVC_UUID "a1000001-b10e-4c2a-9b00-000000000001"
|
||||||
|
#define CHR_UUID "a1000002-b10e-4c2a-9b00-000000000002"
|
||||||
|
|
||||||
I2SStream i2s;
|
I2SStream i2s;
|
||||||
BluetoothA2DPSink sink;
|
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) {
|
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);
|
auto cfg = i2s.defaultConfig(TX_MODE);
|
||||||
cfg.pin_bck = I2S_BCK_PIN;
|
cfg.pin_bck = I2S_BCK_PIN; cfg.pin_ws = I2S_WS_PIN; cfg.pin_data = I2S_DATA_PIN;
|
||||||
cfg.pin_ws = I2S_WS_PIN;
|
cfg.sample_rate = rate; cfg.channels = 2; cfg.bits_per_sample = 16;
|
||||||
cfg.pin_data = I2S_DATA_PIN;
|
cfg.is_master = true; cfg.buffer_count = 8; cfg.buffer_size = 512;
|
||||||
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;
|
|
||||||
i2s.begin(cfg);
|
i2s.begin(cfg);
|
||||||
current_sample_rate = rate;
|
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) {
|
void on_conn_state(esp_a2d_connection_state_t state, void *obj) {
|
||||||
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) Serial.println("[SINK] iPhone CONNECTED");
|
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");
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(500);
|
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);
|
start_i2s(44100);
|
||||||
|
sink.set_default_bt_mode(ESP_BT_MODE_BTDM);
|
||||||
// false => the sink does NOT run its own I2S; we forward PCM ourselves.
|
|
||||||
sink.set_stream_reader(write_pcm_to_i2s, false);
|
sink.set_stream_reader(write_pcm_to_i2s, false);
|
||||||
sink.set_on_connection_state_changed(on_conn_state);
|
sink.set_on_connection_state_changed(on_conn_state);
|
||||||
sink.set_auto_reconnect(true);
|
sink.set_auto_reconnect(true);
|
||||||
sink.start("BikeAudio");
|
sink.start("BikeAudio");
|
||||||
|
Serial.printf("[SINK] A2DP started heap=%u\n", ESP.getFreeHeap());
|
||||||
Serial.println("[SINK] Advertising 'BikeAudio' — connect from iPhone");
|
Serial.println("[SINK] Advertising 'BikeAudio' — connect iPhone");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Follow the negotiated sample rate (iPhone usually 44100; reconfigure if not).
|
|
||||||
uint16_t sr = sink.sample_rate();
|
uint16_t sr = sink.sample_rate();
|
||||||
if (sr != 0 && sr != current_sample_rate) {
|
if (sr != 0 && sr != current_sample_rate) start_i2s(sr);
|
||||||
Serial.printf("[SINK] sample rate changed %u -> %u, reconfiguring I2S\n",
|
|
||||||
current_sample_rate, sr);
|
|
||||||
start_i2s(sr);
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned long last = 0;
|
static unsigned long last = 0;
|
||||||
if (millis() - last > 5000) {
|
if (millis() - last > 5000) {
|
||||||
Serial.printf("[SINK] iPhone=%s heap=%u\n",
|
Serial.printf("[STATUS] iPhone=%s BLE=%s heap=%u\n",
|
||||||
sink.is_connected() ? "YES" : "no", ESP.getFreeHeap());
|
sink.is_connected() ? "YES" : "no",
|
||||||
|
ble_connected ? "YES" : "no", ESP.getFreeHeap());
|
||||||
last = millis();
|
last = millis();
|
||||||
}
|
}
|
||||||
delay(100);
|
delay(50);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user