feat(ble_audio): Support running ISO & LE Audio with Bluedroid Host

This commit is contained in:
Liu Linyan
2026-06-04 15:41:36 +08:00
parent 77f5214ab0
commit 91fc8d3905
223 changed files with 14714 additions and 2347 deletions
@@ -9,9 +9,9 @@
This example acts as a **BAP Broadcast Sink**. It scans for extended advertisements that carry the Broadcast Audio Announcement Service UUID and whose complete-name AD matches the hard-coded `"BAP Broadcast Source"` string; on a hit, it creates a periodic-advertising sync, builds a BAP broadcast sink for that PA handle and broadcast ID, decodes the BASE / BIGInfo from the PA channel, and then synchronizes to the BIG to receive BIS streams.
The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Sink-side APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_pacs_register` + `esp_ble_audio_pacs_cap_register` (sink PAC and sink location enabled), `esp_ble_audio_bap_scan_delegator_register` (so a Broadcast Assistant can drive PA-sync, broadcast-code, and BIS-sync requests via BASS), `esp_ble_audio_bap_broadcast_sink_register_cb`, `esp_ble_audio_bap_broadcast_sink_create` / `_sync` / `_stop` / `_delete`, and `esp_ble_audio_bap_base_get_subgroup_count` / `_get_bis_indexes`. PAC capabilities are LC3 with sample rates 16 kHz + 24 kHz, frame duration 10 ms, 1 channel, 40–60 octets/frame, 1 frame/SDU. The fallback broadcast code is `"1234"`; if a Broadcast Assistant has supplied one through BASS it is used instead.
The build runs on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) and the ESP BLE Audio component set. Sink-side APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_pacs_register` + `esp_ble_audio_pacs_cap_register` (sink PAC and sink location enabled), `esp_ble_audio_bap_scan_delegator_register` (so a Broadcast Assistant can drive PA-sync, broadcast-code, and BIS-sync requests via BASS), `esp_ble_audio_bap_broadcast_sink_register_cb`, `esp_ble_audio_bap_broadcast_sink_create` / `_sync` / `_stop` / `_delete`, and `esp_ble_audio_bap_base_get_subgroup_count` / `_get_bis_indexes`. PAC capabilities are LC3 with sample rates 16 kHz + 24 kHz, frame duration 10 ms, 1 channel, 40–60 octets/frame, 1 frame/SDU. The fallback broadcast code is `"1234"`; if a Broadcast Assistant has supplied one through BASS it is used instead.
After PA sync is established, `ble_gap_disc_cancel()` stops the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` re-arms the scanner.
After PA sync is established, `scan_stop()` halts the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` calls `scan_start()` to re-arm. The host-specific GAP/PA-sync plumbing lives in `main/bluedroid/scan.c` and `main/nimble/scan.c`; `main.c` only sees the host-agnostic interface in `scan.h`.
## Requirements
@@ -34,11 +34,24 @@ The example inherits a Just-Works pairing model (LE Secure Connections, no MITM,
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4 # or esp32s31
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -46,8 +59,8 @@ idf.py -p PORT flash monitor
1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(&info)` with `info.gap_cb = iso_gap_app_cb`.
2. PACS is registered (`snk_pac` + `snk_loc`), each stream's `ops` field is wired to `stream_ops`, and the LC3 sink capability is registered via `esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, ...)`.
3. The scan delegator (`scan_delegator_cbs`: `recv_state_updated`, `pa_sync_req`, `pa_sync_term_req`, `broadcast_code`, `bis_sync_req`) and broadcast-sink callbacks (`base_recv`, `syncable`) are registered, then `esp_ble_audio_common_start(NULL)` runs.
4. `ext_scan_start()` runs passive extended discovery (`itvl=window=160`). For each `ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV`, `data_cb` matches the complete/short/broadcast name AD type against `"BAP Broadcast Source"` and records the Broadcast ID from the Broadcast Audio Service Data.
5. On match (and only when not already PA-syncing and no scan-delegator state is pinned), `pa_sync_create()` calls `ble_gap_periodic_adv_sync_create` with `skip=0`, `sync_timeout=10s`.
4. `scan_init()` performs host-specific GAP setup (Bluedroid: registers the GAP callback for `*_COMPLETE_EVT` semaphore signalling + posts `EXT_ADV_REPORT` / `PERIODIC_ADV_SYNC_ESTAB` / `PERIODIC_ADV_REPORT` / `PERIODIC_ADV_SYNC_LOST` to the audio engine; NimBLE: no-op — the scan-instance callback is passed at `ble_gap_disc` / `ble_gap_periodic_adv_sync_create` time). `scan_start()` then runs passive extended discovery (`itvl=window=160`). For each `ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV`, `data_cb` matches the complete/short/broadcast name AD type against `"BAP Broadcast Source"` and records the Broadcast ID from the Broadcast Audio Service Data.
5. On match (and only when not already PA-syncing and no scan-delegator state is pinned), `pa_sync_create(addr_type, addr, sid)` invokes the host-specific PA-sync create routine (`ble_gap_periodic_adv_sync_create` / `esp_ble_gap_periodic_adv_create_sync`) with `skip=0`, `sync_timeout=10s`.
6. `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC` clears `pa_syncing`, cancels discovery, stores `sync_handle`, and calls `esp_ble_audio_bap_broadcast_sink_create()`.
7. `base_recv_cb` extracts the subgroup count and BIS index bitfield (masked by `bis_index_mask`); when no Broadcast Assistant is connected, `requested_bis_sync` defaults to `ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF`.
8. `syncable_cb` AND-masks the BASE bitfield with the requested mask, copies `TARGET_BROADCAST_CODE` if the BIG is encrypted (unless BASS already supplied one), and calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen mask and `streams_p`.
@@ -1,4 +1,11 @@
set(srcs "main.c")
idf_component_register(SRCS "${srcs}"
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/scan.c")
else()
list(APPEND srcs "nimble/scan.c")
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "scan.h"
static SemaphoreHandle_t scan_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t scan_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
static esp_ble_ext_scan_params_t ext_scan_params = {
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
.uncoded_cfg = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.scan_interval = SCAN_INTERVAL,
.scan_window = SCAN_WINDOW,
},
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
scan_op_status = param->set_ext_scan_params.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
scan_op_status = param->ext_scan_start.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
scan_op_status = param->ext_scan_stop.status;
xSemaphoreGive(scan_sem);
break;
/* PA sync / ext adv events: forwarded by adapter's BTA path, not here. */
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
scan_sem = xSemaphoreCreateBinary();
if (scan_sem == NULL) {
ESP_LOGE(TAG, "Failed to create scan semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(scan_sem);
return err;
}
return 0;
}
int ext_scan_start(void)
{
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
ESP_LOGI(TAG, "Scanning for broadcast source...");
return ESP_OK;
}
int ext_scan_stop(void)
{
WAIT_API(esp_ble_gap_stop_ext_scan());
return ESP_OK;
}
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
{
esp_ble_gap_periodic_adv_sync_params_t params = {
.filter_policy = 0,
.sid = sid,
.addr_type = addr_type,
.skip = PA_SYNC_SKIP,
.sync_timeout = PA_SYNC_TIMEOUT,
};
memcpy(params.addr, addr, sizeof(params.addr));
/* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is
* air-dependent and surfaces asynchronously. */
return esp_ble_gap_periodic_adv_create_sync(&params);
}
int pa_sync_terminate(uint16_t sync_handle)
{
return esp_ble_gap_periodic_adv_sync_terminate(sync_handle);
}
@@ -13,10 +13,6 @@
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_lc3_defs.h"
#include "esp_ble_audio_bap_api.h"
@@ -25,18 +21,13 @@
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define TAG "BAP_BSNK"
#include "scan.h"
#define TARGET_DEVICE_NAME "BAP Broadcast Source"
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
#define TARGET_BROADCAST_CODE "1234"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
#define PA_SYNC_HANDLE_INIT UINT16_MAX
#define CONN_HANDLE_INIT UINT16_MAX
@@ -90,61 +81,6 @@ static esp_ble_audio_pacs_cap_t cap = {
.codec_cap = &codec_cap,
};
static void ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return;
}
ESP_LOGI(TAG, "Scanning for broadcast source...");
}
static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid)
{
struct ble_gap_periodic_sync_params params = {0};
ble_addr_t sync_addr = {0};
sync_addr.type = addr->type;
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
return ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, &params,
example_audio_gap_event_cb, NULL);
}
static int pa_sync_terminate(void)
{
int err;
err = ble_gap_periodic_adv_sync_terminate(sync_handle);
if (err) {
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
return err;
}
ESP_LOGI(TAG, "PA sync terminated");
return 0;
}
static void recv_state_updated_cb(esp_ble_conn_t *conn,
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
{
@@ -199,13 +135,27 @@ static int pa_sync_term_req_cb(esp_ble_conn_t *conn,
req_recv_state = recv_state;
err = pa_sync_terminate();
/* Nothing to terminate if PAST/PA-sync never landed (e.g., PAST setup failed
* earlier). Issuing the HCI terminate with the sentinel handle gets 0x12
* (Invalid HCI Command Parameters) back from the controller. Mirrors
* cap_acceptor_broadcast.c. */
if (sync_handle == PA_SYNC_HANDLE_INIT) {
ESP_LOGI(TAG, "PA sync never established, skip terminate");
return 0;
}
err = pa_sync_terminate(sync_handle);
if (err) {
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
return -EIO;
}
sync_handle = PA_SYNC_HANDLE_INIT;
ESP_LOGI(TAG, "PA sync terminated");
/* Let the synthesized PA_SYNC_LOST event drive cleanup via pa_sync_lost
* (it gates on the original sync_handle). Resetting sync_handle here would
* make that gate miss and leak broadcast_sink. Mirrors
* cap_acceptor_broadcast.c. */
return 0;
}
@@ -481,7 +431,6 @@ static bool data_cb(uint8_t type, const uint8_t *data,
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
{
struct scan_recv_data sr = {0};
bt_addr_le_t addr;
int err;
/* Periodic advertising interval. 0 if no periodic advertising. */
@@ -500,10 +449,9 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
if (pa_syncing == false && req_recv_state == NULL) {
broadcaster_broadcast_id = sr.broadcast_id;
addr.type = event->ext_scan_recv.addr.type;
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
err = pa_sync_create(&addr, event->ext_scan_recv.sid);
err = pa_sync_create(event->ext_scan_recv.addr.type,
event->ext_scan_recv.addr.val,
event->ext_scan_recv.sid);
if (err) {
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
return;
@@ -533,7 +481,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event)
* via the PA sync channel, so the extended scanner is no longer
* needed. Stop it now — pa_sync_lost() will restart it on loss.
*/
rc = ble_gap_disc_cancel();
rc = ext_scan_stop();
if (rc) {
ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc);
}
@@ -618,6 +566,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "esp_ble_audio_common_api.h"
#include "scan.h"
/* Forward only the GAP events the application consumes. */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_EXT_DISC:
case BLE_GAP_EVENT_PERIODIC_SYNC:
case BLE_GAP_EVENT_PERIODIC_REPORT:
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for broadcast source...");
return 0;
}
int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
{
struct ble_gap_periodic_sync_params params = {0};
ble_addr_t sync_addr = {0};
sync_addr.type = addr_type;
memcpy(sync_addr.val, addr, sizeof(sync_addr.val));
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
return ble_gap_periodic_adv_sync_create(&sync_addr, sid, &params,
gap_event_cb, NULL);
}
int pa_sync_terminate(uint16_t sync_handle)
{
return ble_gap_periodic_adv_sync_terminate(sync_handle);
}
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "ble_audio_example_utils.h"
#define TAG "BAP_BSNK"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
int app_host_init(void);
int ext_scan_start(void);
int ext_scan_stop(void);
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid);
int pa_sync_terminate(uint16_t sync_handle);
@@ -3,14 +3,14 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -24,6 +24,8 @@ CONFIG_BT_PAC_SRC_NOTIFIABLE=y
CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,12 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -9,7 +9,7 @@
This example acts as a **BAP Broadcast Source**. It creates a BAP broadcast source from the LC3 `16_2_1` broadcast preset, encodes the BASE into the periodic advertising payload, places the Broadcast Audio Announcement Service UUID and a fixed 24-bit Broadcast ID (`0x123456`) in the extended advertising payload, and then starts the BIG so that BIS streams transmit synthetic SDU data on a fixed cadence.
The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1.
The build runs on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1.
The TX scheduler is built on `example_audio_tx_scheduler_*` helpers; the source comment notes that ESP timer resolution is not accurate enough for the SDU interval, so a `k_work_delayable`-based scheduler is used instead.
@@ -34,11 +34,24 @@ The example inherits a Just-Works pairing model (LE Secure Connections, no MITM,
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4 # or esp32s31
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -46,7 +59,7 @@ idf.py -p PORT flash monitor
1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(NULL)` (no GAP callback for this role).
2. `broadcast_source_setup()` registers `source_started` / `source_stopped` callbacks, populates per-stream channel-allocation BIS metadata (left/right), registers `stream_ops` on each stream, and calls `esp_ble_audio_bap_broadcast_source_create()` with the encrypted preset.
3. `esp_ble_audio_common_start(NULL)` starts the audio stack; each stream's `example_audio_tx_scheduler_init()` is wired with `tx_scheduler_cb`.
4. `ext_adv_start()` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), writes Broadcast Audio Service Data + Broadcast ID + complete name, configures periodic adv (100 ms), writes the BASE returned by `esp_ble_audio_bap_broadcast_source_get_base()`, then starts periodic and extended advertising.
4. `adv_init()` performs host-specific GAP setup (Bluedroid: registers the GAP callback for `*_COMPLETE_EVT` semaphore signalling; NimBLE: no-op — adv-instance callback is passed at configure time). `ext_adv_start()` builds the extended (Broadcast Audio Service Data + Broadcast ID + complete name) and periodic (BASE from `esp_ble_audio_bap_broadcast_source_get_base()`) payloads in host-agnostic bytes, then calls `adv_start(ext_data, ext_len, per_data, per_len)`. The host-specific implementation in `main/bluedroid/adv.c` or `main/nimble/adv.c` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), configures periodic adv (100 ms), writes both payloads, and starts periodic and extended advertising.
5. `broadcast_start()` calls `esp_ble_audio_bap_broadcast_adv_add()` and `esp_ble_audio_bap_broadcast_source_start()` on the same `ADV_HANDLE`, which kicks off BIGInfo + BIS creation.
6. When each BIS stream goes streaming, `stream_started_cb` allocates an SDU-sized buffer and calls `example_audio_tx_scheduler_start()` at `preset_active.qos.interval`; the scheduler invokes `broadcast_source_tx()`, which fills the buffer with the low byte of `seq_num` and calls `esp_ble_audio_bap_stream_send()`.
7. `stream_sent_cb` forwards completions to `example_audio_tx_scheduler_on_sent()` for drift accounting; `stream_stopped_cb` and `stream_disconnected_cb` stop the scheduler; `source_stopped_cb` frees all per-stream buffers.
@@ -1,4 +1,11 @@
set(srcs "main.c")
idf_component_register(SRCS "${srcs}"
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/adv.c")
else()
list(APPEND srcs "nimble/adv.c")
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "ble_audio_example_utils.h"
#define TAG "BAP_BSRC"
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
#define PER_ADV_INTERVAL_MS 100
int app_host_init(void);
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len);
@@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "adv.h"
static SemaphoreHandle_t adv_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t adv_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED,
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.channel_map = ADV_CHNL_ALL,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.primary_phy = ESP_BLE_GAP_PHY_1M,
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_2M,
.sid = ADV_SID,
.scan_req_notif = false,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.tx_power = ADV_TX_POWER,
};
static esp_ble_gap_periodic_adv_params_t periodic_adv_params = {
.interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
.properties = 0,
};
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
[0] = { ADV_HANDLE, 0, 0 },
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
adv_op_status = param->ext_adv_start.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->peroid_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->period_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT:
adv_op_status = param->period_adv_start.status;
xSemaphoreGive(adv_sem);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
adv_sem = xSemaphoreCreateBinary();
if (adv_sem == NULL) {
ESP_LOGE(TAG, "Failed to create adv semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(adv_sem);
return err;
}
return 0;
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len)
{
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params));
#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false));
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true));
#else
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data));
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE));
#endif
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -9,17 +9,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_lc3_defs.h"
#include "esp_ble_audio_bap_api.h"
@@ -29,7 +22,7 @@
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define TAG "BAP_BSRC"
#include "adv.h"
ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active,
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT |
@@ -42,16 +35,6 @@ ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active,
#define LOCAL_BROADCAST_CODE "1234" /* Maximum length is 16 */
#define LOCAL_BROADCAST_ID 0x123456
#define ADV_HANDLE 0x00
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
#define STREAM_COUNT CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT
#define SUBGROUP_COUNT CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT
@@ -369,138 +352,55 @@ static uint8_t *per_adv_data_get(uint8_t *data_len)
return data;
}
static int ext_adv_start(void)
static int broadcast_start(void)
{
struct ble_gap_periodic_adv_params per_params = {0};
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
esp_ble_audio_bap_broadcast_adv_info_t info = {
.adv_handle = ADV_HANDLE,
};
/* BASE (per_adv_data_get) needs broadcast_source to exist — built earlier
* by broadcast_source_setup(), so safe to call here. */
uint8_t *ext_data = NULL;
uint8_t *per_data = NULL;
uint8_t data_len = 0;
int err;
uint8_t ext_len = 0;
uint8_t per_len = 0;
int err = 0;
ext_params.connectable = 0;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = ADV_ADDRESS;
ext_params.primary_phy = ADV_PRIMARY_PHY;
ext_params.secondary_phy = ADV_SECONDARY_PHY;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = ADV_INTERVAL;
ext_params.itvl_max = ADV_INTERVAL;
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
goto end;
}
ext_data = ext_adv_data_get(&data_len);
ext_data = ext_adv_data_get(&ext_len);
if (ext_data == NULL) {
err = -ENOMEM;
goto end;
}
data = os_msys_get_pkthdr(data_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
err = -ENOMEM;
goto end;
}
err = os_mbuf_append(data, ext_data, data_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
goto end;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
goto end;
}
per_params.include_tx_power = 0;
per_params.itvl_min = PER_ADV_INTERVAL;
per_params.itvl_max = PER_ADV_INTERVAL;
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
if (err) {
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
goto end;
}
per_data = per_adv_data_get(&data_len);
per_data = per_adv_data_get(&per_len);
if (per_data == NULL) {
err = -ENOMEM;
goto end;
}
data = os_msys_get_pkthdr(data_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get per adv mbuf");
err = -ENOMEM;
goto end;
}
err = os_mbuf_append(data, per_data, data_len);
err = ext_adv_start(ext_data, ext_len, per_data, per_len);
if (err) {
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
os_mbuf_free_chain(data);
goto end;
}
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
goto end;
}
err = ble_gap_periodic_adv_start(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
goto end;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
goto end;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
free(ext_data);
}
if (per_data) {
free(per_data);
}
return err;
}
static void broadcast_start(void)
{
esp_ble_audio_bap_broadcast_adv_info_t info = {
.adv_handle = ADV_HANDLE,
};
int err;
err = esp_ble_audio_bap_broadcast_adv_add(&info);
if (err) {
ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err);
return;
goto end;
}
err = esp_ble_audio_bap_broadcast_source_start(broadcast_source, ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
return;
}
end:
if (ext_data != NULL) {
free(ext_data);
}
if (per_data != NULL) {
free(per_data);
}
return err;
}
void app_main(void)
@@ -521,6 +421,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(NULL);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -548,10 +454,9 @@ void app_main(void)
}
}
err = ext_adv_start();
err = broadcast_start();
if (err) {
ESP_LOGE(TAG, "Failed to start broadcast, err %d", err);
return;
}
broadcast_start();
}
@@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "adv.h"
/* Non-connectable broadcaster: no GAP events for the application. */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
return 0;
}
int app_host_init(void)
{
return 0;
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len)
{
struct ble_gap_periodic_adv_params per_params = {0};
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data;
int err;
ext_params.connectable = 0;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(ext_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return -1;
}
err = os_mbuf_append(data, ext_data, ext_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return err;
}
per_params.include_tx_power = 0;
per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
if (err) {
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(per_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get per adv mbuf");
return -1;
}
err = os_mbuf_append(data, per_data, per_len);
if (err) {
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
return err;
}
err = ble_gap_periodic_adv_start(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
return err;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return err;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -3,14 +3,19 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
CONFIG_BT_BAP_BROADCAST_SOURCE=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,9 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -7,7 +7,7 @@
## Overview
This example implements a **BAP Unicast Client** on top of the NimBLE host stack. It scans for a peer advertising the ASCS service UUID with a complete local name of `BAP Unicast Server`, opens an ACL connection, initiates pairing, exchanges MTU, and starts GATT service discovery.
This example implements a **BAP Unicast Client** on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay). It scans for a peer advertising the ASCS service UUID with a complete local name of `BAP Unicast Server`, opens an ACL connection, initiates pairing, exchanges MTU, and starts GATT service discovery.
Once the peer is reachable, the client uses the BAP unicast client API (`esp_ble_audio_bap_unicast_client_discover`, `esp_ble_audio_bap_stream_config`, `esp_ble_audio_bap_unicast_group_create`, `esp_ble_audio_bap_stream_qos`, `esp_ble_audio_bap_stream_enable`, `esp_ble_audio_bap_stream_connect`, `esp_ble_audio_bap_stream_start`) to discover sink and source ASEs, configure each with the LC3 `16_2_1` unicast preset, build a unicast group, set QoS, enable, and ISO-connect the streams. Source (TX-from-server) streams are started by this client; sink (TX-to-server) streams are started by the server.
@@ -34,19 +34,32 @@ This example inherits a Just-Works pairing model (LE Secure Connections, no MITM
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, sets the device name `BAP Unicast Client`, and calls `ext_scan_start`.
1. `app_main` initializes NVS, the BLE host, runs `app_host_init` for host-specific setup (GAP callback, plus a Bluedroid-only GATTC app registration so we have a `gattc_if` for outgoing aux-open), brings up the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, calls `set_device_name()` (uses `LOCAL_DEVICE_NAME` from `central.h`), then calls `ext_scan_start`.
2. Passive extended scanning runs forever; `scan_data_cb` matches the complete local name `BAP Unicast Server` and parses the ASCS service-data AD for announcement type plus a 32-bit available-contexts field.
3. On a connectable match `conn_create` cancels scanning and issues `ble_gap_connect`; the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `ble_gap_security_initiate`.
4. After the security-change event the client triggers `ble_gattc_exchange_mtu`; `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start`, and once both MTU and discovery are reported `discover_sinks` runs.
3. On a connectable match `ext_scan_recv` calls `ext_scan_stop` and then `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`); the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `pairing_start` (NimBLE: `ble_gap_security_initiate`; Bluedroid: `esp_ble_set_encryption`).
4. After the security-change event the client triggers `exchange_mtu`. On NimBLE this issues `ble_gattc_exchange_mtu`; on Bluedroid the GATTC adapter auto-configures MTU inside `BTA_GATTC_Enh_Open`, so `exchange_mtu` drives `esp_ble_audio_gattc_disc_start` directly. `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start` (the retry is idempotent — `-EALREADY` is treated as success), and once both MTU and discovery are reported `discover_sinks` runs.
5. `discover_cb` chains sink discovery into `discover_sources`; `endpoint_cb` records each EP into the next free `sinks[]`/`sources[]` slot, and source completion kicks `configure_stream`, which configures every EP with `unicast_preset.codec_cfg`.
6. After the last `config_cb`, `create_group` builds `pair_params` (sinks first, then sources) with `ESP_BLE_ISO_PACKING_SEQUENTIAL` and calls `esp_ble_audio_bap_unicast_group_create`, then `set_stream_qos`.
7. When `is_all_stream_qos_set` returns true `enable_stream` walks every stream; on the final `enable_cb`, `connect_stream` ISO-connects each pair (sink first if available); when all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX.
@@ -116,8 +129,8 @@ I (xxx) BAP_UCL: Disconnected: handle ... reason 0x...
Run [unicast_server](../unicast_server/) on a second board.
1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name.
2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`.
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`.
2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`).
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and `Security:`.
4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server.
5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation.
6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics.
@@ -1,5 +1,12 @@
set(srcs "stream_tx.c"
"main.c")
idf_component_register(SRCS "${srcs}"
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/central.c")
else()
list(APPEND srcs "nimble/central.c")
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,193 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_ble_audio_common_api.h"
#include "central.h"
static SemaphoreHandle_t scan_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t scan_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
/* Cached peer address. Bluedroid's pairing and disconnect APIs key off
* bd_addr rather than conn_handle, so we stash the addr at conn_create
* time and reuse it in pairing_start / security_failed_recover. */
static esp_bd_addr_t peer_bda;
static esp_ble_ext_scan_params_t ext_scan_params = {
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
.uncoded_cfg = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.scan_interval = SCAN_INTERVAL,
.scan_window = SCAN_WINDOW,
},
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
scan_op_status = param->set_ext_scan_params.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
scan_op_status = param->ext_scan_start.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
scan_op_status = param->ext_scan_stop.status;
xSemaphoreGive(scan_sem);
break;
/* SMP request handling for Just Works pairing (IO_CAP=NONE). NC_REQ
* still fires under LE Secure Connections — auto-accept. We never get
* SEC_REQ here because the central initiates via esp_ble_set_encryption. */
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
break;
/* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT
* is forwarded by adapter's BTA path, don't re-post here. */
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
scan_sem = xSemaphoreCreateBinary();
if (scan_sem == NULL) {
ESP_LOGE(TAG, "Failed to create scan semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(scan_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_scan_start(void)
{
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
ESP_LOGI(TAG, "Scanning for unicast server...");
return ESP_OK;
}
int ext_scan_stop(void)
{
WAIT_API(esp_ble_gap_stop_ext_scan());
return ESP_OK;
}
int conn_create(uint8_t addr_type, const uint8_t addr[6])
{
/* Values shared with NimBLE side via central.h for behavioral parity.
* Without prefer_ext_connect_params_set, L2CAP falls back to defaults
* and logs "No extend connection parameters set". */
const esp_ble_gap_conn_params_t conn_params = {
.scan_interval = INIT_SCAN_INTERVAL,
.scan_window = INIT_SCAN_WINDOW,
.interval_min = CONN_INTERVAL,
.interval_max = CONN_INTERVAL,
.latency = CONN_LATENCY,
.supervision_timeout = CONN_TIMEOUT,
.min_ce_len = CONN_MIN_CE_LEN,
.max_ce_len = CONN_MAX_CE_LEN,
};
esp_gatt_if_t gattc_if;
esp_err_t err;
memcpy(peer_bda, addr, sizeof(peer_bda));
err = esp_ble_gap_prefer_ext_connect_params_set(
peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err);
return err;
}
/* Use the audio engine's GATTC if so OPEN/CONNECT events route back to
* the engine. aux_open initiates an ACL against an extended advertiser.
*
* engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet registered;
* feeding that into aux_open silently no-ops in BTC and no acl_connect
* event ever fires, leaving the caller in a no-scan / no-conn dead state. */
gattc_if = esp_ble_audio_bluedroid_get_gattc_if();
if (gattc_if == ESP_GATT_IF_NONE) {
ESP_LOGE(TAG, "GATTC not registered");
return ESP_ERR_INVALID_STATE;
}
return esp_ble_gattc_aux_open(gattc_if, peer_bda,
(esp_ble_addr_type_t)addr_type, true);
}
int pairing_start(uint16_t conn_handle)
{
(void)conn_handle;
return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM);
}
int exchange_mtu(uint16_t conn_handle)
{
(void)conn_handle;
/* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open
* brings up the ACL — the MTU_UPDATED event arrives without an explicit
* kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op
* on bluedroid and a real ble_gattc_exchange_mtu on nimble. */
return 0;
}
void security_failed_recover(uint16_t conn_handle, uint8_t status)
{
(void)conn_handle;
/* Asymmetric bond state: we still hold an LTK for this peer but it
* cleared its side, so encrypt-with-cached-key times out. Drop the bond
* and tear down the link; the next reconnect runs fresh pairing. */
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
esp_ble_remove_bond_device(peer_bda);
esp_ble_gap_disconnect(peer_bda);
}
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "ble_audio_example_utils.h"
#define TAG "BAP_UCL"
#define LOCAL_DEVICE_NAME "BAP Unicast Client"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
/* ACL init parameters shared between bluedroid and nimble host wrappers.
* Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */
#define INIT_SCAN_INTERVAL 16 /* 10ms */
#define INIT_SCAN_WINDOW 16 /* 10ms */
#define CONN_INTERVAL 24 /* 30ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 5s */
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_MAX_CE_LEN 0xFFFF
int app_host_init(void);
int set_device_name(void);
int ext_scan_start(void);
int ext_scan_stop(void);
int conn_create(uint8_t addr_type, const uint8_t addr[6]);
int pairing_start(uint16_t conn_handle);
int exchange_mtu(uint16_t conn_handle);
/* Recover from an SMP failure on the given connection — typically deletes the
* stale bond and terminates the link so the next reconnect runs fresh
* pairing. Host-specific implementation. */
void security_failed_recover(uint16_t conn_handle, uint8_t status);
@@ -7,31 +7,16 @@
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "host/ble_gap.h"
#include "services/gap/ble_svc_gap.h"
#include "central.h"
#include "stream_tx.h"
#define TARGET_DEVICE_NAME "BAP Unicast Server"
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define INIT_SCAN_INTERVAL 16 /* 10ms */
#define INIT_SCAN_WINDOW 16 /* 10ms */
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
#define CONN_MAX_CE_LEN 0xFFFF
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_DURATION 10000 /* 10s */
#define CONN_HANDLE_INIT 0xFFFF
#define ASCS_RSP_NONE (0b00)
@@ -41,7 +26,6 @@
static uint16_t default_conn_handle = CONN_HANDLE_INIT;
static bool disc_completed;
static bool disc_cancelled;
static bool mtu_exchanged;
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset,
@@ -81,8 +65,6 @@ static struct stream_pair_state {
} stream_pairs[MAX(ARRAY_SIZE(sinks), ARRAY_SIZE(sources))];
static size_t stream_pair_count;
static int ext_scan_start(void);
static void reset_stream_pair_state(void)
{
stream_pair_count = 0;
@@ -1049,49 +1031,6 @@ static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = {
.discover = discover_cb,
};
static int conn_create(ble_addr_t *dst)
{
struct ble_gap_conn_params params = {0};
uint8_t own_addr_type = 0;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
err = ble_gap_disc_cancel();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return err;
}
disc_cancelled = true;
params.scan_itvl = INIT_SCAN_INTERVAL;
params.scan_window = INIT_SCAN_WINDOW;
params.itvl_min = CONN_INTERVAL;
params.itvl_max = CONN_INTERVAL;
params.latency = CONN_LATENCY;
params.supervision_timeout = CONN_TIMEOUT;
params.max_ce_len = CONN_MAX_CE_LEN;
params.min_ce_len = CONN_MIN_CE_LEN;
return ble_gap_connect(own_addr_type, dst, CONN_DURATION, &params,
example_audio_gap_event_cb, NULL);
}
static int pairing_start(uint16_t conn_handle)
{
return ble_gap_security_initiate(conn_handle);
}
static int exchange_mtu(uint16_t conn_handle)
{
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
}
struct unicast_server_adv_data {
bool target_matched;
bool ascs_found;
@@ -1157,7 +1096,6 @@ static bool scan_data_cb(uint8_t type, const uint8_t *data,
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
{
struct unicast_server_adv_data adv = {0};
ble_addr_t dst = {0};
int err;
if (default_conn_handle != CONN_HANDLE_INIT) {
@@ -1178,17 +1116,21 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Unicast server found: type %u contexts 0x%08x",
adv.announcement_type, adv.audio_contexts);
dst.type = event->ext_scan_recv.addr.type;
memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val));
/* Stop scanning before connect — NimBLE rejects ble_gap_connect while
* a discovery procedure is running; Bluedroid prefers the same ordering
* even though it accepts both. On failure, restart scanning so we don't
* stall in a no-scan no-conn state. */
err = ext_scan_stop();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return;
}
err = conn_create(&dst);
err = conn_create(event->ext_scan_recv.addr.type,
event->ext_scan_recv.addr.val);
if (err) {
ESP_LOGE(TAG, "Failed to create conn, err %d", err);
if (disc_cancelled) {
disc_cancelled = false;
ext_scan_start();
}
ext_scan_start();
}
}
@@ -1198,14 +1140,16 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
if (event->acl_connect.status) {
ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status);
/* ext_scan_recv stopped scanning before conn_create. acl_disconnect
* (which restarts scan) only fires on an established connection, so
* we must resume here to avoid a no-scan / no-conn dead state. */
ext_scan_start();
return;
}
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, event->acl_connect.role,
event->acl_connect.dst.val[5], event->acl_connect.dst.val[4],
event->acl_connect.dst.val[3], event->acl_connect.dst.val[2],
event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
default_conn_handle = event->acl_connect.conn_handle;
@@ -1223,7 +1167,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
default_conn_handle = CONN_HANDLE_INIT;
disc_completed = false;
disc_cancelled = false;
mtu_exchanged = false;
/* Delete the group before reset_stream_state() memsets the streams:
@@ -1240,13 +1183,13 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
ext_scan_start();
}
static void security_change(esp_ble_iso_gap_app_event_t *event)
static void security_change(esp_ble_audio_gap_app_event_t *event)
{
int err;
if (event->security_change.status) {
example_audio_security_failed_recover(TAG, event->security_change.conn_handle,
event->security_change.status);
security_failed_recover(event->security_change.conn_handle,
event->security_change.status);
return;
}
@@ -1349,33 +1292,6 @@ static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
}
}
static int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for unicast server...");
return 0;
}
void app_main(void)
{
esp_ble_audio_init_info_t info = {
@@ -1398,6 +1314,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -1428,7 +1350,7 @@ void app_main(void)
return;
}
err = ble_svc_gap_device_name_set("BAP Unicast Client");
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
@@ -0,0 +1,152 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_common_api.h"
#include "central.h"
/* Init/conn parameters are shared with the bluedroid wrapper via central.h.
* CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */
#define CONN_DURATION 10000 /* 10s */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_EXT_DISC:
case BLE_GAP_EVENT_CONNECT:
case BLE_GAP_EVENT_DISCONNECT:
case BLE_GAP_EVENT_ENC_CHANGE:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_MTU:
case BLE_GAP_EVENT_NOTIFY_RX:
case BLE_GAP_EVENT_NOTIFY_TX:
case BLE_GAP_EVENT_SUBSCRIBE:
esp_ble_audio_gatt_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for unicast server...");
return 0;
}
int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
int conn_create(uint8_t addr_type, const uint8_t addr[6])
{
struct ble_gap_conn_params params = {0};
uint8_t own_addr_type = 0;
ble_addr_t dst = {0};
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
params.scan_itvl = INIT_SCAN_INTERVAL;
params.scan_window = INIT_SCAN_WINDOW;
params.itvl_min = CONN_INTERVAL;
params.itvl_max = CONN_INTERVAL;
params.latency = CONN_LATENCY;
params.supervision_timeout = CONN_TIMEOUT;
params.max_ce_len = CONN_MAX_CE_LEN;
params.min_ce_len = CONN_MIN_CE_LEN;
dst.type = addr_type;
memcpy(dst.val, addr, sizeof(dst.val));
return ble_gap_connect(own_addr_type, &dst, CONN_DURATION,
&params, gap_event_cb, NULL);
}
int pairing_start(uint16_t conn_handle)
{
return ble_gap_security_initiate(conn_handle);
}
int exchange_mtu(uint16_t conn_handle)
{
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
}
void security_failed_recover(uint16_t conn_handle, uint8_t status)
{
struct ble_gap_conn_desc desc = {0};
int rc;
/* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical
* cause is asymmetric bond state — drop the stale entry and tear down
* the link so the next reconnect runs fresh pairing. */
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
rc = ble_gap_conn_find(conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}
@@ -3,18 +3,21 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_GATTC_NOTIF_REG_MAX=20
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
CONFIG_BT_BAP_UNICAST_CLIENT=y
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,12 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -7,7 +7,7 @@
## Overview
This example implements a **BAP Unicast Server** on top of the NimBLE host stack. It registers PACS sink and source capabilities (LC3, any frequency, 10 ms frame duration, 2-channel support, 40-120 octets per frame, up to 2 frames per SDU) and a hard-coded location/context set (front-left + front-right; UNSPECIFIED | CONVERSATIONAL | MEDIA), then starts connectable extended advertising on 1M/2M PHY whose payload carries the Flags AD, the ASCS UUID, the targeted unicast announcement service data with sink/source contexts, and the device name `BAP Unicast Server`.
This example implements a **BAP Unicast Server** on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay). It registers PACS sink and source capabilities (LC3, any frequency, 10 ms frame duration, 2-channel support, 40-120 octets per frame, up to 2 frames per SDU) and a hard-coded location/context set (front-left + front-right; UNSPECIFIED | CONVERSATIONAL | MEDIA), then starts connectable extended advertising on 1M/2M PHY whose payload carries the Flags AD, the ASCS UUID, the targeted unicast announcement service data with sink/source contexts, and the device name `BAP Unicast Server`.
The unicast server callbacks (`config`, `reconfig`, `qos`, `enable`, `start`, `metadata`, `disable`, `stop`, `release`) react to ASCS PDUs from a peer client. `config_cb` allocates a free `sink_streams[]`/`source_streams[]` slot and returns a fixed `qos_pref` (unframed PDUs, 2M PHY preference, RTN 2, 10 ms max latency, 20-40 ms presentation delay). `enable_cb` decodes the codec config to sample rate, frame duration, and frame-blocks-per-SDU before accepting the request. Reconfig is rejected unconditionally.
@@ -34,11 +34,24 @@ This example inherits a Just-Works pairing model (LE Secure Connections, no MITM
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -46,7 +59,7 @@ idf.py -p PORT flash monitor
1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, then calls `esp_ble_audio_pacs_register` with `snk_pac/snk_loc/src_pac/src_loc` all true.
2. It registers the unicast server (`esp_ble_audio_bap_unicast_server_register` with `CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT` / `CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT`) and its callback table, registers sink and source PACS capabilities, and attaches `stream_ops` to every preallocated stream slot.
3. PACS location is set per direction (`SINK_LOCATION` / `SOURCE_LOCATION`) and supported plus available contexts are pushed for sink and source.
4. `esp_ble_audio_common_start` and `ble_svc_gap_device_name_set("BAP Unicast Server")` run, then `ext_adv_start` builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name; it configures and starts the extended advertising set on `ADV_HANDLE` 0.
4. `esp_ble_audio_common_start` runs, then `adv_init("BAP Unicast Server")` performs host-specific setup (Bluedroid: create completion sem + register GAP cb + `esp_ble_gap_set_device_name`; NimBLE: `ble_svc_gap_device_name_set`). `ext_adv_start` then builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name in host-agnostic bytes, and calls `adv_start(ext_adv_data, sizeof(ext_adv_data))`. The host-specific implementation in `main/bluedroid/adv.c` or `main/nimble/adv.c` configures and starts the connectable extended advertising set on `ADV_HANDLE` 0.
5. On `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` the connection is logged; after the peer client triggers MTU exchange the server logs the new MTU and calls `esp_ble_audio_gattc_disc_start`.
6. ASCS callbacks fire as the client drives the state machine: `config_cb` allocates a stream and returns the QoS preference, `qos_cb` records `max_sdu`, `enable_cb` validates the codec, `metadata_cb` parses metadata, and `disable`/`stop`/`release` log their requests.
7. `stream_enabled_cb` auto-starts sink streams server-side; `stream_started_cb` resets TX or RX counters depending on direction; `stream_recv_cb` updates per-stream RX metrics on every ISO SDU.
@@ -67,6 +80,7 @@ Client connects:
```
I (xxx) BAP_USR: Connected: handle ... role ... peer ...
I (xxx) BAP_USR: Security: handle ... level ... bonded ...
I (xxx) BAP_USR: MTU updated: handle ... mtu ...
I (xxx) BAP_USR: Service discovery started: handle ...
I (xxx) BAP_USR: Service discovery complete: handle ... status ...
@@ -97,13 +111,26 @@ I (xxx) BAP_USR: [SNK #...] Release request
I (xxx) BAP_USR: Disconnected: handle ... reason 0x...
```
**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave:
```
W (xxx) BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8
W (xxx) BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1
I (xxx) BAP_USR: [SNK #0] ISO disconnected, reason 0x08
I (xxx) BAP_USR: [SNK #0] Stream disabled
E (xxx) BT_APPL: Unknown connection ID: 3 fail sending notification
I (xxx) BAP_USR: [SNK #0] Stream stopped, reason 0x08
```
`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side.
## Peer Pairing
Run [unicast_client](../unicast_client/) on a second board.
1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name.
2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`.
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`.
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and `Security:`.
4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server.
5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation.
6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics.
@@ -1,4 +1,11 @@
set(srcs "main.c")
idf_component_register(SRCS "${srcs}"
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/peripheral.c")
else()
list(APPEND srcs "nimble/peripheral.c")
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_ble_audio_common_api.h"
#include "peripheral.h"
static SemaphoreHandle_t adv_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t adv_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE,
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.channel_map = ADV_CHNL_ALL,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.primary_phy = ESP_BLE_GAP_PHY_1M,
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_2M,
.sid = ADV_SID,
.scan_req_notif = false,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.tx_power = ADV_TX_POWER,
};
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
[0] = { ADV_HANDLE, 0, 0 },
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
adv_op_status = param->ext_adv_start.status;
xSemaphoreGive(adv_sem);
break;
/* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer
* (central) initiates; we accept the security request and confirm the
* numeric comparison. */
case ESP_GAP_BLE_SEC_REQ_EVT:
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
break;
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
adv_sem = xSemaphoreCreateBinary();
if (adv_sem == NULL) {
ESP_LOGE(TAG, "Failed to create adv semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(adv_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len)
{
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return ESP_OK;
}
@@ -13,11 +13,6 @@
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_lc3_defs.h"
#include "esp_ble_audio_bap_api.h"
@@ -26,18 +21,7 @@
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define TAG "BAP_USR"
#define LOCAL_DEVICE_NAME "BAP Unicast Server"
#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1)
#define ADV_HANDLE 0x00
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
#include "peripheral.h"
#define SINK_LOCATION (ESP_BLE_AUDIO_LOCATION_FRONT_LEFT | \
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT)
@@ -536,7 +520,7 @@ static int set_pacs_available_contexts(void)
return 0;
}
static void build_adv_data(void)
static void build_ext_adv_data(void)
{
size_t idx = 0;
@@ -569,60 +553,6 @@ static void build_adv_data(void)
memcpy(&ext_adv_data[idx], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN);
}
static void ext_adv_start(void)
{
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
build_adv_data();
ext_params.connectable = 1;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = ADV_ADDRESS;
ext_params.primary_phy = ADV_PRIMARY_PHY;
ext_params.secondary_phy = ADV_SECONDARY_PHY;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = ADV_INTERVAL;
ext_params.itvl_max = ADV_INTERVAL;
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return;
}
data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return;
}
err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data));
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
}
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
{
if (event->acl_connect.status) {
@@ -632,9 +562,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, event->acl_connect.role,
event->acl_connect.dst.val[5], event->acl_connect.dst.val[4],
event->acl_connect.dst.val[3], event->acl_connect.dst.val[2],
event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
}
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
@@ -642,7 +570,22 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
ext_adv_start();
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
static void security_change(esp_ble_audio_gap_app_event_t *event)
{
if (event->security_change.status) {
ESP_LOGE(TAG, "Security failed: handle %u status %d",
event->security_change.conn_handle,
event->security_change.status);
return;
}
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
}
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
@@ -654,6 +597,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
acl_disconnect(event);
break;
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
security_change(event);
break;
default:
break;
}
@@ -735,6 +681,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -800,11 +752,13 @@ void app_main(void)
return;
}
err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
}
ext_adv_start();
build_ext_adv_data();
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
@@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_common_api.h"
#include "peripheral.h"
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
case BLE_GAP_EVENT_DISCONNECT:
case BLE_GAP_EVENT_ENC_CHANGE:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_MTU:
case BLE_GAP_EVENT_NOTIFY_RX:
case BLE_GAP_EVENT_NOTIFY_TX:
case BLE_GAP_EVENT_SUBSCRIBE:
esp_ble_audio_gatt_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
/* Peer wants to re-pair on top of an existing bond — delete our stale
* bond entry and tell NimBLE to retry pairing. */
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len)
{
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data;
int err;
ext_params.connectable = 1;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(ext_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return -1;
}
err = os_mbuf_append(data, ext_data, ext_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
/* ble_gap_ext_adv_set_data takes ownership of `data` and frees it via
* its 'done' label on both success and failure paths — do NOT free
* here (double-free). API header doesn't document this contract. */
return err;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return err;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "ble_audio_example_utils.h"
#define TAG "BAP_USR"
#define LOCAL_DEVICE_NAME "BAP Unicast Server"
#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1)
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
int app_host_init(void);
int set_device_name(void);
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len);
@@ -3,14 +3,15 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_GATTC_NOTIF_REG_MAX=20
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -24,4 +25,6 @@ CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,12 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -7,7 +7,7 @@
## Overview
This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the NimBLE host stack with ISO and LE Audio support. It can be built with one or both roles selected at build time: a **CAP Acceptor / BAP Unicast Server**, and / or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). Dual-role builds are supported (BAP spec C.2). PACS is registered in all configurations with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast).
This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) with ISO and LE Audio support. It can be built with one or both roles selected at build time: a **CAP Acceptor / BAP Unicast Server**, and / or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). Dual-role builds are supported (BAP spec C.2). PACS is registered in all configurations with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast).
In **unicast** mode (`cap_acceptor_unicast.c`) the acceptor registers BAP unicast-server callbacks (config / reconfig / qos / enable / start / metadata / disable / stop / release) and CAP stream ops, and on enable of a sink ASE automatically issues `bap_stream_start` (Receiver Start Ready). When the source ASE starts, an internal TX scheduler begins sending dummy SDUs. In **broadcast** mode (`cap_acceptor_broadcast.c`) the acceptor registers BAP scan-delegator and broadcast-sink callbacks, drives PA sync (preferring PAST when the Assistant supports it) on request, receives BASE / BIGInfo / broadcast code, then calls `esp_ble_audio_bap_broadcast_sink_sync` on the BIS bitmap selected by the Assistant via BASS Modify Source — up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` BIS streams. The Broadcast Sink object is created on PA sync and deleted on PA loss; Assistant pause / resume keep the same sink. See **Broadcast Mode Internals** below for sequence, gate flags and sink lifecycle. Optional **self-scan** lets the acceptor scan for a broadcast source named `CAP Broadcast Source` and use a hardcoded broadcast code `1234` instead of waiting for a Broadcast Assistant.
@@ -41,11 +41,24 @@ Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -195,6 +208,7 @@ TAG: `CAP_ACC`.
```
I (xxx) CAP_ACC: Advertising started (handle 0)
I (xxx) CAP_ACC: Connected: handle ... role ... peer ...
I (xxx) CAP_ACC: Security: handle ... level ... bonded ...
I (xxx) CAP_ACC: MTU updated: handle ... mtu ...
I (xxx) CAP_ACC: Service discovery started: handle ...
I (xxx) CAP_ACC: Service discovery complete: handle ...
@@ -227,11 +241,25 @@ I (xxx) CAP_ACC: Disconnected: handle ... reason 0x...
I (xxx) CAP_ACC: Advertising started (handle 0)
```
**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave:
```
W (xxx) BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8
W (xxx) BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1
I (xxx) CAP_ACC: [SNK #0] ISO disconnected, reason 0x08
I (xxx) CAP_ACC: [SNK #0] Stream disabled
E (xxx) BT_APPL: Unknown connection ID: 3 fail sending notification
I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x08
```
`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side.
### Broadcast mode (Broadcast Assistant)
```
I (xxx) CAP_ACC: Advertising started (handle 0)
I (xxx) CAP_ACC: Connected: handle ... role ... peer ...
I (xxx) CAP_ACC: Security: handle ... level ... bonded ...
I (xxx) CAP_ACC: Receive state updated, pa_sync 0x... encrypt 0x...
I (xxx) CAP_ACC: Received request to sync to PA (PAST not available): ...
I (xxx) CAP_ACC: Syncing without PAST
@@ -1,12 +1,24 @@
set(srcs "main.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/peripheral.c")
else()
list(APPEND srcs "nimble/peripheral.c")
endif()
if(CONFIG_EXAMPLE_UNICAST)
list(APPEND srcs "cap_acceptor_unicast.c")
endif()
if(CONFIG_EXAMPLE_BROADCAST)
list(APPEND srcs "cap_acceptor_broadcast.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/scan.c")
else()
list(APPEND srcs "nimble/scan.c")
endif()
endif()
idf_component_register(SRCS "${srcs}"
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,148 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_ble_audio_common_api.h"
#include "cap_acceptor.h"
/* Shared with scan.c — single host_sem because init is serialized and only
* peripheral.c owns the GAP cb registration. */
SemaphoreHandle_t cap_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
esp_bt_status_t cap_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, cap_sem, portMAX_DELAY, cap_op_status)
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE,
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.channel_map = ADV_CHNL_ALL,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.primary_phy = ESP_BLE_GAP_PHY_1M,
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_2M,
.sid = ADV_SID,
.scan_req_notif = false,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.tx_power = ADV_TX_POWER,
};
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
[0] = { ADV_HANDLE, 0, 0 },
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
/* Adv-side completion */
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
cap_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(cap_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
cap_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(cap_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
cap_op_status = param->ext_adv_start.status;
xSemaphoreGive(cap_sem);
break;
case ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT:
cap_op_status = param->ext_adv_stop.status;
xSemaphoreGive(cap_sem);
break;
/* Scan-side completion */
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
cap_op_status = param->set_ext_scan_params.status;
xSemaphoreGive(cap_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
cap_op_status = param->ext_scan_start.status;
xSemaphoreGive(cap_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
cap_op_status = param->ext_scan_stop.status;
xSemaphoreGive(cap_sem);
break;
/* PAST params setup — pa_sync_with_past / pa_past_cancel wait via WAIT_API.
* CREATE_SYNC / SYNC_TERMINATE complete events are intentionally
* omitted — those calls are fire-and-forget (sync est/lost is air-
* dependent and surfaces via PA_SYNC_ESTAB / PA_SYNC_LOST). Giving
* the sem here would leave it stale-available for the next WAIT_API. */
case ESP_GAP_BLE_SET_PAST_PARAMS_COMPLETE_EVT:
cap_op_status = param->set_past_params.status;
xSemaphoreGive(cap_sem);
break;
/* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer
* (central) initiates; we accept the security request and confirm the
* numeric comparison. */
case ESP_GAP_BLE_SEC_REQ_EVT:
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
break;
/* AUTH_CMPL has no BTA channel — app must forward it. PA sync / ext adv
* events: already forwarded by adapter's BTA path, don't re-post here. */
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
cap_sem = xSemaphoreCreateBinary();
if (cap_sem == NULL) {
ESP_LOGE(TAG, "Failed to create host semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(cap_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len)
{
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "cap_acceptor.h"
/* Owned by peripheral.c (single host_sem, serialized init). */
extern SemaphoreHandle_t cap_sem;
extern esp_bt_status_t cap_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, cap_sem, portMAX_DELAY, cap_op_status)
#if CONFIG_EXAMPLE_SCAN_SELF
static esp_ble_ext_scan_params_t ext_scan_params = {
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
.uncoded_cfg = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.scan_interval = SCAN_INTERVAL,
.scan_window = SCAN_WINDOW,
},
};
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
#if CONFIG_EXAMPLE_SCAN_SELF
int ext_scan_start(void)
{
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
ESP_LOGI(TAG, "Scanning for broadcast source...");
return 0;
}
int ext_scan_stop(void)
{
WAIT_API(esp_ble_gap_stop_ext_scan());
return 0;
}
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
{
esp_ble_gap_periodic_adv_sync_params_t params = {
.filter_policy = 0,
.sid = sid,
.addr_type = addr_type,
.skip = PA_SYNC_SKIP,
.sync_timeout = PA_SYNC_TIMEOUT,
};
memcpy(params.addr, addr, sizeof(params.addr));
/* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is
* air-dependent and surfaces asynchronously. */
return esp_ble_gap_periodic_adv_create_sync(&params);
}
int pa_sync_with_past(uint16_t conn_handle, uint8_t addr_type, const uint8_t addr[6])
{
/* PAST receive params. Mode 0x02 = sync + report events, no dedup.
* mode>=1 also implicitly enables the periodic adv report stream after the
* controller auto-syncs via PAST — no separate recv_enable needed. */
esp_ble_gap_past_params_t params = {
.mode = ESP_BLE_GAP_PAST_MODE_DUP_FILTER_DISABLED,
.skip = PA_SYNC_SKIP,
.sync_timeout = PA_SYNC_TIMEOUT,
.cte_type = 0,
};
/* Use Assistant's BD addr (peer.dst from ACL_CONNECT), not the broadcast
* source addr from `addr` — BTM dispatches the HCI cmd over the
* Assistant's ACL. */
esp_bd_addr_t peer_addr;
(void)conn_handle;
(void)addr_type;
(void)addr;
memcpy(peer_addr, peer.dst, sizeof(peer_addr));
WAIT_API(esp_ble_gap_set_periodic_adv_sync_trans_params(peer_addr, &params));
/* Actual sync handle is delivered later via PA_SYNC_PAST app event. */
return 0;
}
int pa_past_cancel(uint16_t conn_handle)
{
/* mode=0 → controller no longer auto-syncs incoming PAST from Assistant.
* Like pa_sync_with_past, addressed by Assistant's BD addr (peer.dst). */
esp_ble_gap_past_params_t params = {
.mode = ESP_BLE_GAP_PAST_MODE_NO_SYNC_EVT,
.skip = PA_SYNC_SKIP,
.sync_timeout = PA_SYNC_TIMEOUT,
.cte_type = 0,
};
esp_bd_addr_t peer_addr;
(void)conn_handle;
memcpy(peer_addr, peer.dst, sizeof(peer_addr));
WAIT_API(esp_ble_gap_set_periodic_adv_sync_trans_params(peer_addr, &params));
return 0;
}
int pa_sync_terminate(uint16_t sync_handle)
{
/* iso bluedroid/gap.c synthesizes a PA_SYNC_LOST event on
* BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT, matching
* NimBLE host behavior — the app's pa_sync_lost handler runs
* broadcast_pa_lost automatically. */
return esp_ble_gap_periodic_adv_sync_terminate(sync_handle);
}
@@ -5,13 +5,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "sdkconfig.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_lc3_defs.h"
#include "esp_ble_audio_cap_api.h"
#include "esp_ble_audio_pacs_api.h"
@@ -23,6 +22,19 @@
#define CONN_HANDLE_INIT 0xFFFF
#define LOCAL_DEVICE_NAME "CAP Acceptor"
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
#define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA | \
@@ -37,8 +49,32 @@ struct peer_config {
esp_ble_audio_cap_stream_t source_stream;
esp_ble_audio_cap_stream_t sink_stream;
uint16_t conn_handle;
/* Assistant's BD addr — used by Bluedroid pa_sync_with_past to set per-peer
* PAST receive params. Populated at ACL_CONNECT, cleared at ACL_DISCONNECT. */
uint8_t dst[6];
};
extern struct peer_config peer;
int app_host_init(void);
int set_device_name(void);
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len);
#if CONFIG_EXAMPLE_SCAN_SELF
int ext_scan_start(void);
int ext_scan_stop(void);
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid);
int pa_sync_with_past(uint16_t conn_handle, uint8_t addr_type, const uint8_t addr[6]);
int pa_past_cancel(uint16_t conn_handle);
int pa_sync_terminate(uint16_t sync_handle);
int cap_acceptor_unicast_init(struct peer_config *peer);
int cap_acceptor_broadcast_init(void);
@@ -58,5 +94,5 @@ void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event);
void broadcast_pa_sync_failed(esp_ble_audio_gap_app_event_t *event);
void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event);
void broadcast_pa_lost(uint16_t sync_handle);
#endif /* CONFIG_EXAMPLE_BROADCAST */
@@ -9,7 +9,6 @@
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>
#include <errno.h>
#include "cap_acceptor.h"
@@ -18,13 +17,8 @@
#define TARGET_DEVICE_NAME "CAP Broadcast Source"
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
#define TARGET_BROADCAST_CODE "1234"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
#define PA_SYNC_HANDLE_INIT UINT16_MAX
enum broadcast_flag {
@@ -78,101 +72,9 @@ static struct broadcast_sink {
.sync_handle = PA_SYNC_HANDLE_INIT,
};
static example_audio_rx_metrics_t rx_metrics;
static int pa_sync_without_past(const bt_addr_le_t *addr, uint8_t adv_sid)
{
struct ble_gap_periodic_sync_params params = {0};
ble_addr_t sync_addr = {0};
int err;
sync_addr.type = addr->type;
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
err = ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to create PA sync without past, err %d", err);
}
return err;
}
static int pa_sync_with_past(uint16_t conn_handle,
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
{
struct ble_gap_periodic_sync_params params = {0};
int err;
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
err = ble_gap_periodic_adv_sync_receive(conn_handle, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to enable PAST receive, err %d", err);
return err;
}
/* Tell the Assistant we are waiting for PAST so it sends
* HCI_LE_Periodic_Advertising_Sync_Transfer.
*/
err = esp_ble_audio_bap_scan_delegator_set_pa_state(recv_state->src_id,
ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ);
if (err) {
ESP_LOGE(TAG, "Failed to set PA state to INFO_REQ, err %d", err);
}
return err;
}
static int pa_sync_terminate(void)
{
int err;
err = ble_gap_periodic_adv_sync_terminate(broadcast_sink.sync_handle);
if (err) {
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
}
return err;
}
static example_audio_rx_metrics_t rx_metrics[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
#if CONFIG_EXAMPLE_SCAN_SELF
static int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for broadcast source...");
return 0;
}
static int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
int check_start_scan(void)
{
int err;
@@ -243,7 +145,6 @@ static bool data_cb(uint8_t type, const uint8_t *data,
void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
{
struct scan_recv_data sr = {0};
bt_addr_le_t addr;
int err;
/* Periodic advertising interval. 0 if no periodic advertising. */
@@ -267,10 +168,9 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
broadcast_sink.requested_bis_sync = ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF;
broadcast_sink.broadcast_id = sr.broadcast_id;
addr.type = event->ext_scan_recv.addr.type;
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
err = pa_sync_without_past(&addr, event->ext_scan_recv.sid);
err = pa_sync_create(event->ext_scan_recv.addr.type,
event->ext_scan_recv.addr.val,
event->ext_scan_recv.sid);
if (err) {
return;
}
@@ -417,9 +317,11 @@ static uint8_t broadcast_stream_idx(const esp_ble_audio_bap_stream_t *stream)
static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "[SNK #%u] Stream started", broadcast_stream_idx(stream));
uint8_t idx = broadcast_stream_idx(stream);
example_audio_rx_metrics_reset(&rx_metrics);
ESP_LOGI(TAG, "[SNK #%u] Stream started", idx);
example_audio_rx_metrics_reset(&rx_metrics[idx]);
broadcast_sink.active_streams++;
flag_clear(FLAG_BROADCAST_SYNCING);
@@ -480,11 +382,12 @@ static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
const esp_ble_iso_recv_info_t *info,
const uint8_t *data, uint16_t len)
{
uint8_t idx = broadcast_stream_idx(stream);
char obj_name[10];
rx_metrics.last_sdu_len = len;
snprintf(obj_name, sizeof(obj_name), "SNK #%u", broadcast_stream_idx(stream));
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, obj_name);
rx_metrics[idx].last_sdu_len = len;
snprintf(obj_name, sizeof(obj_name), "SNK #%u", idx);
example_audio_rx_metrics_on_recv(info, &rx_metrics[idx], TAG, obj_name);
}
static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
@@ -538,7 +441,7 @@ static void recv_state_updated_cb(esp_ble_conn_t *conn,
recv_state->pa_sync_state, recv_state->encrypt_state);
for (uint8_t i = 0; i < recv_state->num_subgroups; i++) {
ESP_LOGI(TAG, "subgroup %d bis_sync: 0x%08x", i, recv_state->subgroups[i].bis_sync);
ESP_LOGI(TAG, "subgroup %d bis_sync 0x%08x", i, recv_state->subgroups[i].bis_sync);
}
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) {
@@ -566,14 +469,29 @@ static int pa_sync_req_cb(esp_ble_conn_t *conn,
}
if (past_available) {
err = pa_sync_with_past(conn->handle, recv_state);
err = pa_sync_with_past(conn->handle,
recv_state->addr.type, recv_state->addr.a.val);
if (err) {
return -EIO;
}
/* Tell the Assistant we are waiting for PAST so it sends
* HCI_LE_Periodic_Advertising_Sync_Transfer. */
err = esp_ble_audio_bap_scan_delegator_set_pa_state(recv_state->src_id,
ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ);
if (err) {
ESP_LOGE(TAG, "Failed to set PA state to INFO_REQ, err %d", err);
/* Disarm the PAST receive we just enabled — Assistant was never
* notified, so any late-arriving PAST would land on stale state
* (broadcast_id=0, FLAG_PA_SYNCING unset). */
pa_past_cancel(conn->handle);
return -EIO;
}
ESP_LOGI(TAG, "Waiting for PAST...");
} else {
err = pa_sync_without_past(&recv_state->addr, recv_state->adv_sid);
err = pa_sync_create(recv_state->addr.type, recv_state->addr.a.val,
recv_state->adv_sid);
if (err) {
return err;
}
@@ -596,8 +514,17 @@ static int pa_sync_term_req_cb(esp_ble_conn_t *conn,
broadcast_sink.recv_state = recv_state;
err = pa_sync_terminate();
/* Nothing to terminate if PAST/PA-sync never landed (e.g., PAST setup failed
* earlier). Issuing the HCI terminate with the sentinel handle gets 0x12
* (Invalid HCI Command Parameters) back from the controller. */
if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT) {
ESP_LOGI(TAG, "PA sync never established, skip terminate");
return 0;
}
err = pa_sync_terminate(broadcast_sink.sync_handle);
if (err) {
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
return -EIO;
}
@@ -783,11 +710,11 @@ void broadcast_pa_sync_failed(esp_ble_audio_gap_app_event_t *event)
}
}
void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
void broadcast_pa_lost(uint16_t sync_handle)
{
esp_err_t err;
if (event->pa_sync_lost.sync_handle != broadcast_sink.sync_handle) {
if (sync_handle != broadcast_sink.sync_handle) {
return;
}
@@ -836,6 +763,14 @@ void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
flag_clear(FLAG_BASE_RECEIVED);
flag_clear(FLAG_BROADCAST_SYNCABLE);
flag_clear(FLAG_BROADCAST_CODE_REQUIRED);
#if CONFIG_EXAMPLE_SCAN_SELF
/* Clear cached recv_state so broadcast_scan_recv's gate can drive
* a fresh PA sync. */
broadcast_sink.recv_state = NULL;
check_start_scan();
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
}
int cap_acceptor_broadcast_init(void)
@@ -6,25 +6,11 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "cap_acceptor.h"
#define ADV_HANDLE 0x00
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
static uint8_t codec_data[] =
ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA(
ESP_BLE_AUDIO_CODEC_CAP_FREQ_ANY, /* Sampling frequency Any */
@@ -49,7 +35,7 @@ static esp_ble_audio_pacs_cap_t source_cap = {
.codec_cap = &codec_cap,
};
static struct peer_config peer = {
struct peer_config peer = {
.conn_handle = CONN_HANDLE_INIT,
};
@@ -98,58 +84,6 @@ static uint8_t ext_adv_data[] = {
0x0d, EXAMPLE_AD_TYPE_NAME_COMPLETE, 'c', 'a', 'p', '_', 'a', 'c', 'c', 'e', 'p', 't', 'o', 'r',
};
static void ext_adv_start(void)
{
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
ext_params.connectable = 1;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = ADV_ADDRESS;
ext_params.primary_phy = ADV_PRIMARY_PHY;
ext_params.secondary_phy = ADV_SECONDARY_PHY;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = ADV_INTERVAL;
ext_params.itvl_max = ADV_INTERVAL;
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return;
}
data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return;
}
err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data));
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
}
esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
{
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
@@ -265,7 +199,7 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x",
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
broadcast_pa_lost(event);
broadcast_pa_lost(event->pa_sync_lost.sync_handle);
}
#endif /* CONFIG_EXAMPLE_BROADCAST */
@@ -278,11 +212,10 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, event->acl_connect.role,
event->acl_connect.dst.val[5], event->acl_connect.dst.val[4],
event->acl_connect.dst.val[3], event->acl_connect.dst.val[2],
event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
peer.conn_handle = event->acl_connect.conn_handle;
memcpy(peer.dst, event->acl_connect.dst.val, sizeof(peer.dst));
}
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
@@ -291,8 +224,24 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
peer.conn_handle = CONN_HANDLE_INIT;
memset(peer.dst, 0, sizeof(peer.dst));
ext_adv_start();
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
static void security_change(esp_ble_audio_gap_app_event_t *event)
{
if (event->security_change.status) {
ESP_LOGE(TAG, "Security failed: handle %u status %d",
event->security_change.conn_handle,
event->security_change.status);
return;
}
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
}
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
@@ -318,6 +267,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
acl_disconnect(event);
break;
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
security_change(event);
break;
default:
break;
}
@@ -392,6 +344,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -423,7 +381,7 @@ void app_main(void)
return;
}
err = ble_svc_gap_device_name_set("CAP Acceptor");
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
@@ -439,5 +397,5 @@ void app_main(void)
/* Advertising will be used by Unicast Server and
* Broadcast Sink when self-scanning is disabled.
*/
ext_adv_start();
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
@@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "os/os_mbuf.h"
#include "esp_ble_audio_common_api.h"
#include "cap_acceptor.h"
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
case BLE_GAP_EVENT_DISCONNECT:
case BLE_GAP_EVENT_ENC_CHANGE:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_MTU:
case BLE_GAP_EVENT_NOTIFY_RX:
case BLE_GAP_EVENT_NOTIFY_TX:
case BLE_GAP_EVENT_SUBSCRIBE:
esp_ble_audio_gatt_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len)
{
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
ext_params.connectable = 1;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(ext_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return -1;
}
err = os_mbuf_append(data, ext_data, ext_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return err;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return err;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "esp_ble_audio_common_api.h"
#include "cap_acceptor.h"
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_EXT_DISC:
case BLE_GAP_EVENT_PERIODIC_SYNC:
case BLE_GAP_EVENT_PERIODIC_REPORT:
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
case BLE_GAP_EVENT_PERIODIC_TRANSFER:
case BLE_GAP_EVENT_PERIODIC_TRANSFER_V2:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
default:
break;
}
return 0;
}
#if CONFIG_EXAMPLE_SCAN_SELF
int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for broadcast source...");
return 0;
}
int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
{
struct ble_gap_periodic_sync_params params = {0};
ble_addr_t sync_addr = {0};
int err;
sync_addr.type = addr_type;
memcpy(sync_addr.val, addr, sizeof(sync_addr.val));
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
err = ble_gap_periodic_adv_sync_create(&sync_addr, sid, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
}
return err;
}
int pa_sync_with_past(uint16_t conn_handle, uint8_t addr_type, const uint8_t addr[6])
{
struct ble_gap_periodic_sync_params params = {0};
int err;
(void)addr_type;
(void)addr;
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
err = ble_gap_periodic_adv_sync_receive(conn_handle, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to enable PAST receive, err %d", err);
}
return err;
}
int pa_past_cancel(uint16_t conn_handle)
{
/* params=NULL → mode=0: controller no longer auto-syncs incoming PAST. */
int err = ble_gap_periodic_adv_sync_receive(conn_handle, NULL, NULL, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to cancel PAST receive, err %d", err);
}
return err;
}
int pa_sync_terminate(uint16_t sync_handle)
{
return ble_gap_periodic_adv_sync_terminate(sync_handle);
}
@@ -3,14 +3,16 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_GATTC_NOTIF_REG_MAX=20
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -26,7 +28,9 @@ CONFIG_BT_PAC_SRC_NOTIFIABLE=y
CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_EXAMPLE_UNICAST=y
@@ -0,0 +1,13 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -7,7 +7,7 @@
## Overview
This example implements the **Common Audio Profile (CAP) Initiator** role on top of the NimBLE host stack with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Unicast Initiator / BAP Unicast Client**, or a **CAP Broadcast Source**. A small TX module (`cap_initiator_tx.c`) drives audio packets onto every started stream using the LE Audio example TX scheduler.
This example implements the **Common Audio Profile (CAP) Initiator** role on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Unicast Initiator / BAP Unicast Client**, or a **CAP Broadcast Source**. A small TX module (`cap_initiator_tx.c`) drives audio packets onto every started stream using the LE Audio example TX scheduler.
In **unicast** mode, the initiator scans for connectable extended advertising that carries CAS service data, connects, initiates pairing, exchanges MTU, performs GATT service discovery, then discovers CAS, sink ASEs and source ASEs, configures the codec with the LC3 16_2_1 unicast preset, creates an ad-hoc unicast group, and starts the streams. In **broadcast** mode, the initiator creates a CAP broadcast source with the LC3 16_2_1 broadcast preset, starts non-connectable extended advertising plus periodic advertising carrying the Broadcast Audio Announcement UUID, a hardcoded broadcast ID (`0x123456`), the local device name (`CAP Broadcast Source`), and the BASE, then starts the broadcast stream.
@@ -37,11 +37,24 @@ Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -3,11 +3,22 @@ set(srcs "cap_initiator_tx.c"
if(CONFIG_EXAMPLE_UNICAST)
list(APPEND srcs "cap_initiator_unicast.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/central.c")
else()
list(APPEND srcs "nimble/central.c")
endif()
endif()
if(CONFIG_EXAMPLE_BROADCAST)
list(APPEND srcs "cap_initiator_broadcast.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/adv.c")
else()
list(APPEND srcs "nimble/adv.c")
endif()
endif()
idf_component_register(SRCS "${srcs}"
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "cap_initiator.h"
static SemaphoreHandle_t adv_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t adv_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED,
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.channel_map = ADV_CHNL_ALL,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.primary_phy = ESP_BLE_GAP_PHY_1M,
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_2M,
.sid = ADV_SID,
.scan_req_notif = false,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.tx_power = ADV_TX_POWER,
};
static esp_ble_gap_periodic_adv_params_t periodic_adv_params = {
.interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
.properties = 0,
};
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
[0] = { ADV_HANDLE, 0, 0 },
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
adv_op_status = param->ext_adv_start.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->peroid_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->period_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT:
adv_op_status = param->period_adv_start.status;
xSemaphoreGive(adv_sem);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
adv_sem = xSemaphoreCreateBinary();
if (adv_sem == NULL) {
ESP_LOGE(TAG, "Failed to create adv semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(adv_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len)
{
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params));
#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false));
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true));
#else
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data));
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE));
#endif
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -0,0 +1,189 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_ble_audio_common_api.h"
#include "cap_initiator.h"
static SemaphoreHandle_t scan_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t scan_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
/* Cached peer address. Bluedroid's pairing and disconnect APIs key off
* bd_addr rather than conn_handle, so we stash the addr at conn_create
* time and reuse it in pairing_start / security_failed_recover. */
static esp_bd_addr_t peer_bda;
static esp_ble_ext_scan_params_t ext_scan_params = {
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
.uncoded_cfg = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.scan_interval = SCAN_INTERVAL,
.scan_window = SCAN_WINDOW,
},
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
scan_op_status = param->set_ext_scan_params.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
scan_op_status = param->ext_scan_start.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
scan_op_status = param->ext_scan_stop.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
break;
/* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT
* is forwarded by adapter's BTA path, don't re-post here. */
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
scan_sem = xSemaphoreCreateBinary();
if (scan_sem == NULL) {
ESP_LOGE(TAG, "Failed to create scan semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(scan_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_scan_start(void)
{
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
ESP_LOGI(TAG, "Scanning for CAP Acceptor...");
return 0;
}
int ext_scan_stop(void)
{
WAIT_API(esp_ble_gap_stop_ext_scan());
return 0;
}
int conn_create(uint8_t addr_type, const uint8_t addr[6])
{
/* Values shared with NimBLE side via cap_initiator.h for behavioral parity.
* Without prefer_ext_connect_params_set, L2CAP falls back to defaults
* and logs "No extend connection parameters set". */
const esp_ble_gap_conn_params_t conn_params = {
.scan_interval = INIT_SCAN_INTERVAL,
.scan_window = INIT_SCAN_WINDOW,
.interval_min = CONN_INTERVAL,
.interval_max = CONN_INTERVAL,
.latency = CONN_LATENCY,
.supervision_timeout = CONN_TIMEOUT,
.min_ce_len = CONN_MIN_CE_LEN,
.max_ce_len = CONN_MAX_CE_LEN,
};
esp_gatt_if_t gattc_if;
esp_err_t err;
memcpy(peer_bda, addr, sizeof(peer_bda));
err = esp_ble_gap_prefer_ext_connect_params_set(
peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err);
return err;
}
/* Use the audio engine's GATTC if — events route back to the engine
* which forwards them to the lib. Registering a separate example GATTC
* app would steer events to a handler the engine never sees.
*
* engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet
* registered; feeding that into aux_open silently no-ops in BTC and
* no acl_connect event ever fires, leaving the caller in a no-scan
* / no-conn dead state. */
gattc_if = esp_ble_audio_bluedroid_get_gattc_if();
if (gattc_if == ESP_GATT_IF_NONE) {
ESP_LOGE(TAG, "GATTC not registered");
return ESP_ERR_INVALID_STATE;
}
return esp_ble_gattc_aux_open(gattc_if, peer_bda,
(esp_ble_addr_type_t)addr_type, true);
}
int pairing_start(uint16_t conn_handle)
{
(void)conn_handle;
return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM);
}
int exchange_mtu(uint16_t conn_handle)
{
(void)conn_handle;
/* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open
* brings up the ACL — the MTU_UPDATED event arrives without an explicit
* kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op
* on bluedroid and a real ble_gattc_exchange_mtu on nimble. */
return 0;
}
void security_failed_recover(uint16_t conn_handle, uint8_t status)
{
(void)conn_handle;
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
esp_ble_remove_bond_device(peer_bda);
esp_ble_gap_disconnect(peer_bda);
}
@@ -5,13 +5,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "sdkconfig.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_lc3_defs.h"
#include "esp_ble_audio_bap_api.h"
#include "esp_ble_audio_cap_api.h"
@@ -23,7 +22,52 @@
#define TAG "CAP_INI"
#define CONN_HANDLE_INIT 0xFFFF
#define CONN_HANDLE_INIT 0xFFFF
#if CONFIG_EXAMPLE_UNICAST
#define LOCAL_DEVICE_NAME "CAP Initiator"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
/* ACL init parameters shared between bluedroid and nimble host wrappers.
* Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */
#define INIT_SCAN_INTERVAL 16 /* 10ms */
#define INIT_SCAN_WINDOW 16 /* 10ms */
#define CONN_INTERVAL 24 /* 30ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 5s */
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_MAX_CE_LEN 0xFFFF
#elif CONFIG_EXAMPLE_BROADCAST
#define LOCAL_DEVICE_NAME "CAP Broadcast Source"
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
#define PER_ADV_INTERVAL_MS 100
#endif
int app_host_init(void);
int set_device_name(void);
#if CONFIG_EXAMPLE_UNICAST
int ext_scan_start(void);
int ext_scan_stop(void);
int conn_create(uint8_t addr_type, const uint8_t addr[6]);
int pairing_start(uint16_t conn_handle);
int exchange_mtu(uint16_t conn_handle);
void security_failed_recover(uint16_t conn_handle, uint8_t status);
#elif CONFIG_EXAMPLE_BROADCAST
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len);
#endif
struct tx_stream {
esp_ble_audio_cap_stream_t *stream;
@@ -6,24 +6,11 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "cap_initiator.h"
#define ADV_HANDLE 0x00
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
#define LOCAL_DEVICE_NAME "CAP Broadcast Source"
#define LOCAL_BROADCAST_ID 0x123456
ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(broadcast_preset_16_2_1,
@@ -146,120 +133,6 @@ static uint8_t *per_adv_data_get(uint8_t *data_len)
return data;
}
static int ext_adv_start(void)
{
struct ble_gap_periodic_adv_params per_params = {0};
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
uint8_t *ext_data = NULL;
uint8_t *per_data = NULL;
uint8_t data_len = 0;
int err;
ext_params.connectable = 0;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = ADV_ADDRESS;
ext_params.primary_phy = ADV_PRIMARY_PHY;
ext_params.secondary_phy = ADV_SECONDARY_PHY;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = ADV_INTERVAL;
ext_params.itvl_max = ADV_INTERVAL;
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
goto end;
}
ext_data = ext_adv_data_get(&data_len);
if (ext_data == NULL) {
err = -ENOMEM;
goto end;
}
data = os_msys_get_pkthdr(data_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
err = -ENOMEM;
goto end;
}
err = os_mbuf_append(data, ext_data, data_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
goto end;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
goto end;
}
per_params.include_tx_power = 0;
per_params.itvl_min = PER_ADV_INTERVAL;
per_params.itvl_max = PER_ADV_INTERVAL;
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
if (err) {
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
goto end;
}
per_data = per_adv_data_get(&data_len);
if (per_data == NULL) {
err = -ENOMEM;
goto end;
}
data = os_msys_get_pkthdr(data_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get per adv mbuf");
err = -ENOMEM;
goto end;
}
err = os_mbuf_append(data, per_data, data_len);
if (err) {
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
os_mbuf_free_chain(data);
goto end;
}
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
goto end;
}
err = ble_gap_periodic_adv_start(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
goto end;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
goto end;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
free(ext_data);
}
if (per_data) {
free(per_data);
}
return err;
}
int cap_initiator_broadcast_start(void)
{
esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params = {
@@ -278,6 +151,11 @@ int cap_initiator_broadcast_start(void)
esp_ble_audio_bap_broadcast_adv_info_t info = {
.adv_handle = ADV_HANDLE,
};
/* BASE (per_adv_data_get) needs broadcast_source to exist — build after create. */
uint8_t *ext_data = NULL;
uint8_t *per_data = NULL;
uint8_t ext_len = 0;
uint8_t per_len = 0;
int err;
ESP_LOGI(TAG, "Creating broadcast source");
@@ -288,7 +166,19 @@ int cap_initiator_broadcast_start(void)
return err;
}
err = ext_adv_start();
ext_data = ext_adv_data_get(&ext_len);
if (ext_data == NULL) {
err = -ENOMEM;
goto end;
}
per_data = per_adv_data_get(&per_len);
if (per_data == NULL) {
err = -ENOMEM;
goto end;
}
err = ext_adv_start(ext_data, ext_len, per_data, per_len);
if (err) {
goto end;
}
@@ -302,15 +192,20 @@ int cap_initiator_broadcast_start(void)
err = esp_ble_audio_cap_initiator_broadcast_audio_start(broadcast_source, ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
goto end;
}
return 0;
end:
err = esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source);
if (ext_data != NULL) {
free(ext_data);
}
if (per_data != NULL) {
free(per_data);
}
if (err) {
ESP_LOGE(TAG, "Failed to delete broadcast source, err %d", err);
esp_err_t e = esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source);
if (e) {
ESP_LOGE(TAG, "Failed to delete broadcast source, err %d", e);
}
}
return err;
}
@@ -9,7 +9,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "cap_initiator.h"
@@ -7,24 +7,10 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "cap_initiator.h"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define INIT_SCAN_INTERVAL 16 /* 10ms */
#define INIT_SCAN_WINDOW 16 /* 10ms */
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
#define CONN_MAX_CE_LEN 0xFFFF
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_DURATION 10000 /* 10s */
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_16_2_1,
ESP_BLE_AUDIO_LOCATION_MONO_AUDIO,
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
@@ -39,7 +25,6 @@ static struct peer_config {
esp_ble_conn_t *conn;
uint16_t conn_handle;
bool disc_completed;
bool disc_cancelled;
bool mtu_exchanged;
} peer = {
.conn_handle = CONN_HANDLE_INIT,
@@ -454,54 +439,10 @@ static esp_ble_audio_cap_initiator_cb_t cap_cb = {
.unicast_start_complete = unicast_start_complete_cb,
};
static int conn_create(ble_addr_t *dst)
{
struct ble_gap_conn_params params = {0};
uint8_t own_addr_type = 0;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
err = ble_gap_disc_cancel();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return err;
}
peer.disc_cancelled = true;
params.scan_itvl = INIT_SCAN_INTERVAL;
params.scan_window = INIT_SCAN_WINDOW;
params.itvl_min = CONN_INTERVAL;
params.itvl_max = CONN_INTERVAL;
params.latency = CONN_LATENCY;
params.supervision_timeout = CONN_TIMEOUT;
params.max_ce_len = CONN_MAX_CE_LEN;
params.min_ce_len = CONN_MIN_CE_LEN;
return ble_gap_connect(own_addr_type, dst, CONN_DURATION, &params,
example_audio_gap_event_cb, NULL);
}
static int pairing_start(uint16_t conn_handle)
{
return ble_gap_security_initiate(conn_handle);
}
static int exchange_mtu(uint16_t conn_handle)
{
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
}
static bool check_and_connect(uint8_t type, const uint8_t *data,
uint8_t data_len, void *user_data)
{
esp_ble_audio_gap_app_event_t *event;
ble_addr_t dst = {0};
uint16_t uuid_val;
int err;
@@ -526,17 +467,20 @@ static bool check_and_connect(uint8_t type, const uint8_t *data,
ESP_LOGI(TAG, "Found CAS in peer adv data!");
dst.type = event->ext_scan_recv.addr.type;
memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val));
/* Stop scanning before connect — NimBLE rejects ble_gap_connect while
* a discovery procedure is running. On failure restart scanning so we
* don't stall in no-scan no-conn state. */
err = ext_scan_stop();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return false;
}
err = conn_create(&dst);
err = conn_create(event->ext_scan_recv.addr.type,
event->ext_scan_recv.addr.val);
if (err) {
ESP_LOGE(TAG, "Failed to create conn, err %d", err);
if (peer.disc_cancelled) {
peer.disc_cancelled = false;
cap_initiator_unicast_start();
}
cap_initiator_unicast_start();
}
return false; /* Stop parsing */
@@ -562,14 +506,16 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
if (event->acl_connect.status) {
ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status);
/* check_and_connect stopped scanning before conn_create. acl_disconnect
* (which restarts scan) only fires on an established connection, so
* we must resume here to avoid a no-scan / no-conn dead state. */
cap_initiator_unicast_start();
return;
}
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, event->acl_connect.role,
event->acl_connect.dst.val[5], event->acl_connect.dst.val[4],
event->acl_connect.dst.val[3], event->acl_connect.dst.val[2],
event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
peer.conn_handle = event->acl_connect.conn_handle;
@@ -590,7 +536,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
peer.source_ep = NULL;
peer.sink_ep = NULL;
peer.disc_completed = false;
peer.disc_cancelled = false;
peer.mtu_exchanged = false;
unicast_group_delete();
@@ -603,8 +548,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event)
int err;
if (event->security_change.status) {
example_audio_security_failed_recover(TAG, event->security_change.conn_handle,
event->security_change.status);
security_failed_recover(event->security_change.conn_handle,
event->security_change.status);
return;
}
@@ -707,29 +652,7 @@ void cap_initiator_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event)
int cap_initiator_unicast_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for CAP Acceptor...");
return 0;
return ext_scan_start();
}
int cap_initiator_unicast_init(void)
@@ -6,13 +6,8 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "cap_initiator.h"
@@ -40,6 +35,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -68,16 +69,7 @@ void app_main(void)
return;
}
/* Match the GAP device name to the role this build was compiled for, so
* the name advertised in GAP reads matches the local-name field embedded
* in extended advertising (broadcast mode uses "CAP Broadcast Source",
* which is what the acceptor's self-scan path matches against).
*/
#if CONFIG_EXAMPLE_BROADCAST
err = ble_svc_gap_device_name_set("CAP Broadcast Source");
#else
err = ble_svc_gap_device_name_set("CAP Initiator");
#endif
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
@@ -0,0 +1,123 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "os/os_mbuf.h"
#include "cap_initiator.h"
/* Non-connectable broadcaster: no GAP events for the application. */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len)
{
struct ble_gap_periodic_adv_params per_params = {0};
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
ext_params.connectable = 0;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(ext_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return -1;
}
err = os_mbuf_append(data, ext_data, ext_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return err;
}
per_params.include_tx_power = 0;
per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
if (err) {
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(per_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get per adv mbuf");
return -1;
}
err = os_mbuf_append(data, per_data, per_len);
if (err) {
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
return err;
}
err = ble_gap_periodic_adv_start(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
return err;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return err;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_common_api.h"
#include "cap_initiator.h"
/* Init/conn parameters are shared with the bluedroid wrapper via cap_initiator.h.
* CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */
#define CONN_DURATION 10000 /* 10s */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_EXT_DISC:
case BLE_GAP_EVENT_CONNECT:
case BLE_GAP_EVENT_DISCONNECT:
case BLE_GAP_EVENT_ENC_CHANGE:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_MTU:
case BLE_GAP_EVENT_NOTIFY_RX:
case BLE_GAP_EVENT_NOTIFY_TX:
case BLE_GAP_EVENT_SUBSCRIBE:
esp_ble_audio_gatt_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for CAP Acceptor...");
return 0;
}
int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
int conn_create(uint8_t addr_type, const uint8_t addr[6])
{
struct ble_gap_conn_params params = {0};
uint8_t own_addr_type = 0;
ble_addr_t dst = {0};
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
params.scan_itvl = INIT_SCAN_INTERVAL;
params.scan_window = INIT_SCAN_WINDOW;
params.itvl_min = CONN_INTERVAL;
params.itvl_max = CONN_INTERVAL;
params.latency = CONN_LATENCY;
params.supervision_timeout = CONN_TIMEOUT;
params.max_ce_len = CONN_MAX_CE_LEN;
params.min_ce_len = CONN_MIN_CE_LEN;
dst.type = addr_type;
memcpy(dst.val, addr, sizeof(dst.val));
return ble_gap_connect(own_addr_type, &dst, CONN_DURATION,
&params, gap_event_cb, NULL);
}
int pairing_start(uint16_t conn_handle)
{
return ble_gap_security_initiate(conn_handle);
}
int exchange_mtu(uint16_t conn_handle)
{
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
}
void security_failed_recover(uint16_t conn_handle, uint8_t status)
{
struct ble_gap_conn_desc desc = {0};
int rc;
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
rc = ble_gap_conn_find(conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}
@@ -3,14 +3,15 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_GATTC_NOTIF_REG_MAX=20
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -20,6 +21,8 @@ CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
CONFIG_BT_BAP_BROADCAST_SOURCE=y
CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=2
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_EXAMPLE_UNICAST=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,12 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -4,16 +4,21 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "sdkconfig.h"
#if CONFIG_BLE_LOG_ENABLED
#include "ble_log.h"
#endif
#ifdef CONFIG_BT_BLUEDROID_ENABLED
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#endif /* CONFIG_BT_BLUEDROID_ENABLED */
#ifdef CONFIG_BT_NIMBLE_ENABLED
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
@@ -24,9 +29,109 @@
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#endif /* CONFIG_BT_NIMBLE_ENABLED */
#include "ble_audio_example_init.h"
#define TAG "EXAMPLE_INIT"
#ifdef CONFIG_BT_BLUEDROID_ENABLED
esp_err_t bluetooth_init(void)
{
esp_err_t ret;
/* When controller log is disabled (or running in v1 mode), the
* automatic ble_log_init() inside esp_bt_controller_init() is skipped.
* Init manually here so HOST/ISO/AUDIO compressed logs aren't dropped
* during host bring-up. The function is idempotent.
*
* TODO: Remove this block (and the ble_log_flush() below) once
* ble_log_init() is decoupled from the controller log path and
* owns its own initialization independently. */
#if CONFIG_BLE_LOG_ENABLED && \
!(CONFIG_BT_LE_CONTROLLER_LOG_ENABLED && CONFIG_BT_LE_CONTROLLER_LOG_MODE_BLE_LOG_V2)
if (!ble_log_init()) {
ESP_LOGE(TAG, "Failed to init ble_log");
return ESP_FAIL;
}
#endif
ret = esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
if (ret && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to release classic BT memory, err %d", ret);
goto err_log;
}
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(TAG, "Failed to init controller, err %d", ret);
goto err_log;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(TAG, "Failed to enable controller, err %d", ret);
goto err_controller_init;
}
esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
ret = esp_bluedroid_init_with_cfg(&cfg);
if (ret) {
ESP_LOGE(TAG, "Failed to init bluedroid, err %d", ret);
goto err_controller_enable;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(TAG, "Failed to enable bluedroid, err %d", ret);
goto err_bluedroid_init;
}
/* Just Works pairing (no MITM, SC + bond, IO_CAP=NONE).
* Matches the NimBLE ble_hs_cfg defaults below. */
esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_BOND;
esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE;
uint8_t key_size = 16;
uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(auth_req));
esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(iocap));
esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(key_size));
esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(init_key));
esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(rsp_key));
/* Drain startup-phase log buffers and emit a FLUSH boundary so the
* parser resets stats here — the example's runtime logs start clean.
*
* TODO: Remove together with the manual ble_log_init() above once
* ble_log_init() is decoupled from the controller log path. */
#if CONFIG_BLE_LOG_ENABLED
ble_log_flush();
#endif
return ESP_OK;
err_bluedroid_init:
esp_bluedroid_deinit();
err_controller_enable:
esp_bt_controller_disable();
err_controller_init:
esp_bt_controller_deinit();
err_log:
/* Mirror the manual ble_log_init() block above — same Kconfig gate so
* we only deinit what we initialized; the controller-owned path is
* left alone. */
#if CONFIG_BLE_LOG_ENABLED && \
!(CONFIG_BT_LE_CONTROLLER_LOG_ENABLED && CONFIG_BT_LE_CONTROLLER_LOG_MODE_BLE_LOG_V2)
ble_log_deinit();
#endif
return ret;
}
#endif /* CONFIG_BT_BLUEDROID_ENABLED */
#ifdef CONFIG_BT_NIMBLE_ENABLED
static SemaphoreHandle_t example_audio_sem;
void ble_store_config_init(void);
@@ -228,7 +333,7 @@ esp_err_t bluetooth_init(void)
/* When controller log is disabled (or running in v1 mode), the
* automatic ble_log_init() inside esp_bt_controller_init() is skipped.
* Init manually here so HOST/ISO/AUDIO compressed logs aren't dropped
* during NimBLE bring-up. The function is idempotent.
* during host bring-up. The function is idempotent.
*
* TODO: Remove this block (and the ble_log_flush() below) once
* ble_log_init() is decoupled from the controller log path and
@@ -297,3 +402,4 @@ err_log:
#endif
return ret;
}
#endif /* CONFIG_BT_NIMBLE_ENABLED */
@@ -26,66 +26,6 @@
*/
#define LOG_INTERVAL_PACKETS 6000
int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg)
{
if (event->type == BLE_GAP_EVENT_EXT_DISC ||
event->type == BLE_GAP_EVENT_PERIODIC_SYNC ||
event->type == BLE_GAP_EVENT_PERIODIC_REPORT ||
event->type == BLE_GAP_EVENT_PERIODIC_SYNC_LOST ||
event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER ||
event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 ||
event->type == BLE_GAP_EVENT_CONNECT ||
event->type == BLE_GAP_EVENT_DISCONNECT ||
event->type == BLE_GAP_EVENT_ENC_CHANGE) {
esp_ble_audio_gap_app_post_event(event->type, event);
} else if (event->type == BLE_GAP_EVENT_MTU ||
event->type == BLE_GAP_EVENT_NOTIFY_RX ||
event->type == BLE_GAP_EVENT_NOTIFY_TX ||
event->type == BLE_GAP_EVENT_SUBSCRIBE) {
esp_ble_audio_gatt_app_post_event(event->type, event);
} else if (event->type == BLE_GAP_EVENT_REPEAT_PAIRING) {
/* Peer wants to re-pair on top of an existing bond (e.g. phone
* cleared its LTK via "Forget device" but our NVS still has it).
* Delete our stale bond and tell NimBLE to retry pairing.
*/
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
return 0;
}
void example_audio_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status)
{
struct ble_gap_conn_desc desc = {0};
int rc;
/* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical
* cause is asymmetric bond state — the two sides bonded previously,
* then one side erased its NVS while the other kept the LTK. The side
* that still has the bond tries encryption with the cached key; the
* other side has nothing to match it against, so SMP times out. Drop
* our entry for this peer and disconnect; the next connection runs
* fresh pairing (peer side recovers via REPEAT_PAIRING).
*/
ESP_LOGE(tag, "Security change failed, status %u, clearing local bond and reconnecting", status);
rc = ble_gap_conn_find(conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
/* Tear down the link: SMP cannot be retried on the same connection
* after a failed/timed-out exchange. The next reconnect starts a
* fresh SMP procedure with the now-cleared bond state.
*/
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}
static void print_hex(const uint8_t *ptr, size_t len)
{
while (len--) {
@@ -16,12 +16,10 @@
#include "sdkconfig.h"
#include "esp_err.h"
#include "esp_log.h"
#include "zephyr/kernel.h"
#include "host/ble_gap.h"
#include "host/ble_hs_adv.h"
#include "host/ble_store.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_ble_audio_bap_api.h"
@@ -52,9 +50,60 @@
#define EXAMPLE_BYTES_LIST_LE48 BT_BYTES_LIST_LE48
#define EXAMPLE_BYTES_LIST_LE64 BT_BYTES_LIST_LE64
int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg);
/* bt_le_addr.val carries the host stack's native byte order: NimBLE is
* LSB-first (BT spec wire order), Bluedroid is MSB-first (BD_ADDR). Use
* this with "%02x:%02x:%02x:%02x:%02x:%02x" to print MSB-first regardless
* of host. Examples can otherwise pass val[] straight to their host APIs
* without conversion. */
#if CONFIG_BT_BLUEDROID_ENABLED
#define EXAMPLE_BT_ADDR_PRINT_ARGS(_v) \
(_v)[0], (_v)[1], (_v)[2], (_v)[3], (_v)[4], (_v)[5]
void example_audio_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status);
/* Fire-and-wait helper for Bluedroid GAP APIs that pair an async call with
* a matching ESP_GAP_BLE_*_COMPLETE_EVT. The example's GAP event handler
* is expected to xSemaphoreGive(_sem) on that COMPLETE_EVT. `TAG` and the
* enclosing function's `esp_err_t` return are taken from the call site.
*
* !!! Demo-only — DO NOT copy into production code. !!!
* Risks (acceptable in linear init sequences, not in customer apps):
* - Same-task deadlock: must NOT be called from the task that dispatches
* the COMPLETE_EVT (BTC task by default; lib forwarding can route some
* events through other tasks — verify the path for your event).
* - portMAX_DELAY = no timeout: hangs forever on controller/firmware
* misbehavior. Production must use a bounded timeout + recovery.
* - Shared binary sem: concurrent callers race; first take consumes
* whichever give arrived. Examples avoid this by serializing init.
* - No abort hook: nothing can break a portMAX_DELAY wait at shutdown.
* - Status not checked: see EXAMPLE_WAIT_API_CHECK below.
*
* Production should drive BLE init/teardown via an event-driven state
* machine (one task owns the BLE flow, callbacks advance state, no
* blocking from inside callbacks). NimBLE host returns ctrl errors
* synchronously and sidesteps most of this complexity. */
#define EXAMPLE_WAIT_API(_call, _sem, _delay) do { \
esp_err_t _err = (_call); \
if (_err != ESP_OK) { \
ESP_LOGE(TAG, #_call " failed, err %d", _err); \
return _err; \
} \
xSemaphoreTake(_sem, _delay); \
} while (0)
/* As EXAMPLE_WAIT_API but also checks the controller status latched by the
* gap_event_handler into `_op_status` after the sem is taken. Returns
* ESP_FAIL on async failure. Caller must provide a static esp_bt_status_t
* and write it from each COMPLETE_EVT param before xSemaphoreGive. */
#define EXAMPLE_WAIT_API_CHECK(_call, _sem, _delay, _op_status) do { \
EXAMPLE_WAIT_API(_call, _sem, _delay); \
if ((_op_status) != ESP_BT_STATUS_SUCCESS) { \
ESP_LOGE(TAG, #_call " ctrl status %d", (_op_status)); \
return ESP_FAIL; \
} \
} while (0)
#else
#define EXAMPLE_BT_ADDR_PRINT_ARGS(_v) \
(_v)[5], (_v)[4], (_v)[3], (_v)[2], (_v)[1], (_v)[0]
#endif
void example_print_codec_cfg(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg);
@@ -9,7 +9,7 @@
This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Receiver (BMR)** role. The device scans for non-connectable extended advertisers that carry both the Broadcast Audio Announcement service (with a 24-bit broadcast ID) and the TMAS service with the **BMS** role bit set, then synchronizes to periodic advertising and the BIG to receive broadcast audio.
The implementation runs on the **NimBLE** host stack and uses ESP-BLE-AUDIO for TMAP role registration, the BAP broadcast sink, the BAP scan delegator, the PACS sink (LC3 capability: 48 kHz, 10 ms, 1 channel, 100 octets/frame, media context — sized for the 48_2_1 preset that the paired BMS sends), and a VCP **Volume Renderer** (initial volume 10, unmuted, step 1). Up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` streams are pre-allocated and reused for every BIG sync; received SDUs are accounted for via `example_audio_rx_metrics_*`.
The implementation uses ESP-BLE-AUDIO for TMAP role registration, the BAP broadcast sink, the BAP scan delegator, the PACS sink (LC3 capability: 48 kHz, 10 ms, 1 channel, 100 octets/frame, media context — sized for the 48_2_1 preset that the paired BMS sends), and a VCP **Volume Renderer** (initial volume 10, unmuted, step 1). Up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` streams are pre-allocated and reused for every BIG sync; received SDUs are accounted for via `example_audio_rx_metrics_*`.
## Requirements
@@ -26,15 +26,28 @@ No build-time options — runtime defaults are baked into source.
### Security & Pairing
NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMR receives broadcast (unencrypted) audio, so these settings only apply if a peer (e.g. a Broadcast Assistant) opens an ATT connection to the scan delegator / VCP renderer.
Security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMR receives broadcast (unencrypted) audio, so these settings only apply if a peer (e.g. a Broadcast Assistant) opens an ATT connection to the scan delegator / VCP renderer.
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -2,5 +2,12 @@ set(srcs "bap_broadcast_sink.c"
"vcp_vol_renderer.c"
"main.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/scan.c")
else()
list(APPEND srcs "nimble/scan.c")
endif()
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -9,15 +9,9 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <errno.h>
#include "tmap_bmr.h"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
#define PA_SYNC_HANDLE_INIT UINT16_MAX
static uint16_t sync_handle = PA_SYNC_HANDLE_INIT;
@@ -142,30 +136,6 @@ static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs;
static void sync_broadcast_pa(const bt_addr_le_t *addr,
uint8_t adv_sid,
uint32_t broadcast_id)
{
struct ble_gap_periodic_sync_params params = {0};
ble_addr_t sync_addr = {0};
int err;
sync_addr.type = addr->type;
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
err = ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
return;
}
bcast_id = broadcast_id;
pa_syncing = true; /* Mark PA sync as in progress */
}
static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data,
uint8_t data_len, void *user_data)
{
@@ -216,7 +186,7 @@ static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data,
void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
{
uint32_t broadcast_id;
bt_addr_le_t addr;
int err;
if ((event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) ||
event->ext_scan_recv.per_adv_itvl == 0) {
@@ -238,10 +208,16 @@ void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Found TMAP BMS, starting PA sync (broadcast ID 0x%06lx)",
broadcast_id);
addr.type = event->ext_scan_recv.addr.type;
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
err = pa_sync_create(event->ext_scan_recv.addr.type,
event->ext_scan_recv.addr.val,
event->ext_scan_recv.sid);
if (err) {
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
return;
}
sync_broadcast_pa(&addr, event->ext_scan_recv.sid, broadcast_id);
bcast_id = broadcast_id;
pa_syncing = true;
}
}
@@ -259,7 +235,7 @@ void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "PA sync %u synced with broadcast ID 0x%06x",
event->pa_sync.sync_handle, bcast_id);
err = ble_gap_disc_cancel();
err = ext_scan_stop();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return;
@@ -296,29 +272,7 @@ void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
int bap_broadcast_sink_scan(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for broadcaster...");
return 0;
return ext_scan_start();
}
int bap_broadcast_sink_init(void)
@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "tmap_bmr.h"
static SemaphoreHandle_t scan_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t scan_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
static esp_ble_ext_scan_params_t ext_scan_params = {
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
.uncoded_cfg = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.scan_interval = SCAN_INTERVAL,
.scan_window = SCAN_WINDOW,
},
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
scan_op_status = param->set_ext_scan_params.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
scan_op_status = param->ext_scan_start.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
scan_op_status = param->ext_scan_stop.status;
xSemaphoreGive(scan_sem);
break;
/* PA sync / ext adv events: forwarded by adapter's BTA path, not here. */
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
scan_sem = xSemaphoreCreateBinary();
if (scan_sem == NULL) {
ESP_LOGE(TAG, "Failed to create scan semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(scan_sem);
return err;
}
return 0;
}
int ext_scan_start(void)
{
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
ESP_LOGI(TAG, "Scanning for broadcaster...");
return 0;
}
int ext_scan_stop(void)
{
WAIT_API(esp_ble_gap_stop_ext_scan());
return 0;
}
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
{
esp_ble_gap_periodic_adv_sync_params_t params = {
.filter_policy = 0,
.sid = sid,
.addr_type = addr_type,
.skip = PA_SYNC_SKIP,
.sync_timeout = PA_SYNC_TIMEOUT,
};
memcpy(params.addr, addr, sizeof(params.addr));
/* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is
* air-dependent and surfaces asynchronously. */
return esp_ble_gap_periodic_adv_create_sync(&params);
}
int pa_sync_terminate(uint16_t sync_handle)
{
return esp_ble_gap_periodic_adv_sync_terminate(sync_handle);
}
@@ -6,13 +6,8 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "tmap_bmr.h"
@@ -26,9 +21,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->pa_sync.sync_handle, event->pa_sync.sid,
event->pa_sync.adv_phy,
event->pa_sync.addr.val[5], event->pa_sync.addr.val[4],
event->pa_sync.addr.val[3], event->pa_sync.addr.val[2],
event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->pa_sync.addr.val));
bap_broadcast_pa_sync(event);
}
@@ -80,6 +73,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&init_info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "esp_ble_audio_common_api.h"
#include "tmap_bmr.h"
/* Forward only the GAP events the application consumes. */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_EXT_DISC:
case BLE_GAP_EVENT_PERIODIC_SYNC:
case BLE_GAP_EVENT_PERIODIC_REPORT:
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for broadcaster...");
return 0;
}
int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
{
struct ble_gap_periodic_sync_params params = {0};
ble_addr_t sync_addr = {0};
sync_addr.type = addr_type;
memcpy(sync_addr.val, addr, sizeof(sync_addr.val));
params.skip = PA_SYNC_SKIP;
params.sync_timeout = PA_SYNC_TIMEOUT;
return ble_gap_periodic_adv_sync_create(&sync_addr, sid, &params,
gap_event_cb, NULL);
}
int pa_sync_terminate(uint16_t sync_handle)
{
return ble_gap_periodic_adv_sync_terminate(sync_handle);
}
@@ -5,12 +5,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "sdkconfig.h"
#include "host/ble_hs.h"
#include "esp_ble_audio_bap_api.h"
#include "esp_ble_audio_cap_api.h"
#include "esp_ble_audio_pacs_api.h"
@@ -22,6 +22,20 @@
#define TAG "TMAP_BMR"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
int app_host_init(void);
int ext_scan_start(void);
int ext_scan_stop(void);
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid);
int pa_sync_terminate(uint16_t sync_handle);
int vcp_vol_renderer_init(void);
void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event);
@@ -9,13 +9,6 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "tmap_bmr.h"
@@ -3,14 +3,16 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=30
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_GATT_MAX_SR_PROFILES=12
CONFIG_BT_GATTC_NOTIF_REG_MAX=30
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -19,6 +21,8 @@ CONFIG_BT_CAP_ACCEPTOR=y
CONFIG_BT_BAP_SCAN_DELEGATOR=y
CONFIG_BT_BAP_BROADCAST_SINK=y
CONFIG_BT_VCP_VOL_REND=y
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,12 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -9,7 +9,7 @@
This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Sender (BMS)** role. The device acts as a non-connectable BAP Broadcast Source: extended advertising carries the TMAS service UUID with the BMS role flag, the Broadcast Audio Announcement service with a 24-bit broadcast ID (`0x123456`), and the complete local name (`"TMAP Broadcast Source"`); periodic advertising carries the encoded BASE.
The implementation runs on the **NimBLE** host stack and uses the ESP-BLE-AUDIO library for the TMAP role registration and the CAP initiator broadcast pieces. A single subgroup with one stream is configured from the LC3 `48_2_1` broadcast preset (front-left location, media context). After the broadcast source is created and the BIG is started, a periodic TX scheduler (`example_audio_tx_scheduler_*`) drives ISO SDU transmission at the stream's QoS interval, and a sent callback tracks packet counts and drift.
The implementation uses the ESP-BLE-AUDIO library for the TMAP role registration and the CAP initiator broadcast pieces. A single subgroup with one stream is configured from the LC3 `48_2_1` broadcast preset (front-left location, media context). After the broadcast source is created and the BIG is started, a periodic TX scheduler (`example_audio_tx_scheduler_*`) drives ISO SDU transmission at the stream's QoS interval, and a sent callback tracks packet counts and drift.
## Requirements
@@ -26,15 +26,28 @@ No build-time options — runtime defaults are baked into source.
### Security & Pairing
NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMS broadcast itself is non-connectable and unencrypted, so these settings only apply if a peer ever opens an ATT connection.
Security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMS broadcast itself is non-connectable and unencrypted, so these settings only apply if a peer ever opens an ATT connection.
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -1,5 +1,12 @@
set(srcs "cap_initiator.c"
"main.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/adv.c")
else()
list(APPEND srcs "nimble/adv.c")
endif()
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,146 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "tmap_bms.h"
static SemaphoreHandle_t adv_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t adv_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED,
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.channel_map = ADV_CHNL_ALL,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.primary_phy = ESP_BLE_GAP_PHY_1M,
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_2M,
.sid = ADV_SID,
.scan_req_notif = false,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.tx_power = ADV_TX_POWER,
};
static esp_ble_gap_periodic_adv_params_t periodic_adv_params = {
.interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
.properties = 0,
};
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
[0] = { ADV_HANDLE, 0, 0 },
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
adv_op_status = param->ext_adv_start.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT:
adv_op_status = param->ext_adv_stop.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->peroid_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->period_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT:
adv_op_status = param->period_adv_start.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_PERIODIC_ADV_STOP_COMPLETE_EVT:
adv_op_status = param->period_adv_stop.status;
xSemaphoreGive(adv_sem);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
adv_sem = xSemaphoreCreateBinary();
if (adv_sem == NULL) {
ESP_LOGE(TAG, "Failed to create adv semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(adv_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len)
{
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params));
#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false));
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true));
#else
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data));
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE));
#endif
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
int ext_adv_stop(void)
{
static const uint8_t ext_adv_stop_inst[1] = { ADV_HANDLE };
WAIT_API(esp_ble_gap_periodic_adv_stop(ADV_HANDLE));
WAIT_API(esp_ble_gap_ext_adv_stop(1, ext_adv_stop_inst));
ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE);
return 0;
}
@@ -6,27 +6,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stddef.h>
#include <stdint.h>
#include <errno.h>
#include <stdlib.h>
#include "esp_timer.h"
#include "esp_random.h"
#include <string.h>
#include <errno.h>
#include "tmap_bms.h"
#define ADV_HANDLE 0x00
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
#define LOCAL_DEVICE_NAME "TMAP Broadcast Source"
#define LOCAL_BROADCAST_ID 0x123456
static esp_ble_audio_cap_initiator_broadcast_subgroup_param_t subgroup_param;
@@ -233,144 +219,6 @@ static uint8_t *per_adv_data_get(uint8_t *data_len)
return data;
}
static int ext_adv_start(void)
{
struct ble_gap_periodic_adv_params per_params = {0};
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
uint8_t *ext_data = NULL;
uint8_t *per_data = NULL;
uint8_t data_len = 0;
int err;
ext_params.connectable = 0;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = ADV_ADDRESS;
ext_params.primary_phy = ADV_PRIMARY_PHY;
ext_params.secondary_phy = ADV_SECONDARY_PHY;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = ADV_INTERVAL;
ext_params.itvl_max = ADV_INTERVAL;
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
goto end;
}
ext_data = ext_adv_data_get(&data_len);
if (ext_data == NULL) {
err = -ENOMEM;
goto end;
}
data = os_msys_get_pkthdr(data_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
err = -ENOMEM;
goto end;
}
err = os_mbuf_append(data, ext_data, data_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
goto end;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
goto end;
}
per_params.include_tx_power = 0;
per_params.itvl_min = PER_ADV_INTERVAL;
per_params.itvl_max = PER_ADV_INTERVAL;
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
if (err) {
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
goto end;
}
per_data = per_adv_data_get(&data_len);
if (per_data == NULL) {
err = -ENOMEM;
goto end;
}
data = os_msys_get_pkthdr(data_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get per adv mbuf");
err = -ENOMEM;
goto end;
}
err = os_mbuf_append(data, per_data, data_len);
if (err) {
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
os_mbuf_free_chain(data);
goto end;
}
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
goto end;
}
err = ble_gap_periodic_adv_start(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
goto end;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
goto end;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
free(ext_data);
}
if (per_data) {
free(per_data);
}
return err;
}
static int ext_adv_stop(void)
{
int ret = 0;
int err;
err = ble_gap_periodic_adv_stop(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err);
ret = err;
}
err = ble_gap_ext_adv_stop(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err);
ret = err;
}
if (ret == 0) {
ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE);
}
return ret;
}
int cap_initiator_setup(void)
{
esp_ble_audio_bap_broadcast_adv_info_t info = {
@@ -378,6 +226,11 @@ int cap_initiator_setup(void)
};
bool adv_started = false;
bool adv_added = false;
/* BASE (per_adv_data_get) needs broadcast_source to exist — build after create. */
uint8_t *ext_data = NULL;
uint8_t *per_data = NULL;
uint8_t ext_len = 0;
uint8_t per_len = 0;
int err;
stream_params.stream = &broadcast_stream;
@@ -402,7 +255,19 @@ int cap_initiator_setup(void)
return err;
}
err = ext_adv_start();
ext_data = ext_adv_data_get(&ext_len);
if (ext_data == NULL) {
err = -ENOMEM;
goto end;
}
per_data = per_adv_data_get(&per_len);
if (per_data == NULL) {
err = -ENOMEM;
goto end;
}
err = ext_adv_start(ext_data, ext_len, per_data, per_len);
if (err) {
goto end;
}
@@ -420,18 +285,25 @@ int cap_initiator_setup(void)
err = esp_ble_audio_cap_initiator_broadcast_audio_start(broadcast_source, ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
goto end;
}
ESP_LOGI(TAG, "CAP initiator setup");
return 0;
end:
if (ext_data != NULL) {
free(ext_data);
}
if (per_data != NULL) {
free(per_data);
}
if (err == 0) {
ESP_LOGI(TAG, "CAP initiator setup");
return 0;
}
if (adv_added) {
err = esp_ble_audio_bap_broadcast_adv_delete(&info);
if (err) {
ESP_LOGE(TAG, "Failed to delete adv for broadcast source, err %d", err);
esp_err_t e = esp_ble_audio_bap_broadcast_adv_delete(&info);
if (e) {
ESP_LOGE(TAG, "Failed to delete adv for broadcast source, err %d", e);
}
}
@@ -6,13 +6,8 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "tmap_bms.h"
@@ -38,6 +33,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&init_info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -61,5 +62,11 @@ void app_main(void)
return;
}
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
}
cap_initiator_setup();
}
@@ -0,0 +1,147 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "os/os_mbuf.h"
#include "tmap_bms.h"
/* Non-connectable broadcaster: no GAP events for the application. */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len)
{
struct ble_gap_periodic_adv_params per_params = {0};
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
ext_params.connectable = 0;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(ext_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return -1;
}
err = os_mbuf_append(data, ext_data, ext_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return err;
}
per_params.include_tx_power = 0;
per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
if (err) {
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(per_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get per adv mbuf");
return -1;
}
err = os_mbuf_append(data, per_data, per_len);
if (err) {
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
return err;
}
err = ble_gap_periodic_adv_start(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
return err;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return err;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
int ext_adv_stop(void)
{
int ret = 0;
int err;
err = ble_gap_periodic_adv_stop(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err);
ret = err;
}
err = ble_gap_ext_adv_stop(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err);
ret = err;
}
if (ret == 0) {
ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE);
}
return ret;
}
@@ -5,12 +5,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_log.h"
#include "sdkconfig.h"
#include "host/ble_hs.h"
#include "esp_ble_audio_cap_api.h"
#include "esp_ble_audio_tmap_api.h"
#include "esp_ble_audio_bap_lc3_preset_defs.h"
@@ -20,6 +22,23 @@
#define TAG "TMAP_BMS"
#define LOCAL_DEVICE_NAME "TMAP Broadcast Source"
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
#define PER_ADV_INTERVAL_MS 100
int app_host_init(void);
int set_device_name(void);
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
const uint8_t *per_data, uint8_t per_len);
int ext_adv_stop(void);
int cap_initiator_setup(void);
int cap_initiator_update(void);
@@ -3,13 +3,14 @@
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=30
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -17,4 +18,6 @@ CONFIG_BT_TMAP=y
CONFIG_BT_CAP_INITIATOR=y
CONFIG_BT_BAP_BROADCAST_SOURCE=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,10 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -9,7 +9,7 @@
This example takes the **TMAP Call Gateway (CG)** and **Unicast Media Sender (UMS)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS)`. It scans for connectable extended advertising that carries TMAS service data with the **UMR** role bit set, connects to the first match, pairs, exchanges MTU, and then drives TMAP and VCP discovery before bringing up unicast audio.
The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`.
The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`.
## Requirements
@@ -30,11 +30,24 @@ Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bond
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -4,5 +4,12 @@ set(srcs "cap_initiator.c"
"vcp_vol_ctlr.c"
"main.c")
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/central.c")
else()
list(APPEND srcs "nimble/central.c")
endif()
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,193 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_ble_audio_common_api.h"
#include "tmap_central.h"
static SemaphoreHandle_t scan_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t scan_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
/* Cached peer address. Bluedroid's pairing and disconnect APIs key off
* bd_addr rather than conn_handle, so we stash the addr at conn_create
* time and reuse it in pairing_start / security_failed_recover. */
static esp_bd_addr_t peer_bda;
static esp_ble_ext_scan_params_t ext_scan_params = {
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
.uncoded_cfg = {
.scan_type = BLE_SCAN_TYPE_PASSIVE,
.scan_interval = SCAN_INTERVAL,
.scan_window = SCAN_WINDOW,
},
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
scan_op_status = param->set_ext_scan_params.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
scan_op_status = param->ext_scan_start.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
scan_op_status = param->ext_scan_stop.status;
xSemaphoreGive(scan_sem);
break;
/* SMP request handling for Just Works pairing (IO_CAP=NONE). NC_REQ
* still fires under LE Secure Connections — auto-accept. We never get
* SEC_REQ here because the central initiates via esp_ble_set_encryption. */
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
break;
/* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT
* is forwarded by adapter's BTA path, don't re-post here. */
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
scan_sem = xSemaphoreCreateBinary();
if (scan_sem == NULL) {
ESP_LOGE(TAG, "Failed to create scan semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(scan_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_scan_start(void)
{
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
ESP_LOGI(TAG, "Scanning for peripheral...");
return 0;
}
int ext_scan_stop(void)
{
WAIT_API(esp_ble_gap_stop_ext_scan());
return 0;
}
int conn_create(uint8_t addr_type, const uint8_t addr[6])
{
/* Values shared with NimBLE side via tmap_central.h for behavioral parity.
* Without prefer_ext_connect_params_set, L2CAP falls back to defaults
* and logs "No extend connection parameters set". */
const esp_ble_gap_conn_params_t conn_params = {
.scan_interval = INIT_SCAN_INTERVAL,
.scan_window = INIT_SCAN_WINDOW,
.interval_min = CONN_INTERVAL,
.interval_max = CONN_INTERVAL,
.latency = CONN_LATENCY,
.supervision_timeout = CONN_TIMEOUT,
.min_ce_len = CONN_MIN_CE_LEN,
.max_ce_len = CONN_MAX_CE_LEN,
};
esp_gatt_if_t gattc_if;
esp_err_t err;
memcpy(peer_bda, addr, sizeof(peer_bda));
err = esp_ble_gap_prefer_ext_connect_params_set(
peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err);
return err;
}
/* Use the audio engine's GATTC if so OPEN/CONNECT events route back to
* the engine. aux_open initiates an ACL against an extended advertiser.
*
* engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet registered;
* feeding that into aux_open silently no-ops in BTC and no acl_connect
* event ever fires, leaving the caller in a no-scan / no-conn dead state. */
gattc_if = esp_ble_audio_bluedroid_get_gattc_if();
if (gattc_if == ESP_GATT_IF_NONE) {
ESP_LOGE(TAG, "GATTC not registered");
return ESP_ERR_INVALID_STATE;
}
return esp_ble_gattc_aux_open(gattc_if, peer_bda,
(esp_ble_addr_type_t)addr_type, true);
}
int pairing_start(uint16_t conn_handle)
{
(void)conn_handle;
return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM);
}
int exchange_mtu(uint16_t conn_handle)
{
(void)conn_handle;
/* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open
* brings up the ACL — the MTU_UPDATED event arrives without an explicit
* kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op
* on bluedroid and a real ble_gattc_exchange_mtu on nimble. */
return 0;
}
void security_failed_recover(uint16_t conn_handle, uint8_t status)
{
(void)conn_handle;
/* Asymmetric bond state: we still hold an LTK for this peer but it
* cleared its side, so encrypt-with-cached-key times out. Drop the bond
* and tear down the link; the next reconnect runs fresh pairing. */
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
esp_ble_remove_bond_device(peer_bda);
esp_ble_gap_disconnect(peer_bda);
}
@@ -11,8 +11,6 @@
#include <stdio.h>
#include <stdlib.h>
#include "esp_timer.h"
#include "esp_ble_audio_bap_lc3_preset_defs.h"
#include "tmap_central.h"
@@ -7,8 +7,6 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "tmap_central.h"
@@ -7,36 +7,14 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "tmap_central.h"
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define INIT_SCAN_INTERVAL 16 /* 10ms */
#define INIT_SCAN_WINDOW 16 /* 10ms */
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
#define CONN_MAX_CE_LEN 0xFFFF
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_DURATION 10000 /* 10s */
static uint16_t default_conn_handle = CONN_HANDLE_INIT;
static bool disc_completed;
static bool disc_cancelled;
static bool mtu_exchanged;
uint16_t default_conn_handle_get(void)
@@ -68,80 +46,10 @@ static esp_ble_audio_tmap_cb_t tmap_callbacks = {
.discovery_complete = tmap_discovery_complete,
};
static void ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
return;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return;
}
ESP_LOGI(TAG, "Scanning for peripheral...");
}
static int conn_create(ble_addr_t *dst)
{
struct ble_gap_conn_params params = {0};
uint8_t own_addr_type = 0;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
err = ble_gap_disc_cancel();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return err;
}
disc_cancelled = true;
params.scan_itvl = INIT_SCAN_INTERVAL;
params.scan_window = INIT_SCAN_WINDOW;
params.itvl_min = CONN_INTERVAL;
params.itvl_max = CONN_INTERVAL;
params.latency = CONN_LATENCY;
params.supervision_timeout = CONN_TIMEOUT;
params.max_ce_len = CONN_MAX_CE_LEN;
params.min_ce_len = CONN_MIN_CE_LEN;
return ble_gap_connect(own_addr_type, dst, CONN_DURATION, &params,
example_audio_gap_event_cb, NULL);
}
static int pairing_start(uint16_t conn_handle)
{
return ble_gap_security_initiate(conn_handle);
}
static int exchange_mtu(uint16_t conn_handle)
{
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
}
static bool check_and_connect(uint8_t type, const uint8_t *data,
uint8_t data_len, void *user_data)
{
esp_ble_audio_gap_app_event_t *event;
ble_addr_t dst = {0};
uint16_t tmap_role;
uint16_t uuid_val;
int err;
@@ -179,17 +87,17 @@ static bool check_and_connect(uint8_t type, const uint8_t *data,
return false; /* Stop parsing */
}
dst.type = event->ext_scan_recv.addr.type;
memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val));
err = ext_scan_stop();
if (err) {
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
return false;
}
err = conn_create(&dst);
err = conn_create(event->ext_scan_recv.addr.type,
event->ext_scan_recv.addr.val);
if (err) {
ESP_LOGE(TAG, "Failed to create conn, err %d", err);
if (disc_cancelled) {
disc_cancelled = false;
ext_scan_start();
}
ext_scan_start();
}
return false; /* Stop parsing */
@@ -220,9 +128,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, event->acl_connect.role,
event->acl_connect.dst.val[5], event->acl_connect.dst.val[4],
event->acl_connect.dst.val[3], event->acl_connect.dst.val[2],
event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
default_conn_handle = event->acl_connect.conn_handle;
@@ -240,7 +146,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
default_conn_handle = CONN_HANDLE_INIT;
disc_completed = false;
disc_cancelled = false;
mtu_exchanged = false;
unicast_group_delete();
@@ -253,8 +158,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event)
int err;
if (event->security_change.status) {
example_audio_security_failed_recover(TAG, event->security_change.conn_handle,
event->security_change.status);
security_failed_recover(event->security_change.conn_handle,
event->security_change.status);
return;
}
@@ -393,6 +298,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&init_info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -431,7 +342,7 @@ void app_main(void)
return;
}
err = ble_svc_gap_device_name_set("TMAP Central");
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
@@ -5,9 +5,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <stdio.h>
#include "tmap_central.h"
int mcp_server_init(void)
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <string.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_ble_audio_common_api.h"
#include "tmap_central.h"
/* Init/conn parameters are shared with the bluedroid wrapper via tmap_central.h.
* CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */
#define CONN_DURATION 10000 /* 10s */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_EXT_DISC:
case BLE_GAP_EVENT_CONNECT:
case BLE_GAP_EVENT_DISCONNECT:
case BLE_GAP_EVENT_ENC_CHANGE:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_MTU:
case BLE_GAP_EVENT_NOTIFY_RX:
case BLE_GAP_EVENT_NOTIFY_TX:
case BLE_GAP_EVENT_SUBSCRIBE:
esp_ble_audio_gatt_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_scan_start(void)
{
struct ble_gap_disc_params params = {0};
uint8_t own_addr_type;
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
params.passive = 1;
params.itvl = SCAN_INTERVAL;
params.window = SCAN_WINDOW;
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &params,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return err;
}
ESP_LOGI(TAG, "Scanning for peripheral...");
return 0;
}
int ext_scan_stop(void)
{
return ble_gap_disc_cancel();
}
int conn_create(uint8_t addr_type, const uint8_t addr[6])
{
struct ble_gap_conn_params params = {0};
uint8_t own_addr_type = 0;
ble_addr_t dst = {0};
int err;
err = ble_hs_id_infer_auto(0, &own_addr_type);
if (err) {
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
return err;
}
params.scan_itvl = INIT_SCAN_INTERVAL;
params.scan_window = INIT_SCAN_WINDOW;
params.itvl_min = CONN_INTERVAL;
params.itvl_max = CONN_INTERVAL;
params.latency = CONN_LATENCY;
params.supervision_timeout = CONN_TIMEOUT;
params.max_ce_len = CONN_MAX_CE_LEN;
params.min_ce_len = CONN_MIN_CE_LEN;
dst.type = addr_type;
memcpy(dst.val, addr, sizeof(dst.val));
return ble_gap_connect(own_addr_type, &dst, CONN_DURATION,
&params, gap_event_cb, NULL);
}
int pairing_start(uint16_t conn_handle)
{
return ble_gap_security_initiate(conn_handle);
}
int exchange_mtu(uint16_t conn_handle)
{
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
}
void security_failed_recover(uint16_t conn_handle, uint8_t status)
{
struct ble_gap_conn_desc desc = {0};
int rc;
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
rc = ble_gap_conn_find(conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}
@@ -6,6 +6,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <assert.h>
#include <errno.h>
@@ -27,11 +28,42 @@
#include "esp_ble_audio_vocs_api.h"
#include "esp_ble_audio_tbs_api.h"
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define TAG "TMAP_CEN"
#define CONN_HANDLE_INIT 0xFFFF
#define CONN_HANDLE_INIT 0xFFFF
#define LOCAL_DEVICE_NAME "TMAP Central"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
/* ACL init parameters shared between bluedroid and nimble host wrappers.
* Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */
#define INIT_SCAN_INTERVAL 16 /* 10ms */
#define INIT_SCAN_WINDOW 16 /* 10ms */
#define CONN_INTERVAL 24 /* 30ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 5s */
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_MAX_CE_LEN 0xFFFF
int app_host_init(void);
int set_device_name(void);
int ext_scan_start(void);
int ext_scan_stop(void);
int conn_create(uint8_t addr_type, const uint8_t addr[6]);
int pairing_start(uint16_t conn_handle);
int exchange_mtu(uint16_t conn_handle);
void security_failed_recover(uint16_t conn_handle, uint8_t status);
uint16_t default_conn_handle_get(void);
@@ -7,7 +7,6 @@
*/
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include "tmap_central.h"
@@ -2,18 +2,17 @@
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=40
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=1
CONFIG_BT_GATT_MAX_SR_PROFILES=12
CONFIG_BT_GATTC_NOTIF_REG_MAX=64
CONFIG_BT_BLE_FEAT_ISO_EN=y
CONFIG_BT_ISO_MAX_CHAN=2
@@ -32,4 +31,6 @@ CONFIG_BT_MCTL_LOCAL_PLAYER_REMOTE_CONTROL=y
CONFIG_BT_TBS=y
CONFIG_BT_TBS_SUPPORTED_FEATURES=3
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
@@ -0,0 +1,12 @@
# NimBLE host overlay for this example.
# Use with:
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=64
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -9,7 +9,7 @@
This example takes the **TMAP Call Terminal (CT)** and **Unicast Media Receiver (UMR)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR)`. It runs connectable extended advertising at a 200 ms interval that includes the GAP Earbud appearance, the ASCS/CAS/TMAS UUIDs, an ASCS targeted unicast announcement carrying sink and source contexts (UNSPECIFIED | CONVERSATIONAL | MEDIA | GAME | INSTRUCTIONAL), a CAS targeted announcement, the TMAS UMR|CT role payload, and the device name `tmap_peripheral`. When the Duo earbuds option is selected, a CSIS RSI is added to the advertisement.
The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a `mcp_send_cmd` for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the `enabled` callback. Device name is set to `TMAP Peripheral`.
The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a `mcp_send_cmd` for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the `enabled` callback. Device name is set to `TMAP Peripheral`.
## Requirements
@@ -34,11 +34,24 @@ Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bond
## Build & Flash
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
### Bluedroid host (default)
```bash
idf.py set-target esp32h4
idf.py -p PORT flash monitor
```
### NimBLE host
```bash
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
```
For `esp32s31`, replace the chip overlay accordingly.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
@@ -75,6 +88,7 @@ Connection / discovery:
```
TMAP_PER: Connected: handle <h> role <r> peer <addr>
TMAP_PER: Security: handle <h> level <l> bonded <b>
TMAP_PER: MTU updated: handle <h> mtu <m>
TMAP_PER: Service discovery started: handle <h>
TMAP_PER: Service discovery complete: handle <h> status 0
@@ -113,6 +127,19 @@ TMAP_PER: Call <i> terminated
TMAP_PER: Disconnected: handle <h> reason 0x<rr>
```
**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave:
```
W BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8
W BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1
TMAP_PER: [SNK #0] ISO disconnected, reason 0x08
TMAP_PER: [SNK #0] Stream disabled
E BT_APPL: Unknown connection ID: 3 fail sending notification
TMAP_PER: [SNK #0] Stream stopped, reason 0x08
```
`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side.
Tag is `TMAP_PER`.
## Peer Pairing
@@ -10,5 +10,12 @@ if(CONFIG_EXAMPLE_TMAP_PER_DUO)
list(APPEND srcs "csip_set_member.c")
endif()
if(CONFIG_BT_BLUEDROID_ENABLED)
list(APPEND srcs "bluedroid/peripheral.c")
else()
list(APPEND srcs "nimble/peripheral.c")
endif()
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "."
REQUIRES bt nvs_flash)
@@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_gap_ble_api.h"
#include "esp_ble_audio_common_api.h"
#include "tmap_peripheral.h"
static SemaphoreHandle_t adv_sem;
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
static esp_bt_status_t adv_op_status;
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE,
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
.channel_map = ADV_CHNL_ALL,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.primary_phy = ESP_BLE_GAP_PHY_1M,
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_2M,
.sid = ADV_SID,
.scan_req_notif = false,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.tx_power = ADV_TX_POWER,
};
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
[0] = { ADV_HANDLE, 0, 0 },
};
static void gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
adv_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
adv_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(adv_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
adv_op_status = param->ext_adv_start.status;
xSemaphoreGive(adv_sem);
break;
/* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer
* (central) initiates; we accept the security request and confirm the
* numeric comparison. */
case ESP_GAP_BLE_SEC_REQ_EVT:
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
break;
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
default:
break;
}
}
int app_host_init(void)
{
esp_err_t err;
adv_sem = xSemaphoreCreateBinary();
if (adv_sem == NULL) {
ESP_LOGE(TAG, "Failed to create adv semaphore");
return -1;
}
err = esp_ble_gap_register_callback(gap_event_handler);
if (err) {
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
vSemaphoreDelete(adv_sem);
return err;
}
return 0;
}
int set_device_name(void)
{
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len)
{
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -10,7 +10,6 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "tmap_peripheral.h"
@@ -9,11 +9,6 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "host/ble_hs.h"
#include "tmap_peripheral.h"
@@ -7,31 +7,12 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "tmap_peripheral.h"
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define ADV_HANDLE 0x00
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
static uint8_t general_adv_data[] = {
/* Flags */
0x02, EXAMPLE_AD_TYPE_FLAGS, (EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR),
@@ -77,9 +58,9 @@ static uint8_t tmap_adv_data[] = {
};
static uint8_t dev_name_adv_data[] = {
/* Complete Device Name */
0x10, EXAMPLE_AD_TYPE_NAME_COMPLETE, 't', 'm', 'a', 'p', '_',
'p', 'e', 'r', 'i', 'p', 'h', 'e', 'r', 'a', 'l',
/* Complete Device Name — must match LOCAL_DEVICE_NAME set via set_device_name(). */
0x10, EXAMPLE_AD_TYPE_NAME_COMPLETE, 'T', 'M', 'A', 'P', ' ',
'P', 'e', 'r', 'i', 'p', 'h', 'e', 'r', 'a', 'l',
};
#if CONFIG_BT_CSIP_SET_MEMBER
@@ -108,7 +89,7 @@ uint16_t default_conn_handle_get(void)
return default_conn_handle;
}
static int set_ext_adv_data(void)
static int build_ext_adv_data(void)
{
uint16_t data_len = 0;
@@ -143,63 +124,6 @@ static int set_ext_adv_data(void)
return 0;
}
static void ext_adv_start(void)
{
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
ext_params.connectable = 1;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = ADV_ADDRESS;
ext_params.primary_phy = ADV_PRIMARY_PHY;
ext_params.secondary_phy = ADV_SECONDARY_PHY;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = ADV_INTERVAL;
ext_params.itvl_max = ADV_INTERVAL;
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
example_audio_gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return;
}
err = set_ext_adv_data();
if (err) {
return;
}
data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return;
}
err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data));
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
}
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
{
if (event->acl_connect.status) {
@@ -209,9 +133,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, event->acl_connect.role,
event->acl_connect.dst.val[5], event->acl_connect.dst.val[4],
event->acl_connect.dst.val[3], event->acl_connect.dst.val[2],
event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]);
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
default_conn_handle = event->acl_connect.conn_handle;
}
@@ -223,7 +145,22 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
default_conn_handle = CONN_HANDLE_INIT;
ext_adv_start();
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
static void security_change(esp_ble_audio_gap_app_event_t *event)
{
if (event->security_change.status) {
ESP_LOGE(TAG, "Security failed: handle %u status %d",
event->security_change.conn_handle,
event->security_change.status);
return;
}
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
}
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
@@ -235,6 +172,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
acl_disconnect(event);
break;
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
security_change(event);
break;
default:
break;
}
@@ -317,6 +257,12 @@ void app_main(void)
return;
}
err = app_host_init();
if (err) {
ESP_LOGE(TAG, "Failed to init host, err %d", err);
return;
}
err = esp_ble_audio_common_init(&init_info);
if (err) {
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
@@ -369,11 +315,16 @@ void app_main(void)
return;
}
err = ble_svc_gap_device_name_set("TMAP Peripheral");
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
}
ext_adv_start();
err = build_ext_adv_data();
if (err) {
return;
}
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
@@ -7,9 +7,6 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "tmap_peripheral.h"
@@ -322,6 +319,9 @@ int mcp_discover_mcs(void)
return -ENOTCONN;
}
/* New session — re-arm the initial read cascade. */
is_remote_read = false;
err = esp_ble_audio_mcc_discover_mcs(conn_handle, true);
if (err) {
ESP_LOGE(TAG, "Failed to discover MCS, err %d", err);
@@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "os/os_mbuf.h"
#include "esp_ble_audio_common_api.h"
#include "tmap_peripheral.h"
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
case BLE_GAP_EVENT_DISCONNECT:
case BLE_GAP_EVENT_ENC_CHANGE:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_MTU:
case BLE_GAP_EVENT_NOTIFY_RX:
case BLE_GAP_EVENT_NOTIFY_TX:
case BLE_GAP_EVENT_SUBSCRIBE:
esp_ble_audio_gatt_app_post_event(event->type, event);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
struct ble_gap_conn_desc desc = {0};
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
int app_host_init(void)
{
return 0;
}
int set_device_name(void)
{
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
}
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len)
{
struct ble_gap_ext_adv_params ext_params = {0};
struct os_mbuf *data = NULL;
int err;
ext_params.connectable = 1;
ext_params.scannable = 0;
ext_params.legacy_pdu = 0;
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
ext_params.tx_power = ADV_TX_POWER;
ext_params.sid = ADV_SID;
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
gap_event_cb, NULL);
if (err) {
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
return err;
}
data = os_msys_get_pkthdr(ext_len, 0);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
return -1;
}
err = os_mbuf_append(data, ext_data, ext_len);
if (err) {
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
os_mbuf_free_chain(data);
return err;
}
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
if (err) {
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
return err;
}
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
if (err) {
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
return err;
}
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -7,9 +7,6 @@
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "tmap_peripheral.h"
@@ -5,6 +5,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_log.h"
#include "sdkconfig.h"
@@ -22,11 +26,19 @@
#include "esp_ble_audio_media_proxy_api.h"
#include "esp_ble_audio_vocs_api.h"
#include "ble_audio_example_init.h"
#include "ble_audio_example_utils.h"
#define TAG "TMAP_PER"
#define CONN_HANDLE_INIT 0xFFFF
#define CONN_HANDLE_INIT 0xFFFF
#define LOCAL_DEVICE_NAME "TMAP Peripheral"
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
#define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
@@ -40,6 +52,12 @@
ESP_BLE_AUDIO_CONTEXT_TYPE_GAME | \
ESP_BLE_AUDIO_CONTEXT_TYPE_INSTRUCTIONAL)
int app_host_init(void);
int set_device_name(void);
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len);
uint16_t default_conn_handle_get(void);
int vcp_vol_renderer_init(void);

Some files were not shown because too many files have changed in this diff Show More