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
@@ -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