Merge branch 'idf/ble_audio_broadcast_assistant' into 'master'

feat(ble_audio): Add BAP Broadcast Assistant example

See merge request espressif/esp-idf!52480
This commit is contained in:
Island
2026-09-09 14:56:03 +08:00
39 changed files with 2538 additions and 285 deletions

View File

@@ -1564,36 +1564,40 @@ esp_err_t esp_ble_audio_bap_base_get_subgroup_codec_id(const esp_ble_audio_bap_b
}
esp_err_t esp_ble_audio_bap_base_get_subgroup_codec_data(const esp_ble_audio_bap_base_subgroup_t *subgroup,
uint8_t **data)
uint8_t **data, size_t *data_len)
{
int err;
int ret;
if (subgroup == NULL || data == NULL) {
if (subgroup == NULL || data == NULL || data_len == NULL) {
return ESP_ERR_INVALID_ARG;
}
err = bt_bap_base_get_subgroup_codec_data(subgroup, data);
if (err) {
ret = bt_bap_base_get_subgroup_codec_data(subgroup, data);
if (ret < 0) {
return ESP_FAIL;
}
*data_len = ret;
return ESP_OK;
}
esp_err_t esp_ble_audio_bap_base_get_subgroup_codec_meta(const esp_ble_audio_bap_base_subgroup_t *subgroup,
uint8_t **meta)
uint8_t **meta, size_t *meta_len)
{
int err;
int ret;
if (subgroup == NULL || meta == NULL) {
if (subgroup == NULL || meta == NULL || meta_len == NULL) {
return ESP_ERR_INVALID_ARG;
}
err = bt_bap_base_get_subgroup_codec_meta(subgroup, meta);
if (err) {
ret = bt_bap_base_get_subgroup_codec_meta(subgroup, meta);
if (ret < 0) {
return ESP_FAIL;
}
*meta_len = ret;
return ESP_OK;
}

View File

@@ -1036,8 +1036,21 @@ esp_err_t esp_ble_audio_bap_broadcast_assistant_discover(uint16_t conn_handle);
* to start scanning itself.
*
* @param conn_handle Connection handle.
* @param start_scan Start scanning if true. If false, the application should
* enable scan itself.
* @param start_scan Deliver scan results to the `scan` callback if true.
*
* @note The application always owns the scanner: start it first with the
* host's own GAP API (esp_ble_gap_start_ext_scan / ble_gap_disc), then
* call this. The BASS Remote Scan Started operation is written either
* way; start_scan only decides whether the Broadcast Audio
* Announcements that scanner reports are also parsed and handed to the
* `scan` member of esp_ble_audio_bap_broadcast_assistant_cb_t.
* Leave that member NULL when passing false. Note the callback carries
* no advertising data, so filtering on anything besides the Broadcast
* ID belongs in the application's scan handler.
*
* @note start_scan is therefore redundant with that member being set, and
* esp_ble_audio_bap_broadcast_assistant_scan_stop has no counterpart
* to it. It is kept only for API compatibility.
*
* @return ESP_OK on success, or an error code on failure.
*/
@@ -1284,24 +1297,30 @@ esp_err_t esp_ble_audio_bap_base_get_subgroup_codec_id(const esp_ble_audio_bap_b
/**
* @brief Get the codec configuration data of a subgroup.
*
* @note The data points into the BASE, and stays valid only as long as it does.
*
* @param subgroup The subgroup pointer.
* @param data Pointer that will point to the resulting codec configuration data.
* @param data_len The length of the @p data (may be 0) on success.
*
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t esp_ble_audio_bap_base_get_subgroup_codec_data(const esp_ble_audio_bap_base_subgroup_t *subgroup,
uint8_t **data);
uint8_t **data, size_t *data_len);
/**
* @brief Get the codec metadata of a subgroup.
*
* @note The metadata points into the BASE, and stays valid only as long as it does.
*
* @param subgroup The subgroup pointer.
* @param meta Pointer that will point to the resulting codec metadata.
* @param meta_len The length of the @p meta (may be 0) on success.
*
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t esp_ble_audio_bap_base_get_subgroup_codec_meta(const esp_ble_audio_bap_base_subgroup_t *subgroup,
uint8_t **meta);
uint8_t **meta, size_t *meta_len);
/**
* @brief Store subgroup codec data in a esp_ble_audio_codec_cfg_t.

View File

@@ -96,6 +96,8 @@ esp_err_t esp_ble_audio_gattc_disc_start(uint16_t conn_handle);
#define ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_PAST BT_LE_GAP_APP_EVENT_PA_SYNC_PAST
/*!< Audio GAP Periodic Sync Lost event */
#define ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_LOST BT_LE_GAP_APP_EVENT_PA_SYNC_LOST
/*!< Audio GAP Periodic Advertising Report event */
#define ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_RECV BT_LE_GAP_APP_EVENT_PA_SYNC_RECV
/*!< Audio GAP Connection Complete event */
#define ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT BT_LE_GAP_APP_EVENT_ACL_CONNECT
/*!< Audio GAP Disconnection Complete event */

View File

@@ -185,7 +185,7 @@ static const uint16_t ext_structs[] = {
sizeof(struct bt_bond_info),
};
#define LEA_VERSION (0x20260903)
#define LEA_VERSION (0x20260905)
struct lib_ext_cfgs {
/* BLE */
@@ -1086,8 +1086,6 @@ struct lib_ext_funcs {
/* Scan */
int (*_scan_cb_register)(struct bt_le_scan_cb *cb);
void (*_scan_cb_unregister)(struct bt_le_scan_cb *cb);
int (*_scan_start)(const struct bt_le_scan_param *param, void *cb);
int (*_scan_stop)(void);
int (*_pa_sync_cb_register)(struct bt_le_per_adv_sync_cb *cb);
int (*_pa_sync_cb_unregister)(struct bt_le_per_adv_sync_cb *cb);
int (*_pa_sync_get_info)(struct bt_le_per_adv_sync *per_adv_sync,
@@ -1300,8 +1298,6 @@ static const struct lib_ext_funcs ext_funcs = {
._scan_cb_register = (void *)bt_le_scan_cb_register,
._scan_cb_unregister = (void *)bt_le_scan_cb_unregister,
._scan_start = (void *)bt_le_scan_start,
._scan_stop = (void *)bt_le_scan_stop,
._pa_sync_cb_register = (void *)bt_le_per_adv_sync_cb_register,
._pa_sync_cb_unregister = (void *)bt_le_per_adv_sync_cb_unregister,
._pa_sync_get_info = (void *)bt_le_per_adv_sync_get_info,

View File

@@ -1297,7 +1297,7 @@ int bt_bap_stream_release(struct bt_bap_stream *stream);
* @param seq_num Packet Sequence number. This value shall be incremented for each call to this
* function and at least once per SDU interval for a specific channel.
*
* @return Bytes sent in case of success or negative value in case of error.
* @return 0 in case of success or negative value in case of error.
*/
int bt_bap_stream_send(struct bt_bap_stream *stream, struct net_buf *buf, uint16_t seq_num);
@@ -1315,7 +1315,7 @@ int bt_bap_stream_send(struct bt_bap_stream *stream, struct net_buf *buf, uint16
* @param ts Timestamp of the SDU in microseconds (us). This value can be used to transmit
* multiple SDUs in the same SDU interval in a CIG or BIG.
*
* @return Bytes sent in case of success or negative value in case of error.
* @return 0 in case of success or negative value in case of error.
*/
int bt_bap_stream_send_ts(struct bt_bap_stream *stream, struct net_buf *buf, uint16_t seq_num,
uint32_t ts);
@@ -1336,7 +1336,7 @@ int bt_bap_stream_send_ts(struct bt_bap_stream *stream, struct net_buf *buf, uin
* @retval 0 on success
* @retval -EINVAL if the stream is invalid, if the stream is not configured for sending or if it is
* not connected with a isochronous stream
* @retval Any return value from bt_iso_chan_get_tx_sync()
* @retval 0 on success, or any negative value from bt_iso_chan_get_tx_sync()
*/
int bt_bap_stream_get_tx_sync(struct bt_bap_stream *stream, struct bt_iso_tx_info *info);
@@ -2891,6 +2891,13 @@ struct bt_bap_broadcast_assistant_cb {
* Called when the scanner finds an advertiser that advertises the
* BT_UUID_BROADCAST_AUDIO UUID.
*
* Delivered only while bt_bap_broadcast_assistant_scan_start() has been
* called with start_scan set to true, and only for what the application's
* own scanner reports — this port never starts a scanner of its own. Leave
* the member NULL if the application already parses its own scan results.
* Note it carries no advertising data either, so filtering on anything
* besides the Broadcast ID has to happen in that scanner.
*
* @param info Advertiser information.
* @param broadcast_id 24-bit broadcast ID.
*/
@@ -2993,16 +3000,25 @@ int bt_bap_broadcast_assistant_discover(struct bt_conn *conn);
*
* This will let the Broadcast Audio Scan Service server know that this device
* is actively scanning for broadcast sources.
* The function can optionally also start scanning, if the caller does not want
* to start scanning itself.
*
* Scan results, if @p start_scan is true, is sent to the
* bt_bap_broadcast_assistant_scan_cb callback.
* Unlike upstream Zephyr, this port never touches the scanner: the application
* starts it first, through whichever GAP API its host provides, and then calls
* this. @p start_scan therefore no longer means "start scanning" — it only says
* whether the Broadcast Audio Announcements the application's scanner picks up
* should also be parsed and delivered to the `scan` member of
* @ref bt_bap_broadcast_assistant_cb. The Remote Scan Started operation is
* written to the server either way.
*
* That makes @p start_scan redundant with the `scan` member being set: true
* without a `scan` callback only costs the parsing, false with one means it
* never fires. It stays for API compatibility - upstream plans to drop it - and
* bt_bap_broadcast_assistant_scan_stop() has no counterpart to it, always
* taking delivery back down.
*
* @param conn Connection to the Broadcast Audio Scan Service server.
* Used to let the server know that we are scanning.
* @param start_scan Start scanning if true. If false, the application should
* enable scan itself.
* @param start_scan Deliver scan results to the `scan` callback if true.
* Either way the application owns the scanner itself.
* @retval 0 Success
* @retval -EINVAL @p conn is NULL of if @p conn has not done discovery
@@ -3018,6 +3034,11 @@ int bt_bap_broadcast_assistant_scan_start(struct bt_conn *conn,
/**
* @brief Stop remote scanning for BISes for a server.
*
* Writes the Remote Scan Stopped operation and, if this @p conn had asked for
* scan results, stops delivering them to the `scan` callback. The application's
* own scanner is left running — it started it, and this port never drives the
* scanner from here (see bt_bap_broadcast_assistant_scan_start()).
*
* @param conn Connection to the server.
* @retval 0 Success

View File

@@ -221,7 +221,7 @@ void bt_cap_stream_ops_register(struct bt_cap_stream *stream, struct bt_bap_stre
* function and at least once per SDU interval for a specific channel.
*
* @retval -EINVAL if stream object is NULL
* @retval Any return value from bt_bap_stream_send()
* @retval 0 on success, or any negative value from bt_bap_stream_send()
*/
int bt_cap_stream_send(struct bt_cap_stream *stream, struct net_buf *buf, uint16_t seq_num);
@@ -240,7 +240,7 @@ int bt_cap_stream_send(struct bt_cap_stream *stream, struct net_buf *buf, uint16
* multiple SDUs in the same SDU interval in a CIG or BIG.
*
* @retval -EINVAL if stream object is NULL
* @retval Any return value from bt_bap_stream_send()
* @retval 0 on success, or any negative value from bt_bap_stream_send()
*/
int bt_cap_stream_send_ts(struct bt_cap_stream *stream, struct net_buf *buf, uint16_t seq_num,
uint32_t ts);
@@ -256,7 +256,7 @@ int bt_cap_stream_send_ts(struct bt_cap_stream *stream, struct net_buf *buf, uin
* @param[out] info Transmit info object.
*
* @retval -EINVAL if stream object is NULL
* @retval Any return value from bt_bap_stream_get_tx_sync()
* @retval 0 on success, or any negative value from bt_bap_stream_get_tx_sync()
*/
int bt_cap_stream_get_tx_sync(struct bt_cap_stream *stream, struct bt_iso_tx_info *info);

View File

@@ -421,106 +421,6 @@ free:
bt_le_gap_event_free(qev);
}
int bt_le_bluedroid_scan_start(const struct bt_le_scan_param *param)
{
tBTM_STATUS status;
LOG_DBG("[B]ScanStart[%u][%u][%u]", param->type, param->interval, param->window);
#if USE_DIRECT_HCI
{
/* HCI LE Set Extended Scan Parameters (uncoded only):
* own_addr_type(1) | scan_filter_policy(1) | scanning_phys(1)
* | per-phy { scan_type(1) | scan_interval(2) | scan_window(2) } */
uint8_t cmd_params[8];
cmd_params[0] = BLE_ADDR_PUBLIC;
cmd_params[1] = 0; /* filter_policy: accept all */
cmd_params[2] = 0x01; /* scanning_phys: LE 1M only */
cmd_params[3] = param->type;
sys_put_le16(param->interval, cmd_params + 4);
sys_put_le16(param->window, cmd_params + 6);
status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_EXT_SCAN_PARAMS,
cmd_params, sizeof(cmd_params),
NULL, 0);
}
#else /* USE_DIRECT_HCI */
{
tBTM_BLE_EXT_SCAN_PARAMS scan_params = {0};
scan_params.own_addr_type = BLE_ADDR_PUBLIC;
scan_params.filter_policy = 0;
scan_params.scan_duplicate = 0;
scan_params.cfg_mask = BTM_BLE_GAP_EXT_SCAN_UNCODE_MASK;
scan_params.uncoded_cfg.scan_type = param->type;
scan_params.uncoded_cfg.scan_interval = param->interval;
scan_params.uncoded_cfg.scan_window = param->window;
bt_le_host_lock();
status = BTM_BleSetExtendedScanParams(&scan_params);
bt_le_host_unlock();
}
#endif /* USE_DIRECT_HCI */
if (status != BTM_SUCCESS) {
LOG_ERR("[B]SetScanParamsFail[%02x]", status);
return bluedroid_err_to_errno(status);
}
#if USE_DIRECT_HCI
{
/* HCI LE Set Extended Scan Enable:
* enable(1) | filter_duplicates(1) | duration(2) | period(2)
* duration=period=0 → continuous scan. */
uint8_t cmd_params[6] = { 1, 0, 0, 0, 0, 0 };
status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_EXT_SCAN_ENABLE,
cmd_params, sizeof(cmd_params),
NULL, 0);
}
#else /* USE_DIRECT_HCI */
bt_le_host_lock();
status = BTM_BleExtendedScan(true, 0, 0);
bt_le_host_unlock();
#endif /* USE_DIRECT_HCI */
if (status != BTM_SUCCESS) {
LOG_ERR("[B]ScanStartFail[%02x]", status);
}
return bluedroid_err_to_errno(status);
}
int bt_le_bluedroid_scan_stop(void)
{
tBTM_STATUS status;
LOG_DBG("[B]ScanStop");
#if USE_DIRECT_HCI
{
/* HCI LE Set Extended Scan Enable with enable=0; other fields
* are ignored by the controller per spec but must be present. */
uint8_t cmd_params[6] = { 0, 0, 0, 0, 0, 0 };
status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_EXT_SCAN_ENABLE,
cmd_params, sizeof(cmd_params),
NULL, 0);
}
#else /* USE_DIRECT_HCI */
bt_le_host_lock();
status = BTM_BleExtendedScan(false, 0, 0);
bt_le_host_unlock();
#endif /* USE_DIRECT_HCI */
if (status != BTM_SUCCESS) {
LOG_ERR("[B]ScanStopFail[%02x]", status);
}
return bluedroid_err_to_errno(status);
}
int bt_le_bluedroid_gap_init(void)
{
#if (BLE_50_EXTEND_SYNC_EN == TRUE)

View File

@@ -17,10 +17,6 @@ extern "C" {
void bt_le_bluedroid_gap_post_event(uint16_t event, void *param);
int bt_le_bluedroid_scan_start(const struct bt_le_scan_param *param);
int bt_le_bluedroid_scan_stop(void);
int bt_le_bluedroid_gap_init(void);
#ifdef __cplusplus

View File

@@ -273,46 +273,3 @@ void bt_le_nimble_gap_post_event(void *param)
free:
bt_le_gap_event_free(qev);
}
int bt_le_nimble_scan_start(const struct bt_le_scan_param *param, ble_gap_event_fn *cb)
{
struct ble_gap_ext_disc_params uncoded = {0};
int rc;
LOG_DBG("[N]ScanStart[%u][%u][%u]", param->type, param->interval, param->window);
uncoded.itvl = param->interval;
uncoded.window = param->window;
uncoded.passive = !param->type;
/* LE Audio sources broadcast via extended advertising; legacy
* ble_gap_disc would miss them. Uncoded-only mirrors the Bluedroid
* side which sets BTM_BLE_GAP_EXT_SCAN_UNCODE_MASK. */
rc = ble_gap_ext_disc(BLE_OWN_ADDR_PUBLIC, 0, 0, 0, 0, 0,
&uncoded, NULL, cb, NULL);
if (rc) {
LOG_ERR("[N]ScanStartFail[%d]", rc);
}
return nimble_err_to_errno(rc);
}
int bt_le_nimble_scan_stop(void)
{
int rc;
LOG_DBG("[N]ScanStop");
rc = ble_gap_disc_cancel();
if (rc && rc != BLE_HS_EALREADY) {
LOG_ERR("[N]ScanStopFail[%d]", rc);
}
/* EALREADY (not scanning, e.g. after privacy preemption): treat as success
* so bt_le_scan_stop clears a stale BT_DEV_SCANNING instead of locking out. */
if (rc == BLE_HS_EALREADY) {
rc = 0;
}
return nimble_err_to_errno(rc);
}

View File

@@ -19,10 +19,6 @@ extern "C" {
void bt_le_nimble_gap_post_event(void *param);
int bt_le_nimble_scan_start(const struct bt_le_scan_param *param, ble_gap_event_fn *cb);
int bt_le_nimble_scan_stop(void);
#ifdef __cplusplus
}
#endif

View File

@@ -413,7 +413,12 @@ static void handle_security_change_event_safe(struct bt_le_gap_app_param *param)
event.security_change.dst.val,
false);
if (gatt_conn == NULL) {
LOG_ERR("GapSecChgUnknownDev");
/* ACL disconnected between the BTC post and this handler — the same
* race as GapPastUnknownSrc, and routine on a link that drops while
* pairing. The producer leaves conn_handle at 0, which an app would
* read as its own connection, so invalidate it before dispatch. */
LOG_WRN("GapSecChgUnknownDev");
event.security_change.conn_handle = BT_CONN_HANDLE_INVALID;
goto end;
}

View File

@@ -431,7 +431,8 @@ int bt_le_per_adv_sync_report_recv_listener(uint16_t sync_handle,
per_adv_sync = bt_le_per_adv_sync_find(sync_handle);
if (per_adv_sync == NULL) {
LOG_ERR("PaSyncNotFound[%u]", sync_handle);
/* Reports queued before a terminate arrive after the sync is deleted. */
LOG_INF("PaSyncNotFound[%u]", sync_handle);
return -ENODEV;
}
@@ -463,7 +464,8 @@ void hci_le_biginfo_adv_report(struct net_buf *buf)
per_adv_sync = bt_le_per_adv_sync_find(evt->sync_handle);
if (per_adv_sync == NULL) {
LOG_ERR("PaSyncNotFound[%u]", evt->sync_handle);
/* Same post-teardown race as the PA report path above. */
LOG_INF("PaSyncNotFound[%u]", evt->sync_handle);
return;
}
@@ -489,47 +491,6 @@ void hci_le_biginfo_adv_report(struct net_buf *buf)
}
}
_LIB_ONLY
int bt_le_scan_start(const struct bt_le_scan_param *param, void *cb)
{
int err = 0;
if (atomic_test_bit(bt_dev.flags, BT_DEV_SCANNING) == false) {
#if CONFIG_BT_BLUEDROID_ENABLED
ARG_UNUSED(cb);
err = bt_le_bluedroid_scan_start(param);
#else
err = bt_le_nimble_scan_start(param, cb);
#endif
if (err == 0) {
atomic_set_bit(bt_dev.flags, BT_DEV_SCANNING);
}
} else {
err = -EALREADY;
}
return err;
}
_LIB_ONLY
int bt_le_scan_stop(void)
{
int err = 0;
if (atomic_test_bit(bt_dev.flags, BT_DEV_SCANNING)) {
#if CONFIG_BT_BLUEDROID_ENABLED
err = bt_le_bluedroid_scan_stop();
#else
err = bt_le_nimble_scan_stop();
#endif
if (err == 0) {
atomic_clear_bit(bt_dev.flags, BT_DEV_SCANNING);
}
}
return err;
}
static void past_features_set(void)
{
#if CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER

View File

@@ -933,7 +933,9 @@ int bt_iso_chan_disconnect(struct bt_iso_chan *chan);
* each call to this function and at least once per SDU
* interval for a specific channel.
*
* @return Number of octets sent in case of success or negative value in case of error.
* @note Unlike the upstream API, this returns 0 rather than the octet count.
*
* @return 0 in case of success or negative value in case of error.
*/
int bt_iso_chan_send(struct bt_iso_chan *chan, struct net_buf *buf, uint16_t seq_num);
@@ -957,7 +959,9 @@ int bt_iso_chan_send(struct bt_iso_chan *chan, struct net_buf *buf, uint16_t seq
* This value can be used to transmit multiple
* SDUs in the same SDU interval in a CIG or BIG.
*
* @return Number of octets sent in case of success or negative value in case of error.
* @note Unlike the upstream API, this returns 0 rather than the octet count.
*
* @return 0 in case of success or negative value in case of error.
*/
int bt_iso_chan_send_ts(struct bt_iso_chan *chan, struct net_buf *buf, uint16_t seq_num,
uint32_t ts);

View File

@@ -42,6 +42,7 @@ Application Examples
* **BAP (Basic Audio Profile)**
* :example:`bluetooth/esp_ble_audio/bap/broadcast_assistant` demonstrates how to act as a BAP Broadcast Assistant that scans for a broadcast source and adds it to a Scan Delegator over BASS.
* :example:`bluetooth/esp_ble_audio/bap/broadcast_sink` demonstrates how to act as a BAP Broadcast Sink that synchronizes to a broadcast source and receives BIS audio streams.
* :example:`bluetooth/esp_ble_audio/bap/broadcast_source` demonstrates how to act as a BAP Broadcast Source that creates a BIG and sends broadcast audio over BIS.
* :example:`bluetooth/esp_ble_audio/bap/unicast_client` demonstrates how to discover and connect to a unicast server and establish BAP unicast streams.

View File

@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(bap_broadcast_assistant)

View File

@@ -0,0 +1,269 @@
| Supported Targets | ESP32-H4 | ESP32-S31 |
| ----------------- | -------- | --------- |
# BAP Broadcast Assistant Example
(See the README.md file in the upper level `examples` directory for more information about examples.)
## Overview
This example implements the **BAP Broadcast Assistant** 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 never renders or transmits audio itself — it is the device that *tells someone else what to listen to*. In order: it scans for a connectable advertiser exposing the Broadcast Audio Scan Service (a Scan Delegator), connects and bonds with it, discovers BASS and reads back every Broadcast Receive State; it then scans for a Broadcast Audio Announcement, periodic-advertising-syncs to that source itself so it can decode the BASE, and finally writes a BASS **Add Source** naming the exact BIS indexes and metadata it found. If the sink takes up the offer to have that sync transferred (PAST) rather than scan for the source itself, the assistant hands it over. If the sink reports that the stream is encrypted and it has no key, the assistant pushes the Broadcast Code over BASS. The GAP device name is set to `BAP Broadcast Assistant`; the assistant is a scanner and initiator only and never advertises.
Design notes:
* Every stage advances from a GAP/GATT/BASS callback rather than blocking `app_main()`, matching the other examples in this directory.
* A sink is matched on BASS alone — BASS in the advertised UUID list is what identifies a Scan Delegator, and PACS is not required.
* An encrypted broadcast is handled end to end: a receive state reporting `BCODE_REQ` triggers `esp_ble_audio_bap_broadcast_assistant_set_broadcast_code()`, so the sink needs no pre-provisioned key.
* The sync used to read the BASE is reusable: a receive state reporting `INFO_REQ` triggers a Periodic Advertising Sync Transfer of that same handle, so the sink never has to find the source on air.
APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_gattc_disc_start`, `esp_ble_audio_bap_broadcast_assistant_register_cb` / `_discover` / `_read_recv_state` / `_scan_start` / `_scan_stop` / `_add_src` / `_set_broadcast_code`, and the BASE readers `esp_ble_audio_bap_base_get_base_from_ad` / `_get_subgroup_count` / `_foreach_subgroup` / `_subgroup_get_bis_indexes` / `_subgroup_codec_to_codec_cfg`.
Host-specific GAP plumbing (extended scan, ACL create, pairing, MTU, PA sync, PAST send) lives in `main/bluedroid/central.c` and `main/nimble/central.c`; `main.c` only sees the host-agnostic interface in `assistant.h`.
## Requirements
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* A **Broadcast Source** peer — the [broadcast_source](../broadcast_source) example
* A **Scan Delegator** peer — the [broadcast_sink](../broadcast_sink) example built with `EXAMPLE_SCAN_OFFLOAD=y` (it is off by default and the sink does not advertise without it), or [cap/acceptor](../../cap/acceptor), which always advertises but needs `TARGET_SINK_NAME` changed to match its name
> PAST is built in on both hosts (`CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER` / `CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER`). Because this example already holds a sync to the source — it syncs locally to read the BASE — the library offers `PA_Sync = "PAST available"` (0x01) in Add Source, and a delegator that takes the offer up gets that very sync handed over instead of scanning for the source itself. A delegator that would rather establish its own sync is free to ignore the offer; BASS § 3.1.1.4 allows either, and `bap/broadcast_sink` does exactly that.
## Configuration
The two peers and the Broadcast Code are `#define`s at the top of `main/main.c`:
| Macro | Default | Meaning |
| --- | --- | --- |
| `TARGET_SINK_NAME` | `BAP Broadcast Sink` | Only connect to a sink whose advertised name contains this substring. Set it to `CAP Acceptor` for [cap/acceptor](../../cap/acceptor), or empty to accept any BASS advertiser. |
| `TARGET_SOURCE_NAME` | `BAP Broadcast Source` | Only add a source whose advertised or broadcast name contains this substring. Empty accepts any Broadcast Audio Announcement. |
| `TARGET_BROADCAST_CODE` | `1234` | Sent with BASS Set Broadcast Code; must match what the source encrypts with. |
Matching is a case-insensitive substring test (`example_is_substring()`), so a shorter fragment works too. The defaults pair the example with this repo's [broadcast_sink](../broadcast_sink) and [broadcast_source](../broadcast_source) out of the box.
Two sdkconfig values bound what a BASE may contain: `CONFIG_BT_BAP_BASS_MAX_SUBGROUPS` (default 2) bounds the subgroup array written into Add Source, and `CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE` (default 60) bounds the per-subgroup metadata copy. A BASE that exceeds either is truncated with a warning rather than rejected.
### PAST combinations
Whether the SyncInfo is transferred over the ACL or found on air is a build-time
choice on **both** sides: `EXAMPLE_PAST` here and `EXAMPLE_PAST` in
[broadcast_sink](../broadcast_sink) (which in turn needs `EXAMPLE_SCAN_OFFLOAD`).
`PA_Sync` in Add Source states what the **assistant** can do, not what happens:
`0x01` means "I hold this sync and can transfer it", and the sink still chooses
whether to ask for it.
| Assistant | Sink | `PA_Sync` written | Sink's answer | SyncInfo comes from |
| --- | --- | --- | --- | --- |
| on | on | `0x01` offered | `INFO_REQ` | the ACL — transferred, **no scanning** |
| off | on | `0x02` not offered | establishes its own | the air — sink scans |
| on | off | `0x01` offered | declines, establishes its own | the air — sink scans |
| off | off | `0x02` not offered | establishes its own | the air — sink scans |
Declining is legal: BASS § 3.1.1.4 lets the server answer either way for `0x01`
and `0x02` alike. Rows 2 and 4 are indistinguishable from the sink's side —
they differ only in whether the assistant has PAST built in. Row 3 exists at all
because neither side checks the peer's PAST feature bit (`config_past_check` is
off in the library), so the offer goes out regardless of what the sink accepts.
### Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bonding, inherited from `../../common_components/example_init/ble_audio_example_init.c`. BASS characteristics require an encrypted link, so pairing is not optional here.
## 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, `bluetooth_init()`, `app_host_init()`, then `esp_ble_audio_common_init(&info)` with both `gap_cb` and `gatt_cb`. `esp_ble_audio_bap_broadcast_assistant_register_cb(&assistant_cbs)` registers the BASS client callbacks before `esp_ble_audio_common_start(NULL)`.
2. `scan_restart(SCAN_MODE_SINK)` starts a passive extended scan. `sink_data_cb` accepts a connectable advertiser whose 16-bit UUID list contains BASS and whose name matches `TARGET_SINK_NAME`.
3. On a hit the scanner is stopped first (NimBLE rejects `ble_gap_connect` during discovery) and `conn_create()` runs. `acl_connect``pairing_start()``security_change``exchange_mtu()``gatt_mtu_change``esp_ble_audio_gattc_disc_start()``gattc_disc_cmpl`. MTU-updated and discovery-complete can arrive in either order, so both set a flag and the second one calls `bass_discover()`.
4. `assistant_discover_cb` reports the receive state count; `read_next_recv_state()` walks every index through `esp_ble_audio_bap_broadcast_assistant_read_recv_state()`. A `NULL` state is an empty slot, not an error.
5. Once all states are read, `esp_ble_audio_bap_broadcast_assistant_scan_start(conn, false)` sends the BASS *Remote Scan Started* opcode. `start_scan` is `false` on purpose: the example drives its own extended scanner, and letting the library start one too would have the two fight over the controller.
6. `scan_restart(SCAN_MODE_SOURCE)` re-arms the scanner. `source_data_cb` matches the Broadcast Audio Announcement service data (24-bit Broadcast ID) plus `TARGET_SOURCE_NAME`. The advertiser address, SID, Broadcast ID and the **real** periodic interval are cached from the scan report.
7. `pa_sync_create()` syncs locally so the BASE can be read. `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_RECV` feeds each periodic advertising report to `esp_ble_audio_data_parse()`; `base_store_cb` calls `esp_ble_audio_bap_base_get_base_from_ad()` and, on the first BASE, walks the subgroups.
8. Per subgroup, `_subgroup_get_bis_indexes()` supplies `bis_sync` and `_subgroup_codec_to_codec_cfg()` decodes the codec configuration and metadata into buffers the caller supplies through `codec_cfg.data` / `.meta` — see the invariant below, a zeroed `codec_cfg` faults.
9. `add_source()` writes BASS Add Source with `pa_sync = true`, the measured `pa_interval`, and the filled subgroup array. On success `assistant_add_src_cb` sends *Remote Scan Stopped*.
10. `assistant_recv_state_cb` then tracks the sink's progress. `pa_sync_state == INFO_REQ` means the sink took up the PAST offer, and `send_past()` transfers the local sync handle with the Source_ID in the high octet of the service data. `encrypt_state == BCODE_REQ` triggers `send_broadcast_code()`; `BAD_CODE` retries once (latched, so a permanently-wrong code does not loop).
11. Teardown: `acl_disconnect` terminates the local PA sync, clears state and goes back to scanning for a sink. `pa_sync_lost` clears only the source-side state and, while the delegator link is up, resumes scanning for a source.
## Assistant Internals
The assistant is a linear pipeline — find sink, find source, describe source to sink — but it runs entirely out of callbacks and has to time-share **one** extended scanner between the two search phases. Everything below is that arbitration plus the latches that keep each stage from re-firing.
### End-to-end sequence
```
Source (BSRC) Assistant (BA) Delegator (BSNK)
| | |
| |<--- connectable adv ---| scan_mode = SINK
| | | sink_data_cb: BASS in UUID list
| |--- ACL connect+pair -->|
| |--- MTU + GATT disc --->|
| |--- BASS discover ----->| assistant_discover_cb
| |<-- Receive State x N --| read_next_recv_state
| |--- Remote Scan Start ->| scan_start(conn, false)
| | |
|--- Broadcast Audio ->| | scan_mode = SOURCE
| Announcement | | source_data_cb
|<===== PA sync =======| | pa_sync_create
|===== BASE report ===>| | base_store_cb -> subgroups[]
| |--- BASS Add Source --->| add_source()
| | addr/sid/id/ |
| | pa_interval/ |
| | bis_sync+metadata |
| |<-- Add Source rsp -----| assistant_add_src_cb
| |--- Remote Scan Stop -->|
| | |
| ------- delegator took PAST offer ------- |
| |<-- Receive State ------| pa = INFO_REQ
| |--- PAST (over ACL) --->| send_past()
| | |
| ------------ BIG encrypted ------------ |
| |<-- Receive State ------| enc = BCODE_REQ
| |--- BASS Set BCode ---->| send_broadcast_code
| | |
|<================== BIG sync ==================| delegator's own — we
| | | only supplied the code
| |<-- Receive State ------| enc = DECRYPTING, bis_sync != 0
```
### Scanner arbitration
`scan_mode` decides which report handler `ext_scan_recv()` dispatches to, and `SCAN_MODE_IDLE` is what keeps a stale report from starting a second connect or a second PA sync while the first is still in flight.
| `scan_mode` | Scanner | Handler | Leaves on |
| --- | --- | --- | --- |
| `SCAN_MODE_IDLE` | stopped | — | `scan_restart()` from a failure or teardown path |
| `SCAN_MODE_SINK` | running | `ext_scan_recv_sink` | connectable + BASS + name match → stop scan, `IDLE`, `conn_create()` |
| `SCAN_MODE_SOURCE` | running | `ext_scan_recv_source` | Broadcast ID + name match + `per_adv_itvl != 0` → stop scan, `IDLE`, `pa_sync_create()` |
Three things keep the single scanner from being contended:
- The BASS *Remote Scan Started* write passes `start_scan = false`, so the library never opens a scanner of its own.
- Both handlers call `ext_scan_stop()` **before** the operation they start, because NimBLE refuses `ble_gap_connect` while a discovery procedure is running.
- `ext_scan_recv_source` drops reports with `per_adv_itvl == 0` outright: without a periodic train there is no BASE to read and nothing for the delegator to sync to.
### Progress gates
| Variable | Set by | Cleared by |
| --- | --- | --- |
| `conn_handle` | `acl_connect` | `acl_disconnect` |
| `mtu_exchanged` | `gatt_mtu_change` (MTU ≥ `ESP_BLE_AUDIO_ATT_MTU_MIN`) | `acl_disconnect` |
| `disc_completed` | `gattc_disc_cmpl` | `acl_disconnect` |
| `recv_states_synced` | `read_next_recv_state` once the last index is read | `assistant_discover_cb`; `acl_disconnect` |
| `pa_syncing` | `ext_scan_recv_source` after `pa_sync_create()` | `pa_sync` (success and failure); `reset_source_state` |
| `sync_handle` | `pa_sync` (success) | `reset_source_state` |
| `base_received` | `base_store_cb`, only once `add_source()` returned 0 | `assistant_add_src_cb` on failure; `reset_source_state` |
| `subgroup_count` | `base_subgroup_cb`, one per subgroup | `base_store_cb` before each walk; `reset_source_state` |
| `code_attempts` | `send_broadcast_code()` | `assistant_recv_state_removed_cb`; `reset_source_state` |
`reset_source_state()` is the source-side half of teardown and runs from both `acl_disconnect` and `pa_sync_lost`; the delegator-side gates above it are cleared only by `acl_disconnect`.
### Key invariants
- **The local PA sync reads the BASE and is then reusable.** The assistant never creates a BIG sync and never renders audio. Reading the BASE is what lets Add Source name exact BIS indexes instead of `BIS_SYNC_NO_PREF`, and holding that sync is also what makes the library offer PAST — `past_available()` requires a sync to this `{address, SID}` plus PAST_SEND built in.
- **The PA sync outlives Add Source.** Nothing terminates it on success — `acl_disconnect` does, and `pa_sync_lost` reports it going away on its own. Holding it is what allows a retry from the next periodic advertising report.
- **Byte-order conversion touches the BASS parameter only.** GAP hands out addresses in the host's own order (Bluedroid MSB-first, NimBLE on-air/LSB-first) and `pa_sync_create()` wants those bytes unchanged; `addr_host_to_le()` / `addr_type_host_to_le()` in `main.c` convert only what goes into the Add Source PDU, which is always on-air order. Per BASS § 3.1.1.4 the type collapses to two values — `0x00` public (device *or* identity), `0x01` random (device *or* static identity).
- **`codec_cfg.data` and `.meta` are the caller's buffers.** In this port they are pointers, not arrays inside `esp_ble_audio_codec_cfg_t`, and `_subgroup_codec_to_codec_cfg()` memcpy's into both unconditionally — passing a zeroed struct stores to address 0. It also bounds-checks the decoded lengths against `CONFIG_BT_AUDIO_CODEC_CFG_MAX_DATA_SIZE` / `_MAX_METADATA_SIZE` rather than against the buffers it was given, so a smaller buffer would overflow instead of being rejected. `base_subgroup_cb()` therefore points `.meta` straight at this subgroup's `subgroup_meta[]` slot (which is exactly the Kconfig size, and is where Add Source wants the bytes anyway) and `.data` at a shared scratch buffer, since the codec configuration LTVs are decoded but not carried in Add Source.
- **The BASE is acted on once per source.** `pa_sync_recv` returns early unless the report's `sync_handle` matches and `base_received` is clear, so the periodic train's repetition does not re-issue Add Source.
- **A failed Add Source un-latches instead of giving up.** `assistant_add_src_cb` clears `base_received`, so the next periodic advertising report rebuilds the subgroups and writes again.
- **The Broadcast Code is latched at `BROADCAST_CODE_MAX_ATTEMPTS` (2).** The sink republishes its receive state on every change, so a permanently wrong code would otherwise loop forever: one send on `BCODE_REQ`, one retry on `BAD_CODE`, then silence. `recv_state_removed` resets the counter for the next source.
### Event handling
| Event | Action |
| --- | --- |
| `EXT_SCAN_RECV` | Dispatched by `scan_mode`; ignored in `SCAN_MODE_IDLE`. |
| `ACL_CONNECT` with `status != 0` | `scan_restart(SCAN_MODE_SINK)`. `ACL_DISCONNECT` never fires for a link that was never established, so the retry has to happen here. |
| `ACL_DISCONNECT` | Clear every delegator-side gate, terminate the local PA sync if one is held, `reset_source_state()`, back to `SCAN_MODE_SINK`. |
| `SECURITY_CHANGE` with `status != 0` | `security_failed_recover()` — the peer cleared its side of an existing bond, so encrypt-with-cached-key times out; drop the local bond and disconnect, and the next connection pairs fresh. |
| `PA_SYNC` failure | `scan_restart(SCAN_MODE_SOURCE)`; the source may still be there on the next pass. |
| `PA_SYNC_RECV` | Ignored unless `sync_handle` matches and `base_received` is clear. |
| Receive State with `pa_sync_state == INFO_REQ` | `send_past()` transfers the local sync handle over the ACL. Warns instead if the sync has since been lost. |
| `PA_SYNC_LOST` | Source-side state only. Re-scans for a source if the delegator link is still up; otherwise `acl_disconnect` has already re-armed the sink scan. |
| `GATT_MTU_CHANGE` / `GATTC_DISC_CMPL` | Either order. Each sets its flag and the second one calls `bass_discover()`. An MTU below `ESP_BLE_AUDIO_ATT_MTU_MIN` warns and stops the chain. |
## Expected Log
TAG: `BAP_BA`.
```
I (xxx) BAP_BA: Scanning for broadcast sink...
I (xxx) BAP_BA: Broadcast sink found: xx:xx:xx:xx:xx:xx
I (xxx) BAP_BA: Connected: handle 0 peer xx:xx:xx:xx:xx:xx
I (xxx) BAP_BA: Security: handle 0 level 2 bonded 1
I (xxx) BAP_BA: MTU updated: handle 0 mtu 517
I (xxx) BAP_BA: Service discovery complete: handle 0
I (xxx) BAP_BA: BASS discovered, 2 receive state(s)
I (xxx) BAP_BA: Receive state empty
I (xxx) BAP_BA: Remote Scan Started: ok
I (xxx) BAP_BA: Scanning for broadcast source...
I (xxx) BAP_BA: Broadcast source found: id 0x123456 sid 0 pa_interval 600
I (xxx) BAP_BA: PA synced: sync_handle 0, waiting for BASE
I (xxx) BAP_BA: BASE received (1 subgroup(s))
I (xxx) BAP_BA: Subgroup 0: bis_sync 0x00000006 meta_len 4
I (xxx) BAP_BA: Add Source sent: id 0x123456 sid 0 pa_interval 600 subgroups 1
I (xxx) BAP_BA: Add Source OK
I (xxx) BAP_BA: Remote Scan Stopped: ok
I (xxx) BAP_BA: Receive state: src_id 1 id 0x123456 pa 0x01 enc 0x00
I (xxx) BAP_BA: SyncInfo transferred for src_id 1
I (xxx) BAP_BA: Receive state: src_id 1 id 0x123456 pa 0x02 enc 0x01
I (xxx) BAP_BA: Broadcast code sent for src_id 1
I (xxx) BAP_BA: Set Broadcast Code: ok
I (xxx) BAP_BA: Receive state: src_id 1 id 0x123456 pa 0x02 enc 0x02
I (xxx) BAP_BA: subgroup 0 bis_sync 0x00000006
```
`enc` values are `0x01` = code required, `0x02` = decrypting, `0x03` = bad code. `pa 0x01` is *SyncInfo Request* (the sink wants PAST), `0x02` is PA synced. A sink that establishes its own sync goes straight to `0x02` and the `SyncInfo transferred` line does not appear.
On PA sync loss with the delegator still connected:
```
I (xxx) BAP_BA: PA sync lost: sync_handle 0 reason 0x...
I (xxx) BAP_BA: Scanning for broadcast source...
```
## Peer Pairing
Three boards:
| Board | Example |
| --- | --- |
| A | [broadcast_source](../broadcast_source) |
| B | [broadcast_sink](../broadcast_sink) with `EXAMPLE_SCAN_OFFLOAD=y` (or [cap/acceptor](../../cap/acceptor)) |
| C | this example |
Expected interaction:
1. A advertises the Broadcast Audio Announcement (Broadcast ID `0x123456`, name `"BAP Broadcast Source"`) and a periodic train carrying the BASE; its BIG is encrypted with `"1234"`.
2. C finds B by its advertised BASS UUID, connects, bonds, and discovers BASS.
3. C finds A, PA-syncs, decodes the BASE, and writes Add Source to B with the exact BIS bitfield.
4. B PA-syncs to A on its own, reports `BCODE_REQ`; C answers with the Broadcast Code.
5. B synchronizes to the BIG and starts receiving — on B you should see its BIS-sync request, `Broadcast code received`, and stream-started logs.
### Troubleshooting
* **`Add Source failed`** — most often the announced `pa_interval` or the advertiser address. This example takes the interval live from the scan report; sending `ESP_BLE_AUDIO_BAP_PA_INTERVAL_UNKNOWN` (`0xFFFF`) instead makes delegators that validate it reject the write. The address written into BASS is always on-air (LSB-first) order, which is a byte reversal of what a Bluedroid GAP event hands out — see `addr_host_to_le()` in `main.c`.
* **Sink stays at `enc 0x03`** — the Broadcast Code does not match the source. It is left-aligned and zero-padded to 16 octets over BASS; note this is the opposite convention to the HCI Create BIG / Create BIG Sync path, which takes the code byte-reversed. Only two sends are attempted per source, so fix the code and reconnect rather than waiting for another retry.
* **Nothing found in `SCAN_MODE_SINK`** — either the peer is not advertising (check that `bap/broadcast_sink` was built with `EXAMPLE_SCAN_OFFLOAD` enabled) or its name does not contain `TARGET_SINK_NAME`.
* **Nothing found in `SCAN_MODE_SOURCE`** — the source must be running a periodic advertising train; reports with `per_adv_itvl == 0` are dropped before the name and Broadcast ID are even looked at.
* **Add Source accepted, sink reports `pa 0x01` and stops** — `0x01` is *SyncInfo Request*: it took up the PAST offer and is waiting for the transfer. Look for `SyncInfo transferred for src_id N` right after; if it says the PA sync is gone instead, the local sync was lost before the sink asked.

View File

@@ -0,0 +1,11 @@
set(srcs "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)

View File

@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
menu "Example: Broadcast Assistant"
config EXAMPLE_PAST
bool "Transfer the local PA sync to the Scan Delegator (PAST)"
default y
select BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER if BT_BLUEDROID_ENABLED
select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER if BT_NIMBLE_ENABLED
help
Offer the sync used to read the BASE in Add Source (PA_Sync = 0x01)
and hand it over on request, so the delegator never scans for the
source. Off advertises 0x02 and leaves the delegator to sync itself;
the host PAST-send feature must then be off as well, which on NimBLE
(default y) means CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER=n.
endmenu

View File

@@ -0,0 +1,62 @@
/*
* 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"
#if CONFIG_BT_BLUEDROID_ENABLED
#define EXAMPLE_HOST_PAST_SEND CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER
#else
#define EXAMPLE_HOST_PAST_SEND CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER
#endif
/* The library offers PAST from the host feature alone, so a mismatch leaves the
* delegator waiting for a transfer this build never sends. */
#if !CONFIG_EXAMPLE_PAST && EXAMPLE_HOST_PAST_SEND
#error "EXAMPLE_PAST=n also needs the host PAST-send feature off \
(CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER / CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER)"
#endif
#define TAG "BAP_BA"
#define LOCAL_DEVICE_NAME "BAP Broadcast Assistant"
#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 24 /* 30ms */
#define CONN_LATENCY 0
#define CONN_TIMEOUT 500 /* 5s */
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_MAX_CE_LEN 0xFFFF
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
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);
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 pa_past_transfer(uint16_t conn_handle, uint16_t sync_handle, uint8_t src_id);

View File

@@ -0,0 +1,216 @@
/*
* 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 "assistant.h"
static SemaphoreHandle_t scan_sem;
static esp_bt_status_t scan_op_status;
static esp_bd_addr_t peer_bda;
#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;
/* 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 and
* the PERIODIC_ADV_* events are forwarded by adapter's BTA path, don't
* re-post them 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));
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])
{
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);
}
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));
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);
}
int pa_past_transfer(uint16_t conn_handle, uint16_t sync_handle, uint8_t src_id)
{
(void)conn_handle;
/* Source_ID in the high octet of Service_Data; addressed by the
* delegator's BD address, since BTM dispatches over its ACL. */
return esp_ble_gap_periodic_adv_sync_trans(peer_bda, (uint16_t)src_id << 8,
sync_handle);
}

View File

@@ -0,0 +1,5 @@
dependencies:
example_init:
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
example_utils:
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
/*
* 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 "assistant.h"
#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:
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;
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;
}
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);
}
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);
}
int pa_past_transfer(uint16_t conn_handle, uint16_t sync_handle, uint8_t src_id)
{
/* Source_ID in the high octet of the service data. */
return ble_gap_periodic_adv_sync_transfer(sync_handle, conn_handle,
(uint16_t)src_id << 8);
}

View File

@@ -0,0 +1,44 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=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_BAP_BROADCAST_ASSISTANT=y
CONFIG_BT_BAP_BROADCAST_ASSISTANT_RECV_STATE_COUNT=2
CONFIG_BT_BAP_BASS_MAX_SUBGROUPS=2
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_FREERTOS_HZ=1000
# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the
# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4).
# CONFIG_SPIRAM=y
# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y
# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y
# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y
# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y
# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over
# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set
# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads.
# CONFIG_BLE_LOG_ENABLED=y
# CONFIG_BT_LOG_CRITICAL_ONLY=y
# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y
# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y
# CONFIG_BLE_LOG_PRPH_UART_DMA=y
# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0
# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000
# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58

View File

@@ -0,0 +1,6 @@
# Override some defaults so BT stack is enabled
# by default in this example
CONFIG_IDF_TARGET="esp32h4"
CONFIG_BT_LE_ISO_SUPPORT=y

View File

@@ -0,0 +1,6 @@
# Override some defaults so BT stack is enabled
# by default in this example
CONFIG_IDF_TARGET="esp32s31"
CONFIG_BT_LE_ISO_SUPPORT=y

View File

@@ -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_LOG_LEVEL_WARNING=y
CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER=y

View File

@@ -9,14 +9,17 @@
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.
With `EXAMPLE_SCAN_OFFLOAD` enabled (off by default) it instead advertises connectable as a **Scan Delegator** and never picks a source itself, waiting for a [broadcast_assistant](../broadcast_assistant) to connect over BASS and tell it which broadcast to receive.
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, 4060 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, `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`.
`scan_stop()` halts the extended scanner once PA sync is established — BASE/BIGInfo arrive over the PA channel. Self-scan builds re-arm it from `pa_sync_lost()`; offload builds arm it only for the duration of a requested sync, from `pa_sync_req_cb()`. 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
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* A peer running the [broadcast_source](../broadcast_source) example (or another BAP Broadcast Source advertising the matching name)
* Optionally, a [broadcast_assistant](../broadcast_assistant) peer to drive sync over BASS
## Configuration
@@ -26,7 +29,39 @@ Open menuconfig:
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source. The example always runs both: it scans for `"BAP Broadcast Source"` directly and registers a BASS scan delegator so a Broadcast Assistant could also drive sync (PAST is rejected — see `pa_sync_req_cb`).
One build-time option, under **Example: Broadcast Sink**:
| Option | Default | Meaning |
| --- | --- | --- |
| `EXAMPLE_SCAN_OFFLOAD` | `n` | Advertise connectable (name `"BAP Broadcast Sink"`, BASS + PACS UUIDs) and let a Broadcast Assistant pick the source, instead of scanning for one. Enable it to pair with [broadcast_assistant](../broadcast_assistant). |
| `EXAMPLE_PAST` | `y` | Take up an Assistant's offer to transfer its own periodic sync, instead of establishing one locally. Needs `EXAMPLE_SCAN_OFFLOAD`; see the table below. |
The two modes are exclusive, because *source selection* is what gets offloaded. A self-found source is registered as a local receive state by `bt_bap_broadcast_sink_create()`, and the Assistant's Add Source for that same `{address type, SID, Broadcast ID}` would then be rejected as a duplicate (`0xFC`, BAP § 6.5.4). In offload builds without PAST the scanner still runs, but only between `pa_sync_req_cb()` and the sync being established: without a transfer the SyncInfo exists only in the source's extended advertising, so it has to be received on air. With `EXAMPLE_PAST=y` and an Assistant that offers it, the scanner never runs at all. The BASS scan delegator is registered in both modes; with `EXAMPLE_SCAN_OFFLOAD=n` nothing drives it.
`EXAMPLE_PAST` selects the host's PAST symbol (`BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER` on NimBLE, `BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER` on Bluedroid), which is what makes the library report PAST as available to an Assistant. Per BASS 3.1.1.4 a server may answer `PA_Sync = 0x01` either by requesting SyncInfo or by establishing the sync itself; `EXAMPLE_PAST` picks which. Either way an Assistant that offers PAST is served rather than rejected.
### PAST combinations
Whether the SyncInfo is transferred over the ACL or found on air is a build-time
choice on **both** sides: `EXAMPLE_PAST` here (which needs `EXAMPLE_SCAN_OFFLOAD`) and `EXAMPLE_PAST` in
[broadcast_assistant](../broadcast_assistant).
`PA_Sync` in Add Source states what the **assistant** can do, not what happens:
`0x01` means "I hold this sync and can transfer it", and the sink still chooses
whether to ask for it.
| Assistant | Sink | `PA_Sync` written | Sink's answer | SyncInfo comes from |
| --- | --- | --- | --- | --- |
| on | on | `0x01` offered | `INFO_REQ` | the ACL — transferred, **no scanning** |
| off | on | `0x02` not offered | establishes its own | the air — sink scans |
| on | off | `0x01` offered | declines, establishes its own | the air — sink scans |
| off | off | `0x02` not offered | establishes its own | the air — sink scans |
Declining is legal: BASS § 3.1.1.4 lets the server answer either way for `0x01`
and `0x02` alike. Rows 2 and 4 are indistinguishable from the sink's side —
they differ only in whether the assistant has PAST built in. Row 3 exists at all
because neither side checks the peer's PAST feature bit (`config_past_check` is
off in the library), so the offer goes out regardless of what the sink accepts.
### Security & Pairing
@@ -59,12 +94,13 @@ For `esp32s31`, replace the chip overlay accordingly.
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. `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.
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). With `EXAMPLE_SCAN_OFFLOAD=n`, `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`.
With `EXAMPLE_SCAN_OFFLOAD` the same `pa_sync_create()` is instead reached from `pa_sync_req_cb()`, which arms the scanner first and then syncs using the address, SID and Broadcast_ID the Assistant wrote into the receive state. With `EXAMPLE_PAST` and `past_available`, that whole branch is skipped: `pa_sync_with_past()` arms the PAST receive, the receive state goes to `INFO_REQ`, and the sync arrives on `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_PAST` instead. The address there is on-air (LSB-first) order and is byte-reversed for Bluedroid by `addr_from_le()`. A failure reports `ESP_BLE_AUDIO_BAP_PA_STATE_FAILED` back over BASS and releases `req_recv_state`, so the Assistant can retry or pick another source.
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`.
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` clears `stream_started`; the sink itself is deleted from `broadcast_sink_stopped_cb` (the `stopped` sink callback), which runs once BASS has cleared `bis_sync` — deleting from `stream_stopped_cb` would race `rem_src` while `bis_sync` is still non-zero. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner.
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` clears `stream_started`; the sink itself is deleted from `broadcast_sink_stopped_cb` (the `stopped` sink callback), which runs once BASS has cleared `bis_sync` — deleting from `stream_stopped_cb` would race `rem_src` while `bis_sync` is still non-zero. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and — in self-scan builds — restarts the scanner.
## Expected Log
@@ -82,7 +118,7 @@ Per-stream RX metrics are then emitted under the `BAP_BSNK` tag, name `SNK #<idx
```
I (xxx) BAP_BSNK: Receive state updated, pa_sync 0x... encrypt 0x...
I (xxx) BAP_BSNK: Received request to sync to PA (PAST {not }available): ...
I (xxx) BAP_BSNK: Assistant requests PA sync to 0x... (PAST {not }available)
I (xxx) BAP_BSNK: Broadcast code received
I (xxx) BAP_BSNK: BIS sync req: broadcast_id 0x... BIS mask 0x... subgroup mask 0x... (...)
I (xxx) BAP_BSNK: Received request to terminate PA sync
@@ -106,4 +142,4 @@ Run [broadcast_source](../broadcast_source/) on a second board. Expected interac
3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`.
4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code.
5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics.
6. Stopping the source (or losing PA sync) tears down the BIG; `broadcast_sink_stopped_cb` then deletes the sink (after BASS clears `bis_sync`) and scanning resumes.
6. Stopping the source (or losing PA sync) tears down the BIG; `broadcast_sink_stopped_cb` then deletes the sink (after BASS clears `bis_sync`) and, in self-scan builds, scanning resumes.

View File

@@ -6,9 +6,25 @@ menu "Example: Broadcast Sink"
config EXAMPLE_SCAN_OFFLOAD
bool "Whether to wait for a Broadcast Assistant"
select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER
help
If set to true, the example will start advertising connectable
for Broadcast Assistants.
If set to true, the example advertises connectable for Broadcast
Assistants and never picks a source itself — an Assistant selects
one and drives PA / BIS sync over BASS. The scanner only runs
while a requested periodic sync is being established.
If set to false, the example scans for TARGET_DEVICE_NAME and
syncs on its own, without advertising.
config EXAMPLE_PAST
bool "Accept a transferred PA sync from the Assistant (PAST)"
depends on EXAMPLE_SCAN_OFFLOAD
default y
select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER if BT_NIMBLE_ENABLED
select BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER if BT_BLUEDROID_ENABLED
help
When the Assistant offers PA_Sync = 0x01, answer SyncInfo Request
and let it hand its sync over, so no scanning is needed. Off means
always establishing the sync locally instead, which BASS 3.1.1.4
allows for either request value.
endmenu

View File

@@ -15,6 +15,8 @@
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_ble_audio_common_api.h"
#include "scan.h"
static SemaphoreHandle_t scan_sem;
@@ -53,7 +55,44 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event,
xSemaphoreGive(scan_sem);
break;
/* PA sync / ext adv events: forwarded by adapter's BTA path, not here. */
#if CONFIG_EXAMPLE_PAST
/* pa_sync_with_past() waits on this; without it WAIT_API blocks forever. */
case ESP_GAP_BLE_SET_PAST_PARAMS_COMPLETE_EVT:
scan_op_status = param->set_past_params.status;
xSemaphoreGive(scan_sem);
break;
#endif /* CONFIG_EXAMPLE_PAST */
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
scan_op_status = param->ext_adv_set_params.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
scan_op_status = param->ext_adv_data_set.status;
xSemaphoreGive(scan_sem);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
scan_op_status = param->ext_adv_start.status;
xSemaphoreGive(scan_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. */
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_ble_audio_gap_app_post_event(event, param);
break;
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
default:
break;
}
@@ -115,3 +154,64 @@ int pa_sync_terminate(uint16_t sync_handle)
{
return esp_ble_gap_periodic_adv_sync_terminate(sync_handle);
}
#if CONFIG_EXAMPLE_PAST
int pa_sync_with_past(uint16_t conn_handle, const uint8_t addr[6])
{
/* mode 0x02 = sync and report; the report stream comes with it, so no
* separate enable. Addressed by the Assistant's BD address, since BTM
* dispatches over its ACL. */
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,
};
esp_bd_addr_t peer_addr;
(void)conn_handle;
memcpy(peer_addr, addr, sizeof(peer_addr));
WAIT_API(esp_ble_gap_set_periodic_adv_sync_trans_params(peer_addr, &params));
/* Sync handle arrives later on the PA_SYNC_PAST event. */
return ESP_OK;
}
#endif /* CONFIG_EXAMPLE_PAST */
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
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 },
};
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 ESP_OK;
}
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */

View File

@@ -14,6 +14,14 @@
#include "esp_log.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#if CONFIG_BT_BLUEDROID_ENABLED
#include "esp_bt_defs.h"
#else
#include "nimble/ble.h"
#endif
#include "esp_ble_audio_lc3_defs.h"
#include "esp_ble_audio_bap_api.h"
#include "esp_ble_audio_pacs_api.h"
@@ -48,6 +56,9 @@ static uint16_t sync_handle = PA_SYNC_HANDLE_INIT;
static bool pa_syncing;
static uint16_t conn_handle = CONN_HANDLE_INIT;
#if CONFIG_EXAMPLE_PAST
static uint8_t peer_addr[6];
#endif /* CONFIG_EXAMPLE_PAST */
static volatile bool stream_started;
static volatile bool base_received;
static uint32_t bis_index_bitfield;
@@ -81,6 +92,26 @@ static esp_ble_audio_pacs_cap_t cap = {
.codec_cap = &codec_cap,
};
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
/* Connectable advertising so a Broadcast Assistant can find us and drive sync
* over BASS. The Assistant matches on the BASS UUID; PACS is advertised too so
* it can check our capabilities before picking a source. */
static uint8_t ext_adv_data[] = {
/* Flags */
0x02, EXAMPLE_AD_TYPE_FLAGS, (EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR),
/* Incomplete List of 16-bit Service UUIDs */
0x05, EXAMPLE_AD_TYPE_UUID16_SOME,
(ESP_BLE_AUDIO_UUID_BASS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_BASS_VAL >> 8) & 0xFF),
(ESP_BLE_AUDIO_UUID_PACS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_PACS_VAL >> 8) & 0xFF),
/* Service Data - Broadcast Audio Scan Service */
0x03, EXAMPLE_AD_TYPE_SERVICE_DATA16,
(ESP_BLE_AUDIO_UUID_BASS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_BASS_VAL >> 8) & 0xFF),
/* Complete Device Name */
0x13, EXAMPLE_AD_TYPE_NAME_COMPLETE,
'B', 'A', 'P', ' ', 'B', 'r', 'o', 'a', 'd', 'c', 'a', 's', 't', ' ', 'S', 'i', 'n', 'k',
};
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
static void recv_state_updated_cb(esp_ble_conn_t *conn,
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
{
@@ -96,28 +127,114 @@ static void recv_state_updated_cb(esp_ble_conn_t *conn,
}
}
/* recv_state->addr carries the on-air (LSB-first) order the BASS PDU used;
* pa_sync_create() takes the active host's own order — MSB-first under
* Bluedroid, on-air under NimBLE. */
static void addr_le_to_host(uint8_t dst[6], const uint8_t src[6])
{
#if CONFIG_BT_BLUEDROID_ENABLED
for (size_t i = 0; i < 6; i++) {
dst[i] = src[5 - i];
}
#else
memcpy(dst, src, 6);
#endif
}
/* Likewise the address type: BASS 3.1.1.4 carries only 0x00 public (device or
* identity) and 0x01 random (device or static identity); pa_sync_create() wants
* the host's own enum. */
static uint8_t addr_type_le_to_host(uint8_t type)
{
#if CONFIG_BT_BLUEDROID_ENABLED
return (type == BT_ADDR_LE_PUBLIC ||
type == BT_ADDR_LE_PUBLIC_ID) ? BLE_ADDR_TYPE_PUBLIC : BLE_ADDR_TYPE_RANDOM;
#else
return (type == BT_ADDR_LE_PUBLIC ||
type == BT_ADDR_LE_PUBLIC_ID) ? BLE_ADDR_PUBLIC : BLE_ADDR_RANDOM;
#endif
}
static int pa_sync_req_cb(esp_ble_conn_t *conn,
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
bool past_available, uint16_t pa_interval)
{
ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailable): %u",
past_available ? "" : "not ",
recv_state->pa_sync_state);
uint8_t addr[6];
int err;
req_recv_state = recv_state;
ESP_LOGI(TAG, "Assistant requests PA sync to 0x%06lx (PAST %savailable)",
(unsigned long)recv_state->broadcast_id,
past_available ? "" : "not ");
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED ||
recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ ||
sync_handle != PA_SYNC_HANDLE_INIT) {
/* Already syncing */
ESP_LOGW(TAG, "Rejecting PA sync request");
return -EALREADY;
/* BASS 3.1.1.4 lets the server answer either way for 0x01 and 0x02 alike:
* request SyncInfo, or establish the sync itself. Rejecting is not an
* option, so EXAMPLE_PAST only picks which of the two we take. */
if (pa_syncing || sync_handle != PA_SYNC_HANDLE_INIT) {
if (recv_state->broadcast_id == broadcaster_broadcast_id) {
/* Already on the requested train — nothing left to do. */
req_recv_state = recv_state;
return 0;
}
ESP_LOGW(TAG, "Busy with 0x%06lx, rejecting",
(unsigned long)broadcaster_broadcast_id);
return -EBUSY;
}
/* Drive the sync straight off the receive state instead of waiting to
* stumble across the source in our own scan: ext_scan_recv() stops
* creating syncs the moment req_recv_state is set, so nothing else would.
*/
addr_le_to_host(addr, recv_state->addr.a.val);
#if CONFIG_EXAMPLE_PAST
if (past_available) {
ESP_LOGW(TAG, "Currently not support PAST");
return -ENOTSUP;
err = pa_sync_with_past(conn_handle, peer_addr);
if (err) {
ESP_LOGE(TAG, "Failed to enable PAST receive, err %d", err);
return -EIO;
}
/* Ask for the transfer; the sync arrives on PA_SYNC_PAST. */
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 -EIO;
}
ESP_LOGI(TAG, "Waiting for SyncInfo transfer...");
goto pending;
}
#endif /* CONFIG_EXAMPLE_PAST */
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
/* Without a transfer the SyncInfo only exists on air, and this controller
* needs the scanner up to pick it out; pa_sync() takes it back down. */
err = ext_scan_start();
if (err) {
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
return -EIO;
}
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
err = pa_sync_create(addr_type_le_to_host(recv_state->addr.type), addr,
recv_state->adv_sid);
if (err) {
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
ext_scan_stop();
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
return -EIO;
}
#if CONFIG_EXAMPLE_PAST
pending:
#endif /* CONFIG_EXAMPLE_PAST */
req_recv_state = recv_state;
broadcaster_broadcast_id = recv_state->broadcast_id;
pa_syncing = true;
return 0;
}
@@ -334,6 +451,13 @@ static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
}
broadcast_sink = NULL;
#if !CONFIG_EXAMPLE_SCAN_OFFLOAD
/* No Assistant owns this source, so the receive state was ours and the
* delete took it with it. Drop the pointer before pa_sync_lost() reads
* a src_id that no longer exists. */
req_recv_state = NULL;
#endif /* !CONFIG_EXAMPLE_SCAN_OFFLOAD */
}
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
@@ -457,7 +581,13 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
return;
}
if (pa_syncing == false && req_recv_state == NULL) {
/* A connected Assistant owns source selection: without this gate, the
* synthesized PA_SYNC_LOST that follows its terminate request would clear
* req_recv_state and we would immediately re-sync the very train it just
* told us to drop. Anything already streaming keeps running — the gate
* only stops us from starting something new. */
if (pa_syncing == false && req_recv_state == NULL &&
conn_handle == CONN_HANDLE_INIT) {
broadcaster_broadcast_id = sr.broadcast_id;
err = pa_sync_create(event->ext_scan_recv.addr.type,
@@ -481,6 +611,25 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event)
if (event->pa_sync.status) {
ESP_LOGE(TAG, "PA sync failed, status %d", event->pa_sync.status);
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
/* Nothing left for the scanner armed by pa_sync_req_cb() to do. */
if (event->type == ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC) {
rc = ext_scan_stop();
if (rc) {
ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc);
}
}
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
/* Report it so the assistant can retry or pick another source. Clearing
* req_recv_state also lets our own scan take another run at it —
* ext_scan_recv() is gated on it being NULL. */
if (req_recv_state != NULL) {
esp_ble_audio_bap_scan_delegator_set_pa_state(req_recv_state->src_id,
ESP_BLE_AUDIO_BAP_PA_STATE_FAILED);
req_recv_state = NULL;
}
return;
}
@@ -488,13 +637,13 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Broadcast source PA synced, creating Broadcast Sink");
/* PA sync is established; the BASE / BIGInfo reports will arrive
* 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 = ext_scan_stop();
if (rc) {
ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc);
/* BASE / BIGInfo arrive over the PA channel from here on. A transferred
* sync never started a scanner, so there is nothing to take down. */
if (event->type == ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC) {
rc = ext_scan_stop();
if (rc) {
ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc);
}
}
err = esp_ble_audio_bap_broadcast_sink_create(event->pa_sync.sync_handle,
@@ -512,6 +661,14 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
if (sync_handle == event->pa_sync_lost.sync_handle) {
/* Publish it before dropping the pointer. A Modify Source asking us to
* sync again is ignored while the receive state still reads SYNCED, and
* set_pa_state() is also what re-arms the pa_sync_req callback. */
if (req_recv_state != NULL) {
esp_ble_audio_bap_scan_delegator_set_pa_state(
req_recv_state->src_id, ESP_BLE_AUDIO_BAP_PA_STATE_NOT_SYNCED);
}
sync_handle = PA_SYNC_HANDLE_INIT;
pa_syncing = false;
base_received = false;
@@ -531,10 +688,47 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
broadcast_sink = NULL;
}
#if !CONFIG_EXAMPLE_SCAN_OFFLOAD
ext_scan_start();
#endif /* !CONFIG_EXAMPLE_SCAN_OFFLOAD */
}
}
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
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_adv_start(ext_adv_data, sizeof(ext_adv_data));
return;
}
ESP_LOGI(TAG, "Broadcast Assistant connected: handle %u",
event->acl_connect.conn_handle);
#if CONFIG_EXAMPLE_PAST
memcpy(peer_addr, event->acl_connect.dst.val, sizeof(peer_addr));
#endif /* CONFIG_EXAMPLE_PAST */
/* base_recv_cb() only falls back to BIS_SYNC_NO_PREF while nobody is
* driving us over BASS — without this the fallback would keep overwriting
* whatever the assistant asked for in bis_sync_req_cb(). */
conn_handle = event->acl_connect.conn_handle;
}
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
{
ESP_LOGI(TAG, "Broadcast Assistant disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
conn_handle = CONN_HANDLE_INIT;
/* Extended advertising stops on connect; re-arm so the assistant (or
* another one) can come back. */
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
}
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
{
switch (event->type) {
@@ -542,11 +736,22 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
ext_scan_recv(event);
break;
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC:
#if CONFIG_EXAMPLE_PAST
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_PAST:
#endif /* CONFIG_EXAMPLE_PAST */
pa_sync(event);
break;
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_LOST:
pa_sync_lost(event);
break;
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT:
acl_connect(event);
break;
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
acl_disconnect(event);
break;
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
default:
break;
}
@@ -624,5 +829,23 @@ void app_main(void)
return;
}
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
err = set_device_name();
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
}
err = ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
if (err) {
ESP_LOGE(TAG, "Failed to start advertising, err %d", err);
return;
}
#else
/* Scanning is the thing being offloaded, so it only runs when nobody else
* is doing it for us. Self-syncing would register a local receive state
* for the source and the Assistant's Add Source for the same one would
* come back as a duplicate. */
ext_scan_start();
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
}

View File

@@ -11,6 +11,11 @@
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
#include "host/ble_store.h"
#include "services/gap/ble_svc_gap.h"
#include "os/os_mbuf.h"
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
#include "esp_ble_audio_common_api.h"
@@ -26,6 +31,27 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg)
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
esp_ble_audio_gap_app_post_event(event->type, event);
break;
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
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;
}
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
default:
break;
}
@@ -84,7 +110,82 @@ int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
gap_event_cb, NULL);
}
#if CONFIG_EXAMPLE_PAST
int pa_sync_with_past(uint16_t conn_handle, const uint8_t addr[6])
{
(void)addr;
struct ble_gap_periodic_sync_params params = {
.skip = PA_SYNC_SKIP,
.sync_timeout = PA_SYNC_TIMEOUT,
};
return ble_gap_periodic_adv_sync_receive(conn_handle, &params,
gap_event_cb, NULL);
}
#endif /* CONFIG_EXAMPLE_PAST */
int pa_sync_terminate(uint16_t sync_handle)
{
return ble_gap_periodic_adv_sync_terminate(sync_handle);
}
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
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;
}
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */

View File

@@ -12,12 +12,19 @@
#define TAG "BAP_BSNK"
#define LOCAL_DEVICE_NAME "BAP Broadcast Sink"
#define SCAN_INTERVAL 160 /* 100ms */
#define SCAN_WINDOW 160 /* 100ms */
#define PA_SYNC_SKIP 0
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
#define ADV_HANDLE 0
#define ADV_SID 0
#define ADV_TX_POWER 127
#define ADV_INTERVAL_MS 200
int app_host_init(void);
int ext_scan_start(void);
@@ -25,3 +32,13 @@ 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);
#if CONFIG_EXAMPLE_SCAN_OFFLOAD
int set_device_name(void);
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len);
#endif /* CONFIG_EXAMPLE_SCAN_OFFLOAD */
#if CONFIG_EXAMPLE_PAST
int pa_sync_with_past(uint16_t conn_handle, const uint8_t addr[6]);
#endif /* CONFIG_EXAMPLE_PAST */

View File

@@ -11,6 +11,14 @@
#include <stdbool.h>
#include <errno.h>
#include "sdkconfig.h"
#if CONFIG_BT_BLUEDROID_ENABLED
#include "esp_bt_defs.h"
#else
#include "nimble/ble.h"
#endif
#include "cap_acceptor.h"
#if CONFIG_EXAMPLE_SCAN_SELF
@@ -63,21 +71,6 @@ static inline bool flag_test_and_set(uint8_t bit)
return was;
}
/* The audio stack keeps addresses on-air (LSB-first) in bt_addr_le_t, Bluedroid takes
* and reports them MSB-first, NimBLE on-air. Convert whenever one meets the other; the
* reversal is its own inverse, so this serves both directions.
*/
static void addr_order_copy(uint8_t dst[6], const uint8_t src[6])
{
#if CONFIG_BT_BLUEDROID_ENABLED
for (size_t i = 0; i < 6; i++) {
dst[i] = src[5 - i];
}
#else
memcpy(dst, src, 6);
#endif
}
static struct broadcast_sink {
esp_ble_audio_cap_stream_t cap_streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state;
@@ -585,6 +578,21 @@ static void recv_state_updated_cb(esp_ble_conn_t *conn,
}
}
/* The audio stack keeps addresses on-air (LSB-first), Bluedroid takes and reports
* them MSB-first, NimBLE on-air. Convert whenever one meets the other; the
* reversal is its own inverse, so this serves both directions.
*/
static void addr_order_copy(uint8_t dst[6], const uint8_t src[6])
{
#if CONFIG_BT_BLUEDROID_ENABLED
for (size_t i = 0; i < 6; i++) {
dst[i] = src[5 - i];
}
#else
memcpy(dst, src, 6);
#endif
}
static int pa_sync_req_cb(esp_ble_conn_t *conn,
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
bool past_available, uint16_t pa_interval)
@@ -769,18 +777,32 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn,
return 0;
}
/* The address type needs the same treatment. BASS 3.1.1.4 defines only two
* values for Advertiser_Address_Type, each covering its identity form as well:
* 0x00 public (device or identity), 0x01 random (device or static identity). */
static uint8_t addr_type_host_to_le(uint8_t type)
{
#if CONFIG_BT_BLUEDROID_ENABLED
return (type == BLE_ADDR_TYPE_PUBLIC ||
type == BLE_ADDR_TYPE_RPA_PUBLIC) ? BT_ADDR_LE_PUBLIC : BT_ADDR_LE_RANDOM;
#else
return (type == BLE_ADDR_PUBLIC ||
type == BLE_ADDR_PUBLIC_ID) ? BT_ADDR_LE_PUBLIC : BT_ADDR_LE_RANDOM;
#endif
}
void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event)
{
bt_addr_le_t addr = {0};
uint8_t addr_type = addr_type_host_to_le(event->pa_sync.addr.type);
uint8_t addr[6];
int err;
addr.type = event->pa_sync.addr.type;
addr_order_copy(addr.a.val, event->pa_sync.addr.val);
addr_order_copy(addr, event->pa_sync.addr.val);
if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT ||
(broadcast_sink.recv_state &&
broadcast_sink.recv_state->addr.type == addr.type &&
memcmp(broadcast_sink.recv_state->addr.a.val, addr.a.val, sizeof(addr.a.val)) == 0 &&
broadcast_sink.recv_state->addr.type == addr_type &&
memcmp(broadcast_sink.recv_state->addr.a.val, addr, sizeof(addr)) == 0 &&
broadcast_sink.recv_state->adv_sid == event->pa_sync.sid)) {
ESP_LOGI(TAG, "PA sync %u synced for broadcast sink", event->pa_sync.sync_handle);

View File

@@ -169,7 +169,7 @@ int local_public_addr_get(uint8_t addr[6])
}
/* Bluedroid hands out addresses MSB-first; the caller feeds this to the audio
* stack, which stores a bt_addr_le_t and puts it on air LSB-first.
* stack, which puts it on air LSB-first.
*/
for (size_t i = 0; i < 6; i++) {
addr[i] = bda[5 - i];

View File

@@ -16,13 +16,13 @@
*/
static void diag_log_cis_dir(const char *phase, size_t idx, const char *dir,
uint16_t iso_interval,
const struct bt_iso_unicast_tx_info *info)
const esp_ble_iso_unicast_tx_info_t *info)
{
ESP_LOGI(TAG, "[%s][%zu] %s {ft %u bn %u latency %lu us sdu_interval %s}",
phase, idx, dir,
iso_interval != 0 ? (unsigned)(info->flush_timeout / iso_interval) : 0U,
info->bn, (unsigned long)info->latency,
info->sdu_interval == BT_ISO_SDU_INTERVAL_UNKNOWN ? "unknown (v1 event)"
info->sdu_interval == ESP_BLE_ISO_SDU_INTERVAL_UNKNOWN ? "unknown (v1 event)"
: "see qos");
}

View File

@@ -37,7 +37,7 @@ static uint8_t collect_members(const uint16_t *conn_handles, size_t count,
return found;
}
static void csip_discover_cb(struct bt_conn *conn,
static void csip_discover_cb(esp_ble_conn_t *conn,
const esp_ble_audio_csip_set_coordinator_set_member_t *member,
int err, size_t set_count)
{

View File

@@ -224,7 +224,7 @@ static void read_media_state_cb(esp_ble_conn_t *conn, int err, uint8_t state)
}
}
static void send_cmd_cb(esp_ble_conn_t *conn, int err, const struct mpl_cmd *cmd)
static void send_cmd_cb(esp_ble_conn_t *conn, int err, const esp_ble_audio_mpl_cmd_t *cmd)
{
if (err) {
ESP_LOGE(TAG, "Send command failed, err %d, cmd %p", err, cmd);
@@ -234,7 +234,7 @@ static void send_cmd_cb(esp_ble_conn_t *conn, int err, const struct mpl_cmd *cmd
ESP_LOGI(TAG, "Send command succeeded, cmd %p", cmd);
}
static void cmd_ntf_cb(esp_ble_conn_t *conn, int err, const struct mpl_cmd_ntf *ntf)
static void cmd_ntf_cb(esp_ble_conn_t *conn, int err, const esp_ble_audio_mpl_cmd_ntf_t *ntf)
{
if (err) {
ESP_LOGE(TAG, "Invalid command ntf received, err %d, ntf %p", err, ntf);