Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/configs/sdkconfig.a2dp_external_codec
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Self-contained sdkconfig fragment for the bt_a2dp_sink_external_codec example.
# Can be used alone (`ESP_IDF_SDKCONFIG_DEFAULTS=this_file`) OR layered
# on top of .github/configs/sdkconfig.defaults.

# Stack sizes that Rust applications generally want bumped from defaults.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=8192
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=8192
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096

# Bluetooth Classic + Bluedroid (A2DP sink prerequisites).
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
# Disable the BLE host stack entirely. We've added a cfg gate to
# esp-idf-svc's `bt::ble` module (src/bt.rs) so it only compiles when
# CONFIG_BT_BLE_ENABLED=y, which sidesteps the BLE GATT bindings that
# changed in esp-idf v6.1-dev.
CONFIG_BT_BLE_ENABLED=n
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BTC_TASK_STACK_SIZE=15000

# A2DP sink in external-codec mode with AAC + SBC SEPs.
CONFIG_BT_A2DP_ENABLE=y
CONFIG_BT_A2DP_USE_EXTERNAL_CODEC=y
CONFIG_BT_A2DP_CODEC_AAC_ENABLED=y
CONFIG_BT_A2DP_SEP_NUM_MAX=2

# ESP32-WROVER (8 MB QSPI PSRAM). Bluedroid + A2DP wants more
# heap than the bare 320 KiB internal SRAM gives.
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_QUAD=y
CONFIG_SPIRAM_TYPE_AUTO=y
CONFIG_SPIRAM_SPEED_80M=y
# Flash and PSRAM share the SPI bus, so the two clock speeds must match.
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384
CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST=y
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ remote_component = { name = "espressif/lan87xx", version = "1.*" }
### Added
- Compatibility with ESP-IDF V6.0, and some pre-release 6.0.x.
- Added support for the Generic Ethernet PHY driver: particularly useful on ESP-IDF 6.0+ as it is built-in.
- Added support for Bluetooth A2DP External Codec API in esp-idf v6.1 (w/ AAC codec negotiation)

## [0.52.1] - 2026-03-10

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ log = { version = "0.4", default-features = false }
uncased = { version = "0.9.7", default-features = false }
embedded-hal-async = { version = "1", default-features = false }
embedded-svc = { version = "0.29", default-features = false }
esp-idf-hal = { version = "0.46.1", default-features = false }
esp-idf-hal = { version = "0.46.1", default-features = false, features = ["pcnt-legacy"] }
Comment thread
billylindeman marked this conversation as resolved.
Outdated
embassy-time-driver = { version = "0.2.1", optional = true, features = ["tick-hz-1_000_000"] }
embassy-time-queue-utils = { version = "0.3", optional = true }
embassy-futures = "0.1.2"
Expand Down
1 change: 1 addition & 0 deletions espflash.toml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
[idf_format_args]
partition_table = "partitions.csv"
117 changes: 117 additions & 0 deletions examples/bt_a2dp_sink_external_codec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! A2DP sink example using the external-codec API.
//!
//! Initialises Bluedroid classic, advertises the device as A2DP sink with
//! both AAC and SBC stream endpoints, and logs incoming events including
//! the encoded audio buffers delivered by the peer source (typically a
//! phone). Decoding of those buffers is intentionally left out — pair this
//! with Espressif's `esp_audio_codec` managed component (or any other
//! decoder) on top.
//!
//! Required sdkconfig settings:
//!
//! ```ignore
//! CONFIG_BT_ENABLED=y
//! CONFIG_BT_BLUEDROID_ENABLED=y
//! CONFIG_BT_CLASSIC_ENABLED=y
//! CONFIG_BT_A2DP_ENABLE=y
//! CONFIG_BT_A2DP_USE_EXTERNAL_CODEC=y
//! CONFIG_BT_A2DP_SEP_NUM_MAX=2
//! CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
//! CONFIG_BTDM_CTRL_MODE_BLE_ONLY=n
//! CONFIG_BTDM_CTRL_MODE_BTDM=n
//! CONFIG_BT_BTC_TASK_STACK_SIZE=15000
//! ```
//!
//! To run this example:
//! MCU=esp32 ESP_IDF_SDKCONFIG_DEFAULTS=.github/configs/sdkconfig.a2dp_external_codec cargo +esp espflash flash --target xtensa-esp32-espidf --example bt_a2dp_sink_external_codec --monitor

