diff --git a/src/board_sink.cpp b/src/board_sink.cpp index f5b67bd..ee8a405 100644 --- a/src/board_sink.cpp +++ b/src/board_sink.cpp @@ -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 #include "AudioTools.h" #include "BluetoothA2DPSink.h" +#include +#include +#include #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); }