#![allow(unknown_lints)]
#![allow(unexpected_cfgs)]

#[cfg(all(esp32, esp_idf_bt_a2dp_use_external_codec))]
fn main() -> anyhow::Result<()> {
example::main()
}

#[cfg(not(all(esp32, esp_idf_bt_a2dp_use_external_codec)))]
fn main() -> anyhow::Result<()> {
println!("FALLBACK MAIN: cfg gate did not match");
panic!("This example requires ESP32 with CONFIG_BT_A2DP_USE_EXTERNAL_CODEC=y");
}

#[cfg(all(esp32, esp_idf_bt_a2dp_use_external_codec))]
mod example {
use std::sync::Arc;

use esp_idf_svc::bt::a2dp::{A2dpEvent, Codec, EspA2dp, Sink};
use esp_idf_svc::bt::gap::{DiscoveryMode, EspGap};
use esp_idf_svc::bt::{reduce_bt_memory, BtClassic, BtDriver};
use esp_idf_svc::hal::delay::FreeRtos;
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::log::EspLogger;
use esp_idf_svc::nvs::EspDefaultNvsPartition;

use log::info;

pub fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
EspLogger::initialize_default();
info!("step 1: logger up");

let peripherals = Peripherals::take()?;
info!("step 2: peripherals taken");
let nvs = EspDefaultNvsPartition::take()?;
info!("step 3: nvs taken");

let mut modem = peripherals.modem;

reduce_bt_memory(unsafe { modem.reborrow() })?;
info!("step 4: reduce_bt_memory ok");

let bt = Arc::new(BtDriver::<BtClassic>::new(modem, Some(nvs.clone()))?);
info!("step 5: BtDriver up");

let gap = EspGap::new(bt.clone())?;
info!("step 6: gap created");
gap.set_device_name("ESP32_A2DP_SINK")?;
gap.set_scan_mode(true, DiscoveryMode::Discoverable)?;
info!("step 7: gap configured");

let a2dp = EspA2dp::<'_, _, _, Sink>::new_external_codec(bt.clone())?;
info!("step 8: a2dp external-codec sink up");

a2dp.subscribe(|event| {
match &event {
A2dpEvent::SinkAudioData(buf) => {
info!(
"audio frame: {} frames, {} bytes, ts {}",
buf.frames(),
buf.data().len(),
buf.timestamp()
);
}
A2dpEvent::SinkEndpointRegistered { seid, state } => {
info!("SEP {seid} register result: {state:?}");
}
A2dpEvent::AudioCodecConfigured { bd_addr, codec } => {
info!("negotiated codec with {bd_addr:?}: {codec:?}");
}
other => info!("a2dp event: {other:?}"),
}
0
})?;

info!("step 9: subscribed; registering SEPs");
// AAC at seid 0 (preferred), SBC at seid 1 (mandatory fallback).
for (seid, codec) in [(0, Codec::aac_default()), (1, Codec::sbc_default())] {
info!("registering {} at seid {}", codec.name(), seid);
a2dp.register_sink_endpoint(seid, &codec)?;
}

info!("step 10: A2DP sink ready; pair with phone and play audio");

loop {
FreeRtos::delay_ms(10_000);
}
}
}
1 change: 1 addition & 0 deletions src/bt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extern crate alloc;
pub mod a2dp;
#[cfg(all(esp32, esp_idf_bt_classic_enabled, esp_idf_bt_a2dp_enable))]
pub mod avrc;
#[cfg(esp_idf_bt_ble_enabled)]
Comment thread
ivmarkov marked this conversation as resolved.
pub mod ble;
#[cfg(all(esp32, esp_idf_bt_classic_enabled))]
pub mod gap;
Expand Down
Loading
Loading