From 3b70c1a856dd7f7bb4d6b2c661806d9237038dac Mon Sep 17 00:00:00 2001 From: Linyan Liu Date: Tue, 12 May 2026 19:09:30 +0800 Subject: [PATCH] feat(ble_audio): Support using PAST in the cap/acceptor example --- .../esp_ble_audio/api/esp_ble_audio_bap_api.c | 8 +- .../api/esp_ble_audio_common_api.c | 7 +- .../esp_ble_audio/api/esp_ble_audio_has_api.c | 7 +- .../api/esp_ble_audio_media_proxy_api.c | 7 +- .../api/esp_ble_audio_micp_api.c | 7 +- .../esp_ble_audio/api/esp_ble_audio_tbs_api.c | 4 +- .../api/esp_ble_audio_tmap_api.c | 7 +- .../esp_ble_audio/api/esp_ble_audio_vcp_api.c | 7 +- .../api/include/esp_ble_audio_common_api.h | 2 + .../esp_ble_audio/host/adapter/nimble/init.c | 8 +- .../host/adapter/nimble/profiles/ascs.c | 14 +- .../host/adapter/nimble/profiles/bass.c | 14 +- .../host/adapter/nimble/profiles/cas.c | 12 +- .../host/adapter/nimble/profiles/csis.c | 4 +- .../host/adapter/nimble/profiles/has.c | 20 +- .../host/adapter/nimble/profiles/mcs.c | 20 +- .../host/adapter/nimble/profiles/mics.c | 20 +- .../host/adapter/nimble/profiles/pacs.c | 4 +- .../host/adapter/nimble/profiles/server.c | 10 +- .../host/adapter/nimble/profiles/tbs.c | 21 +- .../host/adapter/nimble/profiles/tmas.c | 21 +- .../host/adapter/nimble/profiles/vcs.c | 20 +- .../host/common/include/common/init.h | 9 +- .../bt/esp_ble_audio/host/common/init.c | 8 +- components/bt/esp_ble_audio/lib/lib | 2 +- .../api/include/esp_ble_iso_common_api.h | 2 + .../bt/esp_ble_iso/host/adapter/nimble/gap.c | 18 + .../host/adapter/nimble/gatt/gatt.db.c | 5 +- .../bt/esp_ble_iso/host/common/app/gap.c | 49 + components/bt/esp_ble_iso/host/common/gatt.c | 7 +- components/bt/esp_ble_iso/host/common/host.c | 16 +- .../host/common/include/common/app/gap.h | 28 + .../host/common/include/common/scan.h | 5 + components/bt/esp_ble_iso/host/common/iso.c | 3 - components/bt/esp_ble_iso/host/common/scan.c | 69 +- components/bt/esp_ble_iso/host/iso/iso.c | 7 +- .../include/subsys/bluetooth/host/hci_core.h | 11 + .../bap/unicast_client/main/main.c | 3 +- .../esp_ble_audio/cap/acceptor/README.md | 140 ++- .../cap/acceptor/main/Kconfig.projbuild | 43 +- .../cap/acceptor/main/cap_acceptor.h | 2 + .../acceptor/main/cap_acceptor_broadcast.c | 981 +++++++++++------- .../esp_ble_audio/cap/acceptor/main/main.c | 25 +- .../initiator/main/cap_initiator_unicast.c | 3 +- .../example_utils/ble_audio_example_utils.c | 40 + .../example_utils/ble_audio_example_utils.h | 3 + .../esp_ble_audio/tmap/central/main/main.c | 3 +- .../esp_ble_iso/cis_central/main/main.c | 3 +- .../example_utils/ble_iso_example_utils.c | 40 + .../example_utils/ble_iso_example_utils.h | 3 + 50 files changed, 1210 insertions(+), 562 deletions(-) diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c index 11e512c2937..1d41fa6fa31 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c @@ -25,13 +25,13 @@ esp_err_t esp_ble_audio_bap_unicast_server_register(const esp_ble_audio_bap_unic return ESP_FAIL; } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_ascs_init(); if (err) { bt_bap_unicast_server_unregister_safe(); return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } @@ -723,13 +723,13 @@ esp_err_t esp_ble_audio_bap_scan_delegator_register(esp_ble_audio_bap_scan_deleg return ESP_FAIL; } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_bass_init(); if (err) { bt_bap_scan_delegator_unregister_safe(); return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c index d6c33b81adf..b47db6b776e 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "esp_ble_audio_common_api.h" @@ -53,7 +54,11 @@ esp_err_t esp_ble_audio_gattc_disc_start(uint16_t conn_handle) int err; err = bt_gattc_disc_start_safe(conn_handle); - if (err) { + /* -EALREADY means disc already in progress for this conn (e.g. peer sent + * MTU exchange twice triggering two gatt_mtu_change events). The + * caller's goal is already true, treat as success. + */ + if (err && err != -EALREADY) { return ESP_FAIL; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c index 68dd6c082ed..0e2467df333 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c @@ -133,12 +133,15 @@ esp_err_t esp_ble_audio_has_register(const esp_ble_audio_has_features_param_t *f return ESP_FAIL; } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_has_init(); if (err) { + /* TODO: rollback register_safe once lib exposes an unregister API; + * retry will hit -EALREADY. Only reachable on GATT alloc failure. + */ return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c index 9e1a37375de..5f1754b6ae3 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c @@ -566,12 +566,15 @@ esp_err_t esp_ble_audio_media_proxy_pl_init(void) return ESP_FAIL; } -#if CONFIG_BT_MCS && BLE_AUDIO_SVC_SEP_ADD +#if CONFIG_BT_MCS && BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_media_proxy_pl_init(); if (err) { + /* TODO: rollback pl_init_safe once lib exposes an undo API; + * retry will hit -EALREADY. Only reachable on GATT alloc failure. + */ return ESP_FAIL; } -#endif /* CONFIG_BT_MCS && BLE_AUDIO_SVC_SEP_ADD */ +#endif /* CONFIG_BT_MCS && BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c index bc2b6a561e3..4c836ef448e 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c @@ -22,12 +22,15 @@ esp_err_t esp_ble_audio_micp_mic_dev_register(esp_ble_audio_micp_mic_dev_registe return ESP_FAIL; } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_micp_mic_dev_init(); if (err) { + /* TODO: rollback register_safe once lib exposes an unregister API; + * retry will hit -EALREADY. Only reachable on GATT alloc failure. + */ return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c index 20aab0bbefe..5d6b7f2d269 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c @@ -318,12 +318,12 @@ esp_err_t esp_ble_audio_tbs_register_bearer(const esp_ble_audio_tbs_register_par *bearer_index = ret; -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD if (bt_le_gtbs_init()) { bt_tbs_unregister_bearer_safe(*bearer_index); return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c index 934e0af4ff2..14f3197dc88 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c @@ -18,12 +18,15 @@ esp_err_t esp_ble_audio_tmap_register(esp_ble_audio_tmap_role_t role) return ESP_FAIL; } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_tmas_init(); if (err) { + /* TODO: rollback register_safe once lib exposes an unregister API; + * retry will hit -EALREADY. Only reachable on GATT alloc failure. + */ return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c index 6f071f5686e..9d606766e31 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c @@ -49,12 +49,15 @@ esp_err_t esp_ble_audio_vcp_vol_rend_register(esp_ble_audio_vcp_vol_rend_registe return ESP_FAIL; } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_vcp_vol_rend_init(); if (err) { + /* TODO: rollback register_safe once lib exposes an unregister API; + * retry will hit -EALREADY. Only reachable on GATT alloc failure. + */ return ESP_FAIL; } -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h b/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h index ee6add40b7c..80df0d8a669 100644 --- a/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h +++ b/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h @@ -92,6 +92,8 @@ esp_err_t esp_ble_audio_gattc_disc_start(uint16_t conn_handle); #define ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV BT_LE_GAP_APP_EVENT_EXT_SCAN_RECV /*!< Audio GAP Periodic Sync Established event */ #define ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC BT_LE_GAP_APP_EVENT_PA_SYNC +/*!< Audio GAP Periodic Sync Transfer Received event */ +#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 Connection Complete event */ diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/init.c b/components/bt/esp_ble_audio/host/adapter/nimble/init.c index e9392e2442f..8a8346eeceb 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/init.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/init.c @@ -168,7 +168,7 @@ int bt_le_nimble_audio_init(void) } #endif /* CONFIG_BT_PACS */ -#if (BLE_AUDIO_SVC_SEP_ADD == 0) +#if (BLE_AUDIO_SVC_DEFERRED_ADD == 0) #if CONFIG_BT_ASCS err = bt_le_nimble_ascs_init(); if (err) { @@ -203,7 +203,7 @@ int bt_le_nimble_audio_init(void) return err; } #endif /* CONFIG_BT_HAS */ -#endif /* (BLE_AUDIO_SVC_SEP_ADD == 0) */ +#endif /* (BLE_AUDIO_SVC_DEFERRED_ADD == 0) */ return err; } @@ -419,7 +419,7 @@ int bt_le_nimble_audio_start(void *info) ARG_UNUSED(inc_csis_svc); #endif /* CONFIG_BT_CAP_ACCEPTOR */ -#if (BLE_AUDIO_SVC_SEP_ADD == 0) +#if (BLE_AUDIO_SVC_DEFERRED_ADD == 0) #if CONFIG_BT_MCS err = bt_le_nimble_media_proxy_pl_init(); if (err) { @@ -440,7 +440,7 @@ int bt_le_nimble_audio_start(void *info) return err; } #endif /* CONFIG_BT_MICP_MIC_DEV */ -#endif /* (BLE_AUDIO_SVC_SEP_ADD == 0) */ +#endif /* (BLE_AUDIO_SVC_DEFERRED_ADD == 0) */ err = ble_gatts_start(); if (err) { diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c index 0eb873d6747..830a20e243c 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c @@ -108,8 +108,18 @@ int bt_le_nimble_ascs_attr_handle_set(void) { struct bt_gatt_service *ascs_svc; struct bt_gatt_attr *attr; - uint16_t start_handle; - uint16_t end_handle; + uint16_t start_handle = 0; + uint16_t end_handle = 0; + int rc; + + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_ASCS_VAL), NULL); + if (rc) { + LOG_DBG("[N]AscsNotInit"); + return 0; + } ascs_svc = lib_ascs_svc_get(); assert(ascs_svc); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c index f9fbad6b5f2..f123d0c99bd 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c @@ -84,8 +84,18 @@ int bt_le_nimble_bass_attr_handle_set(void) { struct bt_gatt_service *bass_svc; struct bt_gatt_attr *attr; - uint16_t start_handle; - uint16_t end_handle; + uint16_t start_handle = 0; + uint16_t end_handle = 0; + int rc; + + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_BASS_VAL), NULL); + if (rc) { + LOG_DBG("[N]BassNotInit"); + return 0; + } bass_svc = lib_bap_bass_svc_get(); assert(bass_svc); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c index fdc0407c186..3cee66578f6 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c @@ -50,21 +50,19 @@ static struct ble_gatt_svc_def gatt_svc_cas[] = { int bt_le_nimble_cas_attr_handle_set(void) { struct bt_gatt_service *cas_svc; - uint16_t handle; + uint16_t handle = 0; int rc; - cas_svc = lib_cas_svc_get(); - assert(cas_svc); - - LOG_DBG("[N]CasAttrHdlSet[%u]", cas_svc->attr_count); - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_CAS_VAL), &handle); if (rc) { LOG_ERR("[N]CasNotFound[%d]", rc); return rc; } - LOG_DBG("[N]Hdl[%u]", handle); + cas_svc = lib_cas_svc_get(); + assert(cas_svc); + + LOG_DBG("[N]CasAttrHdlSet[%u][%u]", handle, cas_svc->attr_count); for (size_t i = 0; i < cas_svc->attr_count; i++) { (cas_svc->attrs + i)->handle = handle + i; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c index 0504a1fd876..939976e415c 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c @@ -139,8 +139,8 @@ static int csis_svc_check(void) int bt_le_nimble_csis_attr_handle_set(void) { struct bt_gatt_attr *attr; - uint16_t start_handle; - uint16_t end_handle; + uint16_t start_handle = 0; + uint16_t end_handle = 0; LOG_DBG("[N]CsisAttrHdlSet[%u]", csis_svc_count); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c index 2b80ceafc05..2c94637c8ad 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c @@ -110,21 +110,23 @@ int bt_le_nimble_has_attr_handle_set(void) uint16_t end_handle = 0; int rc; - has_svc = lib_has_svc_get(); - assert(has_svc); - - LOG_DBG("[N]HasAttrHdlSet[%u]", has_svc->attr_count); - assert(has_svc->attr_count > 0); - + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_HAS_VAL), &start_handle); if (rc) { - LOG_ERR("[N]HasNotFound[%d]", rc); - return rc; + LOG_DBG("[N]HasNotInit"); + return 0; } + has_svc = lib_has_svc_get(); + assert(has_svc); + assert(has_svc->attr_count > 0); + end_handle = start_handle + has_svc->attr_count - 1; - LOG_DBG("[N]Hdl[%u][%u]", start_handle, end_handle); + LOG_DBG("[N]HasAttrHdlSet[%u][%u][%u]", + start_handle, end_handle, has_svc->attr_count); for (size_t i = 0; i < has_svc->attr_count; i++) { (has_svc->attrs + i)->handle = start_handle + i; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c index 811b01d8b08..c0386353aef 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c @@ -393,20 +393,22 @@ int bt_le_nimble_gmcs_attr_handle_set(void) uint16_t end_handle = 0; int rc; + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_GMCS_VAL), &start_handle); + if (rc) { + LOG_DBG("[N]GmcsNotInit"); + return 0; + } + gmcs_svc = lib_mcs_svc_get(); assert(gmcs_svc); - LOG_DBG("[N]GmcsAttrHdlSet[%u]", gmcs_svc->attr_count); - - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_GMCS_VAL), &start_handle); - if (rc) { - LOG_ERR("[N]GmcsNotFound[%d]", rc); - return rc; - } - end_handle = start_handle + gmcs_svc->attr_count - 1; - LOG_DBG("[N]Hdl[%u][%u]", start_handle, end_handle); + LOG_DBG("[N]GmcsAttrHdlSet[%u][%u][%u]", + start_handle, end_handle, gmcs_svc->attr_count); for (size_t i = 0; i < gmcs_svc->attr_count; i++) { (gmcs_svc->attrs + i)->handle = start_handle + i; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c index 69aa2b5fdc2..36828699ea4 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c @@ -234,20 +234,22 @@ int bt_le_nimble_mics_attr_handle_set(void) uint16_t end_handle = 0; int rc; + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_MICS_VAL), &start_handle); + if (rc) { + LOG_DBG("[N]MicsNotInit"); + return 0; + } + mics_svc = lib_mics_svc_get(); assert(mics_svc); - LOG_DBG("[N]MicsAttrHdlSet[%u]", mics_svc->attr_count); - - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_MICS_VAL), &start_handle); - if (rc) { - LOG_ERR("[N]MicsNotFound[%d]", rc); - return rc; - } - end_handle = start_handle + mics_svc->attr_count - 1; - LOG_DBG("[N]Hdl[%u][%u]", start_handle, end_handle); + LOG_DBG("[N]MicsAttrHdlSet[%u][%u][%u]", + start_handle, end_handle, mics_svc->attr_count); for (size_t i = 0; i < mics_svc->attr_count; i++) { (mics_svc->attrs + i)->handle = start_handle + i; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c index d6afe254d86..615b9271d8e 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c @@ -162,8 +162,8 @@ int bt_le_nimble_pacs_attr_handle_set(void) { struct bt_gatt_service *pacs_svc; struct bt_gatt_attr *attr; - uint16_t start_handle; - uint16_t end_handle; + uint16_t start_handle = 0; + uint16_t end_handle = 0; pacs_svc = lib_pacs_svc_get(); assert(pacs_svc); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.c index 872908cfe8d..28b0a248749 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.c @@ -74,7 +74,7 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, case BLE_GATT_ACCESS_OP_READ_CHR: attr = bt_gatts_find_attr_by_handle(attr_handle); if (attr == NULL) { - LOG_WRN("[N]RdInvHdl[%u]", attr_handle); + LOG_WRN("[N]RdAttrNotFound[%u]", attr_handle); return BT_GATT_ERR(BT_ATT_ERR_INVALID_HANDLE); } @@ -88,7 +88,7 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, rc = attr->read(conn, attr, (void *)&cb, UINT16_MAX, 0); if (rc < 0) { - LOG_ERR("[N]RdFail[%u][%d]", attr_handle, rc); + LOG_DBG("[N]RdGattErr[%u][%d]", attr_handle, rc); return BT_GATT_ERR(rc); } @@ -97,7 +97,7 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, case BLE_GATT_ACCESS_OP_WRITE_CHR: attr = bt_gatts_find_attr_by_handle(attr_handle); if (attr == NULL) { - LOG_WRN("[N]WrInvHdl[%u]", attr_handle); + LOG_WRN("[N]WrAttrNotFound[%u]", attr_handle); return BT_GATT_ERR(BT_ATT_ERR_INVALID_HANDLE); } @@ -118,7 +118,7 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, LOG_DBG("[N]WrBassControlPoint[%u]", alloc_len); if (OS_MBUF_PKTLEN(ctx->om) > alloc_len) { - LOG_ERR("[N]WrBassCtrlPtTooLong[%u > %u]", + LOG_WRN("[N]WrBassCtrlPtTooLong[%u > %u]", OS_MBUF_PKTLEN(ctx->om), alloc_len); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); } @@ -148,7 +148,7 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, data = NULL; } if (rc < 0) { - LOG_ERR("[N]WrFail[%u][%d]", attr_handle, rc); + LOG_DBG("[N]WrGattErr[%u][%d]", attr_handle, rc); return BT_GATT_ERR(rc); } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c index aa376634c8c..b56a93823b0 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c @@ -165,21 +165,22 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { int bt_le_nimble_gtbs_attr_handle_set(void) { struct bt_gatt_service *gtbs_svc; - uint16_t handle; + uint16_t handle = 0; int rc; + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_GTBS_VAL), &handle); + if (rc) { + LOG_DBG("[N]GtbsNotInit"); + return 0; + } + gtbs_svc = lib_gtbs_svc_get(); assert(gtbs_svc); - LOG_DBG("[N]GtbsAttrHdlSet[%u]", gtbs_svc->attr_count); - - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_GTBS_VAL), &handle); - if (rc) { - LOG_ERR("[N]GtbsNotFound[%d]", rc); - return rc; - } - - LOG_DBG("[N]Hdl[%u]", handle); + LOG_DBG("[N]GtbsAttrHdlSet[%u][%u]", handle, gtbs_svc->attr_count); for (size_t i = 0; i < gtbs_svc->attr_count; i++) { (gtbs_svc->attrs + i)->handle = handle + i; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c index 3b74e210e4f..c97c3ab1bc8 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c @@ -58,21 +58,22 @@ static const struct ble_gatt_svc_def gatt_svc_tmas[] = { int bt_le_nimble_tmas_attr_handle_set(void) { struct bt_gatt_service *tmas_svc; - uint16_t handle; + uint16_t handle = 0; int rc; + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_TMAS_VAL), &handle); + if (rc) { + LOG_DBG("[N]TmasNotInit"); + return 0; + } + tmas_svc = lib_tmas_svc_get(); assert(tmas_svc); - LOG_DBG("[N]TmasAttrHdlSet[%u]", tmas_svc->attr_count); - - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_TMAS_VAL), &handle); - if (rc) { - LOG_ERR("[N]TmasNotFound[%d]", rc); - return rc; - } - - LOG_DBG("[N]Hdl[%u]", handle); + LOG_DBG("[N]TmasAttrHdlSet[%u][%u]", handle, tmas_svc->attr_count); for (size_t i = 0; i < tmas_svc->attr_count; i++) { (tmas_svc->attrs + i)->handle = handle + i; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c index 467958d18a0..58e7e8c4751 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c @@ -370,20 +370,22 @@ int bt_le_nimble_vcs_attr_handle_set(void) uint16_t end_handle = 0; int rc; + /* App may not register this svc (e.g. CAP Acceptor single mode keeps + * unused capability built). Skip rather than fail audio_start. + */ + rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_VCS_VAL), &start_handle); + if (rc) { + LOG_DBG("[N]VcsNotInit"); + return 0; + } + vcs_svc = lib_vcs_svc_get(); assert(vcs_svc); - LOG_DBG("[N]VcsAttrHdlSet[%u]", vcs_svc->attr_count); - - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_VCS_VAL), &start_handle); - if (rc) { - LOG_ERR("[N]VcsNotFound[%d]", rc); - return rc; - } - end_handle = start_handle + vcs_svc->attr_count - 1; - LOG_DBG("[N]Hdl[%u][%u]", start_handle, end_handle); + LOG_DBG("[N]VcsAttrHdlSet[%u][%u][%u]", + start_handle, end_handle, vcs_svc->attr_count); for (size_t i = 0; i < vcs_svc->attr_count; i++) { (vcs_svc->attrs + i)->handle = start_handle + i; diff --git a/components/bt/esp_ble_audio/host/common/include/common/init.h b/components/bt/esp_ble_audio/host/common/include/common/init.h index 9deca7f7127..153cbd8f465 100644 --- a/components/bt/esp_ble_audio/host/common/include/common/init.h +++ b/components/bt/esp_ble_audio/host/common/include/common/init.h @@ -18,13 +18,18 @@ extern "C" { #endif -#define BLE_AUDIO_SVC_SEP_ADD 0 /* Indicate if adding LE Audio services separately */ +/* Defer LE Audio service add from host init to the application + * register call (esp_ble_audio_*_register). Lets the firmware keep + * unused capabilities built in without exposing their attributes + * before the app provides backing state. + */ +#define BLE_AUDIO_SVC_DEFERRED_ADD 1 /* Minimum value required by BAP. * 64 octets is not enough for running 4 ASEs. * Hence use 128 as the default value here. */ -#define BLE_AUDIO_ATT_MTU_MIN 128 +#define BLE_AUDIO_ATT_MTU_MIN 128 struct bt_csip_set_member_svc_inst; diff --git a/components/bt/esp_ble_audio/host/common/init.c b/components/bt/esp_ble_audio/host/common/init.c index 34b4e6be044..8330e483334 100644 --- a/components/bt/esp_ble_audio/host/common/init.c +++ b/components/bt/esp_ble_audio/host/common/init.c @@ -178,12 +178,13 @@ static const uint16_t ext_structs[] = { sizeof(struct bt_bond_info), }; -#define LEA_VERSION (0x20260512) +#define LEA_VERSION (0x20260514) struct lib_ext_cfgs { /* BLE */ bool config_past_sender; bool config_past_receiver; + bool config_past_check; uint8_t config_max_conn; uint8_t config_max_paired; uint16_t config_max_attr_len; @@ -439,6 +440,7 @@ static const struct lib_ext_cfgs ext_cfgs = { /* BLE */ .config_past_sender = CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER, .config_past_receiver = CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER, + .config_past_check = false, .config_max_conn = CONFIG_BT_MAX_CONN, .config_max_paired = CONFIG_BT_MAX_PAIRED, .config_max_attr_len = 251, @@ -2092,7 +2094,7 @@ int bt_le_audio_init(void) return bt_le_nimble_audio_init(); } -#if BLE_AUDIO_SVC_SEP_ADD +#if BLE_AUDIO_SVC_DEFERRED_ADD #if CONFIG_BT_ASCS int bt_le_ascs_init(void) { @@ -2164,7 +2166,7 @@ int bt_le_micp_mic_dev_init(void) return bt_le_nimble_micp_mic_dev_init(); } #endif /* CONFIG_BT_MICP_MIC_DEV */ -#endif /* BLE_AUDIO_SVC_SEP_ADD */ +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ int bt_le_audio_start(void *info) { diff --git a/components/bt/esp_ble_audio/lib/lib b/components/bt/esp_ble_audio/lib/lib index 3796f0e4ef5..b9355d1a1c8 160000 --- a/components/bt/esp_ble_audio/lib/lib +++ b/components/bt/esp_ble_audio/lib/lib @@ -1 +1 @@ -Subproject commit 3796f0e4ef56f7b36fc51a32f41bb968c1e56081 +Subproject commit b9355d1a1c875a3a62a067a6c232fd03fed3523c diff --git a/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h b/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h index 6e9281b4fae..52ec59ac08e 100644 --- a/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h +++ b/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h @@ -552,6 +552,8 @@ esp_err_t esp_ble_iso_chan_send_ts(esp_ble_iso_chan_t *chan, #define ESP_BLE_ISO_GAP_EVENT_EXT_SCAN_RECV BT_LE_GAP_APP_EVENT_EXT_SCAN_RECV /*!< ISO GAP Periodic Sync Established event */ #define ESP_BLE_ISO_GAP_EVENT_PA_SYNC BT_LE_GAP_APP_EVENT_PA_SYNC +/*!< ISO GAP Periodic Sync Transfer Received event */ +#define ESP_BLE_ISO_GAP_EVENT_PA_SYNC_PAST BT_LE_GAP_APP_EVENT_PA_SYNC_PAST /*!< ISO GAP Periodic Sync Lost event */ #define ESP_BLE_ISO_GAP_EVENT_PA_SYNC_LOST BT_LE_GAP_APP_EVENT_PA_SYNC_LOST /*!< ISO GAP Connection Complete event */ diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c index 59d4843887f..341fd6ff152 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c @@ -135,6 +135,24 @@ void bt_le_nimble_gap_post_event(void *param) qev->pa_sync.adv_ca = ev->periodic_sync.adv_clk_accuracy; break; + case BLE_GAP_EVENT_PERIODIC_TRANSFER: + case BLE_GAP_EVENT_PERIODIC_TRANSFER_V2: + /* Both events share the same periodic_transfer union field; V2 only + * adds PAwR-specific tail fields that this handler does not consume. + */ + qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC_PAST; + + qev->pa_sync_past.status = ev->periodic_transfer.status; + qev->pa_sync_past.sync_handle = ev->periodic_transfer.sync_handle; + qev->pa_sync_past.addr.type = ev->periodic_transfer.adv_addr.type; + memcpy(qev->pa_sync_past.addr.val, ev->periodic_transfer.adv_addr.val, BT_ADDR_SIZE); + qev->pa_sync_past.sid = ev->periodic_transfer.sid; + qev->pa_sync_past.adv_phy = ev->periodic_transfer.adv_phy; + qev->pa_sync_past.per_adv_itvl = ev->periodic_transfer.per_adv_itvl; + qev->pa_sync_past.adv_ca = ev->periodic_transfer.adv_clk_accuracy; + qev->pa_sync_past.conn_handle = ev->periodic_transfer.conn_handle; + break; + case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC_LOST; diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c index ab00577286a..1881ea1078b 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c @@ -1411,7 +1411,10 @@ int bt_le_nimble_gattc_db_auto_disc(uint16_t conn_handle) adb = gattc_db_find(conn_handle); if (adb) { - LOG_WRN("[N]GattcDbExist"); + /* Idempotent: caller's goal (disc running for this conn) is + * already true. -EALREADY tells the API layer to map to success. + */ + LOG_DBG("[N]GattcDbExist"); return -EALREADY; } diff --git a/components/bt/esp_ble_iso/host/common/app/gap.c b/components/bt/esp_ble_iso/host/common/app/gap.c index df3b0a373a9..77defe11ac6 100644 --- a/components/bt/esp_ble_iso/host/common/app/gap.c +++ b/components/bt/esp_ble_iso/host/common/app/gap.c @@ -168,6 +168,7 @@ static void handle_pa_sync_event_safe(struct bt_le_gap_app_param *param) event.pa_sync.per_adv_itvl, event.pa_sync.addr.type, event.pa_sync.addr.val, + BT_CONN_HANDLE_INVALID, &per_adv_sync); if (err) { goto end; @@ -182,6 +183,51 @@ end: bt_le_gap_app_cb_evt(&event); } +static void handle_pa_sync_past_event_safe(struct bt_le_gap_app_param *param) +{ + struct bt_le_gap_app_event event = { + .type = BT_LE_GAP_APP_EVENT_PA_SYNC_PAST, + }; + struct bt_le_per_adv_sync *per_adv_sync; + int err; + + event.pa_sync_past.status = param->pa_sync_past.status; + event.pa_sync_past.sync_handle = param->pa_sync_past.sync_handle; + event.pa_sync_past.addr.type = param->pa_sync_past.addr.type; + memcpy(event.pa_sync_past.addr.val, param->pa_sync_past.addr.val, BT_ADDR_SIZE); + event.pa_sync_past.sid = param->pa_sync_past.sid; + event.pa_sync_past.adv_phy = param->pa_sync_past.adv_phy; + event.pa_sync_past.per_adv_itvl = param->pa_sync_past.per_adv_itvl; + event.pa_sync_past.adv_ca = param->pa_sync_past.adv_ca; + event.pa_sync_past.conn_handle = param->pa_sync_past.conn_handle; + + bt_le_host_lock(); + + if (event.pa_sync_past.status) { + goto end; + } + + err = bt_le_per_adv_sync_new(event.pa_sync_past.sync_handle, + event.pa_sync_past.sid, + event.pa_sync_past.adv_phy, + event.pa_sync_past.per_adv_itvl, + event.pa_sync_past.addr.type, + event.pa_sync_past.addr.val, + event.pa_sync_past.conn_handle, + &per_adv_sync); + if (err) { + goto end; + } + + err = bt_le_per_adv_sync_establish_listener(event.pa_sync_past.sync_handle); + assert(err == 0); + +end: + bt_le_host_unlock(); + + bt_le_gap_app_cb_evt(&event); +} + static void handle_pa_sync_lost_event_safe(struct bt_le_gap_app_param *param) { struct bt_le_gap_app_event event = { @@ -437,6 +483,9 @@ void bt_le_gap_handle_event(uint8_t *data, size_t data_len) case BT_LE_GAP_APP_PARAM_PA_SYNC: handle_pa_sync_event_safe(param); break; + case BT_LE_GAP_APP_PARAM_PA_SYNC_PAST: + handle_pa_sync_past_event_safe(param); + break; case BT_LE_GAP_APP_PARAM_PA_SYNC_LOST: handle_pa_sync_lost_event_safe(param); break; diff --git a/components/bt/esp_ble_iso/host/common/gatt.c b/components/bt/esp_ble_iso/host/common/gatt.c index 8c4d6779e9b..fbe167c904c 100644 --- a/components/bt/esp_ble_iso/host/common/gatt.c +++ b/components/bt/esp_ble_iso/host/common/gatt.c @@ -128,10 +128,9 @@ struct bt_gatt_attr *bt_gatts_find_attr_by_handle(uint16_t handle) break; } } - } - - if (attr == NULL) { - LOG_WRN("AttrNotFound[%u]", handle); + if (attr != NULL) { + break; + } } return attr; diff --git a/components/bt/esp_ble_iso/host/common/host.c b/components/bt/esp_ble_iso/host/common/host.c index 5cf78899924..cf5491dee81 100644 --- a/components/bt/esp_ble_iso/host/common/host.c +++ b/components/bt/esp_ble_iso/host/common/host.c @@ -72,10 +72,15 @@ int bt_le_host_init(void) k_mutex_create(&host_mutex); + err = bt_le_scan_init(); + if (err) { + goto delete_mutex; + } + #if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT err = bt_le_l2cap_init(); if (err) { - goto delete_mutex; + goto deinit_scan; } #endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ @@ -96,8 +101,10 @@ deinit_iso: deinit_l2cap: #if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT bt_le_l2cap_deinit(); -delete_mutex: +deinit_scan: /* only reachable when OTS path is compiled in */ #endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ + bt_le_scan_deinit(); +delete_mutex: k_mutex_delete(&host_mutex); return err; @@ -107,11 +114,12 @@ void bt_le_host_deinit(void) { LOG_DBG("HostDeinit"); + bt_le_iso_task_deinit(); + bt_le_iso_deinit(); #if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT bt_le_l2cap_deinit(); #endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ - bt_le_iso_deinit(); - bt_le_iso_task_deinit(); + bt_le_scan_deinit(); k_mutex_delete(&host_mutex); } diff --git a/components/bt/esp_ble_iso/host/common/include/common/app/gap.h b/components/bt/esp_ble_iso/host/common/include/common/app/gap.h index 0676d8679bb..28cb301fa17 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/app/gap.h +++ b/components/bt/esp_ble_iso/host/common/include/common/app/gap.h @@ -45,6 +45,18 @@ struct bt_le_gap_app_pa_sync_param { uint8_t adv_ca; }; +struct bt_le_gap_app_pa_sync_past_param { + uint8_t status; + uint16_t sync_handle; + struct bt_le_addr addr; + uint8_t sid; + uint8_t adv_phy; + uint16_t per_adv_itvl; + uint8_t adv_ca; + /* ACL conn that delivered the PAST. */ + uint16_t conn_handle; +}; + struct bt_le_gap_app_pa_sync_lost_param { uint16_t sync_handle; uint8_t reason; @@ -113,6 +125,7 @@ struct bt_le_gap_app_param { union { struct bt_le_gap_app_ext_scan_recv_param ext_scan_recv; struct bt_le_gap_app_pa_sync_param pa_sync; + struct bt_le_gap_app_pa_sync_past_param pa_sync_past; struct bt_le_gap_app_pa_sync_lost_param pa_sync_lost; struct bt_le_gap_app_pa_sync_recv_param pa_sync_recv; struct bt_le_gap_app_acl_connect_param acl_connect; @@ -126,6 +139,7 @@ struct bt_le_gap_app_param { enum bt_le_gap_app_param_type { BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV, BT_LE_GAP_APP_PARAM_PA_SYNC, + BT_LE_GAP_APP_PARAM_PA_SYNC_PAST, BT_LE_GAP_APP_PARAM_PA_SYNC_LOST, BT_LE_GAP_APP_PARAM_PA_SYNC_RECV, BT_LE_GAP_APP_PARAM_ACL_CONNECT, @@ -160,6 +174,18 @@ struct bt_le_gap_app_event_pa_sync { uint8_t adv_ca; }; +struct bt_le_gap_app_event_pa_sync_past { + uint8_t status; + uint16_t sync_handle; + struct bt_le_addr addr; + uint8_t sid; + uint8_t adv_phy; + uint16_t per_adv_itvl; + uint8_t adv_ca; + /* ACL conn that delivered the PAST. */ + uint16_t conn_handle; +}; + struct bt_le_gap_app_event_pa_sync_lost { uint16_t sync_handle; uint8_t reason; @@ -228,6 +254,7 @@ struct bt_le_gap_app_event { union { struct bt_le_gap_app_event_ext_scan_recv ext_scan_recv; struct bt_le_gap_app_event_pa_sync pa_sync; + struct bt_le_gap_app_event_pa_sync_past pa_sync_past; struct bt_le_gap_app_event_pa_sync_lost pa_sync_lost; struct bt_le_gap_app_event_pa_sync_recv pa_sync_recv; struct bt_le_gap_app_event_acl_connect acl_connect; @@ -242,6 +269,7 @@ struct bt_le_gap_app_event { enum bt_le_gap_app_event_type { BT_LE_GAP_APP_EVENT_EXT_SCAN_RECV, BT_LE_GAP_APP_EVENT_PA_SYNC, + BT_LE_GAP_APP_EVENT_PA_SYNC_PAST, BT_LE_GAP_APP_EVENT_PA_SYNC_LOST, BT_LE_GAP_APP_EVENT_PA_SYNC_RECV, BT_LE_GAP_APP_EVENT_ACL_CONNECT, diff --git a/components/bt/esp_ble_iso/host/common/include/common/scan.h b/components/bt/esp_ble_iso/host/common/include/common/scan.h index 12271db38dc..27aeaedc234 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/scan.h +++ b/components/bt/esp_ble_iso/host/common/include/common/scan.h @@ -39,6 +39,7 @@ int bt_le_per_adv_sync_new(uint16_t sync_handle, uint16_t interval, uint8_t addr_type, const uint8_t addr[6], + uint16_t conn_handle, struct bt_le_per_adv_sync **out_sync); int bt_le_per_adv_sync_delete(uint16_t sync_handle); @@ -51,6 +52,10 @@ int bt_le_per_adv_sync_report_recv_listener(uint16_t sync_handle, const uint8_t *data, size_t data_length); +int bt_le_scan_init(void); + +void bt_le_scan_deinit(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_iso/host/common/iso.c b/components/bt/esp_ble_iso/host/common/iso.c index 108cfffe303..41c5b1834f8 100644 --- a/components/bt/esp_ble_iso/host/common/iso.c +++ b/components/bt/esp_ble_iso/host/common/iso.c @@ -36,9 +36,6 @@ LOG_MODULE_REGISTER(ISO_SHIM, CONFIG_BT_ISO_LOG_LEVEL); #define ISO_PKT_COMP_SDU (0b10) #define ISO_PKT_LAST_FRAG (0b11) -#define BT_LE_FEAT_SET(feat, n) (feat[(n) >> 3] |= BIT((n) & 7)) -#define BT_LE_FEAT_UNSET(feat, n) (feat[(n) >> 3] &= ~BIT((n) & 7)) - static sys_slist_t iso_cbs = SYS_SLIST_STATIC_INIT(&iso_cbs); #if CONFIG_BT_ISO_UNICAST diff --git a/components/bt/esp_ble_iso/host/common/scan.c b/components/bt/esp_ble_iso/host/common/scan.c index 6479cd0fb5c..5d72d9d15ab 100644 --- a/components/bt/esp_ble_iso/host/common/scan.c +++ b/components/bt/esp_ble_iso/host/common/scan.c @@ -14,8 +14,10 @@ #include #include #include +#include #include +#include <../host/conn_internal.h> #include <../host/hci_core.h> #include "common/host.h" @@ -220,8 +222,7 @@ static struct bt_le_per_adv_sync *per_adv_sync_find(uint16_t handle) struct bt_le_per_adv_sync *per_adv_sync = NULL; for (size_t i = 0; i < ARRAY_SIZE(per_adv_sync_pool); i++) { - if (atomic_test_bit(per_adv_sync_pool[i].flags, - BT_PER_ADV_SYNC_SYNCED) && + if (atomic_test_bit(per_adv_sync_pool[i].flags, BT_PER_ADV_SYNC_SYNCED) && per_adv_sync_pool[i].handle == handle) { per_adv_sync = &per_adv_sync_pool[i]; break; @@ -269,11 +270,13 @@ int bt_le_per_adv_sync_new(uint16_t sync_handle, uint16_t interval, uint8_t addr_type, const uint8_t addr[6], + uint16_t conn_handle, struct bt_le_per_adv_sync **out_sync) { struct bt_le_per_adv_sync *per_adv_sync; - LOG_DBG("PaSyncNew[%u][%u][%u][%u]", sync_handle, sid, phy, interval); + LOG_DBG("PaSyncNew[%u][%u][%u][%u][%u]", + sync_handle, sid, phy, interval, conn_handle); if (addr_type > BT_ADDR_LE_RANDOM_ID || addr == NULL) { LOG_ERR("InvAddr[%02x][%p]", addr_type, addr); @@ -296,6 +299,7 @@ int bt_le_per_adv_sync_new(uint16_t sync_handle, per_adv_sync->sid = sid; per_adv_sync->phy = phy; per_adv_sync->interval = interval; + per_adv_sync->conn_handle = conn_handle; per_adv_sync->addr.type = addr_type; memcpy(per_adv_sync->addr.a.val, addr, BT_ADDR_SIZE); @@ -345,7 +349,16 @@ int bt_le_per_adv_sync_establish_listener(uint16_t sync_handle) info.sid = per_adv_sync->sid; info.interval = per_adv_sync->interval; info.phy = per_adv_sync->phy; - info.conn = NULL; /* TODO: update for PAST */ + /* PAST path: a valid conn_handle means PAST delivered the sync. NULL + * lookup here is a race — the ACL disconnected between PAST event + * enqueue and host processing. Synced cb still fires with conn=NULL. + */ + if (per_adv_sync->conn_handle != BT_CONN_HANDLE_INVALID) { + info.conn = bt_conn_lookup_handle(per_adv_sync->conn_handle, BT_CONN_TYPE_LE); + if (info.conn == NULL) { + LOG_WRN("PaSyncConnGone[%u][%u]", sync_handle, per_adv_sync->conn_handle); + } + } SYS_SLIST_FOR_EACH_CONTAINER(&pa_sync_cbs, listener, node) { if (listener->synced) { @@ -353,6 +366,10 @@ int bt_le_per_adv_sync_establish_listener(uint16_t sync_handle) } } + if (info.conn) { + bt_conn_unref(info.conn); + } + return 0; } @@ -492,3 +509,47 @@ int bt_le_scan_stop(void) return err; } + +static void past_features_set(void) +{ +#if CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER + BT_LE_FEAT_SET(bt_dev.le.features, BT_LE_FEAT_BIT_PAST_SEND); +#endif /* CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER */ + +#if CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER + BT_LE_FEAT_SET(bt_dev.le.features, BT_LE_FEAT_BIT_PAST_RECV); +#endif /* CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER */ + + LOG_DBG("PastFeatSet[%s]", bt_hex(bt_dev.le.features, sizeof(bt_dev.le.features))); +} + +static void past_features_unset(void) +{ +#if CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER + BT_LE_FEAT_UNSET(bt_dev.le.features, BT_LE_FEAT_BIT_PAST_SEND); +#endif /* CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER */ + +#if CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER + BT_LE_FEAT_UNSET(bt_dev.le.features, BT_LE_FEAT_BIT_PAST_RECV); +#endif /* CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER */ + + LOG_DBG("PastFeatUnset[%s]", bt_hex(bt_dev.le.features, sizeof(bt_dev.le.features))); +} + +_IDF_ONLY +int bt_le_scan_init(void) +{ + LOG_DBG("ScanInit"); + + past_features_set(); + + return 0; +} + +_IDF_ONLY +void bt_le_scan_deinit(void) +{ + LOG_DBG("ScanDeinit"); + + past_features_unset(); +} diff --git a/components/bt/esp_ble_iso/host/iso/iso.c b/components/bt/esp_ble_iso/host/iso/iso.c index 92a6c3d71bb..8ecfb5b9ec5 100644 --- a/components/bt/esp_ble_iso/host/iso/iso.c +++ b/components/bt/esp_ble_iso/host/iso/iso.c @@ -118,7 +118,12 @@ void hci_iso(struct net_buf *buf) iso = bt_conn_lookup_handle(iso(buf)->handle, BT_CONN_TYPE_ISO); if (iso == NULL) { - LOG_ERR("ConnNotFoundForHdl[%u]", iso(buf)->handle); + /* Expected during BIG/CIG teardown: the bt_conn is unref'd + * synchronously when the host initiates termination, but ISO packets + * already queued for the iso task may still arrive here. Drop them + * silently. + */ + LOG_INF("IsoRxConnGone[%u]", iso(buf)->handle); return; } diff --git a/components/bt/esp_ble_iso/include/subsys/bluetooth/host/hci_core.h b/components/bt/esp_ble_iso/include/subsys/bluetooth/host/hci_core.h index 6f7efc8a12a..af742f9ee5d 100644 --- a/components/bt/esp_ble_iso/include/subsys/bluetooth/host/hci_core.h +++ b/components/bt/esp_ble_iso/include/subsys/bluetooth/host/hci_core.h @@ -78,10 +78,18 @@ struct bt_le_per_adv_sync { /** Advertiser PHY */ uint8_t phy; + /** ACL conn that delivered PAST; BT_CONN_HANDLE_INVALID for non-PAST sync. */ + uint16_t conn_handle; + /** Flags */ ATOMIC_DEFINE(flags, BT_PER_ADV_SYNC_NUM_FLAGS); }; +/* Port-level invalid conn_handle sentinel; matches NimBLE BLE_HS_CONN_HANDLE_NONE. + * Use in adapter-agnostic code that cannot reference NimBLE headers. + */ +#define BT_CONN_HANDLE_INVALID 0xffff + struct bt_dev_le { /* LE features */ uint8_t features[8]; @@ -97,6 +105,9 @@ struct bt_dev { extern struct bt_dev bt_dev; +#define BT_LE_FEAT_SET(feat, n) (feat[(n) >> 3] |= BIT((n) & 7)) +#define BT_LE_FEAT_UNSET(feat, n) (feat[(n) >> 3] &= ~BIT((n) & 7)) + /* Data type to store state related with command to be updated * when command completes successfully. */ diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/main.c b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/main.c index 5686eeac57f..b1517eb001e 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/main.c +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/main.c @@ -1245,7 +1245,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event) int err; if (event->security_change.status) { - ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); + example_audio_security_failed_recover(TAG, event->security_change.conn_handle, + event->security_change.status); return; } diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md index 254f1edcad6..2d936b9589b 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md @@ -7,9 +7,9 @@ ## Overview -This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the NimBLE host stack with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Acceptor / BAP Unicast Server**, or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). PACS is registered in both modes with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast). +This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the NimBLE host stack with ISO and LE Audio support. It can be built with one or both roles selected at build time: a **CAP Acceptor / BAP Unicast Server**, and / or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). Dual-role builds are supported (BAP spec C.2). PACS is registered in all configurations with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast). -In **unicast** mode (`cap_acceptor_unicast.c`) the acceptor registers BAP unicast-server callbacks (config / reconfig / qos / enable / start / metadata / disable / stop / release) and CAP stream ops, and on enable of a sink ASE automatically issues `bap_stream_start` (Receiver Start Ready). When the source ASE starts, an internal TX scheduler begins sending dummy SDUs. In **broadcast** mode (`cap_acceptor_broadcast.c`) the acceptor registers BAP scan-delegator and broadcast-sink callbacks, drives PA sync (without PAST) on request, receives BASE and BIGInfo, then calls `esp_ble_audio_bap_broadcast_sink_sync` on the first BIS index. Optional **self-scan** lets the acceptor scan for a broadcast source named `CAP Broadcast Source` and use a hardcoded broadcast code `1234` instead of waiting for a Broadcast Assistant. +In **unicast** mode (`cap_acceptor_unicast.c`) the acceptor registers BAP unicast-server callbacks (config / reconfig / qos / enable / start / metadata / disable / stop / release) and CAP stream ops, and on enable of a sink ASE automatically issues `bap_stream_start` (Receiver Start Ready). When the source ASE starts, an internal TX scheduler begins sending dummy SDUs. In **broadcast** mode (`cap_acceptor_broadcast.c`) the acceptor registers BAP scan-delegator and broadcast-sink callbacks, drives PA sync (preferring PAST when the Assistant supports it) on request, receives BASE / BIGInfo / broadcast code, then calls `esp_ble_audio_bap_broadcast_sink_sync` on the BIS bitmap selected by the Assistant via BASS Modify Source — up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` BIS streams. The Broadcast Sink object is created on PA sync and deleted on PA loss; Assistant pause / resume keep the same sink. See **Broadcast Mode Internals** below for sequence, gate flags and sink lifecycle. Optional **self-scan** lets the acceptor scan for a broadcast source named `CAP Broadcast Source` and use a hardcoded broadcast code `1234` instead of waiting for a Broadcast Assistant. The acceptor advertises connectable extended advertising on handle 0 with flags, the ASCS+CAS UUID list, CAS service data with targeted-announcement byte, ASCS targeted-announcement and contexts (unicast build), BASS service data (broadcast build), and the complete device name `cap_acceptor`. The GAP/GATT device name is set to `CAP Acceptor`. @@ -26,12 +26,12 @@ Open menuconfig: idf.py menuconfig ``` -Under **Example: CAP Acceptor** -> **CAP Acceptor mode**: +Under **Example: CAP Acceptor**: -* **Unicast** (`EXAMPLE_UNICAST`, default) — act as BAP Unicast Server; advertise CAS + ASCS for a CAP Initiator. Mutually exclusive with Broadcast. -* **Broadcast** (`EXAMPLE_BROADCAST`) — act as BAP Broadcast Sink with Scan Delegator; advertise CAS + BASS. Mutually exclusive with Unicast. +* **Unicast** (`EXAMPLE_UNICAST`, default) — act as BAP Unicast Server; advertise CAS + ASCS for a CAP Initiator. +* **Broadcast** (`EXAMPLE_BROADCAST`) — act as BAP Broadcast Sink with Scan Delegator; advertise CAS + BASS. -When **Broadcast** is selected, an additional option appears: +The two options can be enabled together for a dual-role acceptor (BAP spec C.2). When **Broadcast** is selected and **Unicast** is disabled (dual-role + self-scan coexistence is not yet supported), an additional option appears: * **Scan for Broadcast Sources without Broadcast Assistant** (`EXAMPLE_SCAN_SELF`) — start scanning at boot for a source advertising the name `CAP Broadcast Source`; on match, create the PA sync directly. The hardcoded broadcast code `1234` is used if the BIG is encrypted. Without this option, a separate Broadcast Assistant must drive PA / BIS sync over BASS. @@ -57,9 +57,135 @@ idf.py -p PORT flash monitor 5. If `EXAMPLE_SCAN_SELF` is set, `check_start_scan` starts extended scanning for the broadcast source. 6. `ext_adv_start` configures and starts connectable extended advertising on handle 0 with the CAS / ASCS / BASS service data appropriate to the build. 7. **Unicast**: on ACL connect the connection handle is stored. On MTU change the acceptor starts service discovery (acting as GATT client). The unicast server then handles config -> reconfig -> qos -> enable -> start (auto Receiver Start Ready for sink ASEs) -> metadata -> disable -> stop -> release per ASE; when the source ASE starts, the TX scheduler begins sending dummy SDUs. -8. **Broadcast**: on `pa_sync_req` from a Broadcast Assistant (or on a scan match in self-scan mode) the acceptor creates a PA sync without PAST. On PA sync, it creates a broadcast sink for the broadcast ID, receives BASE and BIGInfo, then syncs to the first BIS. Stream `started` enters the synced state; `stopped` and `pa_sync_lost` clear flags and (in self-scan mode) restart scanning. +8. **Broadcast**: on `pa_sync_req` the acceptor establishes PA sync — preferring HCI Periodic Advertising Sync Transfer (PAST) when the Assistant supports it, otherwise scanning for the source by address / SID. On PA sync the sink is created (inlined in `broadcast_pa_synced`) and lives until both PA and BIS are gone. BASE, BIGInfo and (if the BIG is encrypted) the broadcast code accumulate as gate flags. When the Assistant's BASS Modify Source pushes a non-zero `bis_sync` bitmap and all gates are open, `bap_broadcast_sink_sync` is called with a `streams[]` of size = popcount of the bitmap (≤ `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT`). Assistant pause (`bis_sync = 0`) issues `_stop` but **does not delete** the sink; resume re-`_sync`s the same object so the cached BIGInfo / BASE / QoS stay valid. PA loss does **not** tear down the BIG — per BASS spec the two are independent. See **Broadcast Mode Internals** below for the full sequence and state machine. 9. On ACL disconnect the connection handle is reset and `ext_adv_start` re-arms advertising. +## Broadcast Mode Internals + +The broadcast acceptor coordinates three roles — Scan Delegator (BASS server), Broadcast Sink (BAP), and PA / BIG sync at the LE controller — under a small atomic-flag state machine. `check_sync_broadcast()` is the single fan-in that calls `bap_broadcast_sink_sync` once all gates are open; each callback that sets a gate invokes it. + +### End-to-end sequence (Assistant-driven) + +``` +Source (SRC) Assistant (ASS) Acceptor (ACC) + | | | + | |---- ACL connect+pair --->| + | |---- BASS Add Source ---->| pa_sync_req_cb + | | (id, addr, sid) | + | | | + | | + | ----- PAST supported (peer + local) ---- | + | |<--- PA state=INFO_REQ ---| set after subscribe + | |- HCI PA Sync Transfer -->| + | | + | ----------- else (no PAST) ------------- | + |<------- periodic_adv_sync_create (scan) -------| + | | + |============== PA sync established ============>| broadcast_pa_synced + | | -> create_broadcast_sink + |============== PA report (BASE) ===============>| base_recv_cb + | | -> FLAG_BASE_RECEIVED + |============== PA report (BIGInfo) ============>| syncable_cb + | | -> FLAG_BROADCAST_SYNCABLE + | | + | ------------ BIG encrypted ------------- | + | |---- BASS Set BCode ----->| broadcast_code_cb + | | | -> FLAG_..._CODE_RECEIVED + | | + | |---- BASS Modify Source ->| bis_sync_req_cb + | | (bis_sync bitmap) | -> FLAG_..._SYNC_REQUESTED + | | check_sync_broadcast -> _sync + |<========= HCI LE BIG Create Sync ==============| + | | + |========== BIG Sync Established ===============>| stream_started_cb + | | -> FLAG_BROADCAST_SYNCED + |============== ISO data (BIS) =================>| +``` + +### Sync-gate flags + +`check_sync_broadcast()` short-circuits on the first unset gate and logs which one it waited on. All gates use atomic bit ops so callbacks may run on any thread without ordering tricks. + +| Flag | Set by | Cleared by | +| --------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `FLAG_PA_SYNCED` | `broadcast_pa_synced` | `broadcast_pa_lost` | +| `FLAG_BROADCAST_SYNCABLE` | `syncable_cb` (BIGInfo) | `broadcast_pa_lost` (BIS idle); `broadcast_sink_reset` | +| `FLAG_BASE_RECEIVED` | `base_recv_cb` (first BASE) | `broadcast_pa_lost` (BIS idle); `broadcast_sink_reset` | +| `FLAG_BROADCAST_CODE_REQUIRED` | `syncable_cb` (`biginfo->encryption=1`) | `syncable_cb` (encryption=0); `broadcast_pa_lost` (BIS idle); `broadcast_sink_reset` | +| `FLAG_BROADCAST_CODE_RECEIVED` | `broadcast_code_cb` (BASS Set Broadcast Code); self-scan local code | `broadcast_sink_reset` | +| `FLAG_BROADCAST_SYNC_REQUESTED` | `bis_sync_req_cb` (bitmap ≠ 0); self-scan PA match | `bis_sync_req_cb` (bitmap = 0); `broadcast_sink_reset` | +| `FLAG_BROADCAST_RESYNC_PENDING` | `bis_sync_req_cb` before `_stop` (bitmap change while streaming) | `stream_stopped_cb` after driving the re-sync; `_stop` failure; `broadcast_sink_reset` | +| `FLAG_BROADCAST_SYNCING` | `check_sync_broadcast` after `_sync` returns OK | `stream_started_cb`; `stream_stopped_cb` | +| `FLAG_BROADCAST_SYNCED` | `stream_started_cb` | `stream_stopped_cb` | + +`check_sync_broadcast()` runs `_sync` only when: + +``` +BASE_RECEIVED && BROADCAST_SYNCABLE +&& (!CODE_REQUIRED || CODE_RECEIVED) +&& BROADCAST_SYNC_REQUESTED +&& PA_SYNCED +&& !(SYNCED || SYNCING) +``` + +### Sink object lifecycle + +``` + +---------+ + [start] -->| Created | + +----+----+ + | + | check_sync_broadcast -> _sync + v + +---------+ Modify Source: new +---------+ + | Syncing |<------------------------| Stopped | + +----+----+ bis_sync +----^----+ + | | + | stream_started_cb | Assistant bis_sync = 0 + v | (source pause) OR + +-----------+ | bitmap change -> _stop + | Streaming |---------------------------+ + +-----+-----+ | + | | BIG drops while PA gone + | v + +---------> stream_stopped_cb + !PA_SYNCED + | + | _delete + broadcast_sink_reset + v + [end] +``` + +Key invariants: + +- **PA loss does NOT tear down a running BIS.** Per BASS § 3.2.1.6 / § 3.2.1.9, `PA_Sync_State` and `BIS_Sync_State` are independent. While BIS is streaming/syncing, `broadcast_pa_lost` only notifies the assistant (`PA_Sync_State = 0x00`) and clears PA-only local state (`sync_handle`, `FLAG_PA_SYNCED`); the BIG keeps running and audio continues to flow. +- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming. +- **Sink deletion happens in `stream_stopped_cb` when both PA and BIS are gone.** Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone. +- `bis_sync_req_cb` going `X → 0` (Assistant pause) only issues `_stop`, never `_delete`. Going `X → Y` (BIS bitmap switch) likewise only `_stop`s; the next `check_sync_broadcast` (called from `stream_stopped_cb` when PA still synced) re-`_sync`s the same object. +- `pa_sync_term_req_cb` issues the HCI Periodic Advertising Terminate Sync but does **not** clear `broadcast_sink.sync_handle`. Cleanup runs from `BLE_GAP_EVENT_PERIODIC_SYNC_LOST` → `broadcast_pa_lost`. Resetting the handle early would make that gate miss. + +### Event handling + +| Event | Action | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | +| **PA lost while BIS active** (broadcaster moved mid-stream) | Set `PA_Sync_State = 0x00`; clear PA-only state (`sync_handle`, `FLAG_PA_SYNCED`). Sink + BIS untouched; BIG keeps running. | +| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. | +| **Modify Source `bis_sync = 0`** (PA still synced) | `bis_sync_req_cb` clears `FLAG_BROADCAST_SYNC_REQUESTED` then `_stop`s the BIG. Sink retained. | +| **Modify Source bitmap change** (PA still synced, streaming) | Update `requested_bis_sync` + `FLAG_BROADCAST_SYNC_REQUESTED` + set `FLAG_BROADCAST_RESYNC_PENDING`, then `_stop`. `stream_stopped_cb` clears the flag and re-`_sync`s with the new bitmap. | +| **BIG drops while PA still synced** (e.g. broadcaster pause) | `stream_stopped_cb` clears SYNCED/SYNCING and exposes the loss via `BIS_Sync_State`. `FLAG_BROADCAST_RESYNC_PENDING` is not set, so no auto-retry — per BASS § 3.2.1.9 the assistant drives recovery via Modify Source. | +| **BIG drops after PA lost** (broadcaster turned off) | `stream_stopped_cb` of the last active stream sees `!PA_SYNCED` → `_delete` + `broadcast_sink_reset`. Multi-BIS: earlier callbacks just decrement `active_streams` so `_delete` is not called while the sink is still in use. | +| **Assistant Remove Source** | Spec allows only when BIS not synced; lib handles, app sees no special event. | + +Two recurring patterns that drive the above behavior: + +- **Update local state before calling `_stop`/`_delete`.** The lib may fire `stream_stopped_cb` synchronously from within `_stop`, so the callback must see the post-stop state. Applies in `bis_sync_req_cb` (updates `requested_bis_sync` + flag before `_stop`) and `broadcast_pa_lost` (no longer calls `_stop`). +- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model. + +### Multi-BIS (stereo) configuration + +`CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` selects how many BIS streams the acceptor can sync to simultaneously. The `broadcast_sink.cap_streams[N]` array is sized to this value; `bis_sync_req_cb` rejects bitmaps whose popcount exceeds it (`-ENOMEM` back to BASS), and `check_sync_broadcast` builds the `streams[]` argument from the requested bitmap. `broadcast_sink.active_streams` tracks how many of those BIS are currently streaming so that `_delete` / re-sync only run after the *last* stream's `stopped_cb` — calling them while another BIS is still active hits the lib's `BapBsnkNotIdle` path. + +For Auracast stereo against a Samsung Galaxy (typical bitmap `0x3` = BIS 1 + BIS 2), set this to `2` in the sdkconfig. + ## Expected Log TAG: `CAP_ACC`. diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild index e34aa7fe7fd..a122f1a884d 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild @@ -4,32 +4,27 @@ menu "Example: CAP Acceptor" - choice EXAMPLE_CAP_ACCEPTOR_MODE - prompt "CAP Acceptor mode" - default EXAMPLE_UNICAST + config EXAMPLE_UNICAST + bool "Unicast" + default y help - Select exactly one CAP acceptor mode. + If set, advertise as a Unicast acceptor (ASCS) for unicast audio. - config EXAMPLE_UNICAST - bool "Unicast" - help - If selected, the sample will start advertising connectable - for Broadcast Assistants. + config EXAMPLE_BROADCAST + bool "Broadcast" + default y if !EXAMPLE_UNICAST + select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER + help + If set, advertise as a Broadcast acceptor (BASS Scan Delegator) + for syncable broadcast audio. Can coexist with EXAMPLE_UNICAST + for a dual-role CAP Acceptor (BAP spec C.2 allows both). - config EXAMPLE_BROADCAST - bool "Broadcast" - help - If selected, the sample will start advertising syncable - audio streams. - - endchoice - - if EXAMPLE_BROADCAST - config EXAMPLE_SCAN_SELF - bool "Scan for Broadcast Sources without Broadcast Assistant" - help - If set to true, the sample will start scanning for Broadcast - Sources without waiting for a Broadcast Assistant to connect. - endif + config EXAMPLE_SCAN_SELF + bool "Scan for Broadcast Sources without Broadcast Assistant" + # !EXAMPLE_UNICAST: dual-role + self-scan coexistence not supported yet. + depends on EXAMPLE_BROADCAST && !EXAMPLE_UNICAST + help + If set, the sample will start scanning for Broadcast Sources + without waiting for a Broadcast Assistant to connect. endmenu diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h index a2ad0940f0e..f2b63058b35 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h @@ -56,5 +56,7 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event); void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event); +void broadcast_pa_sync_failed(esp_ble_audio_gap_app_event_t *event); + void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event); #endif /* CONFIG_EXAMPLE_BROADCAST */ diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_broadcast.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_broadcast.c index 4f03d36c2ba..c6f5b7042a9 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_broadcast.c +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_broadcast.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -33,41 +34,98 @@ enum broadcast_flag { FLAG_BROADCAST_SYNCABLE, FLAG_BROADCAST_SYNCING, FLAG_BROADCAST_SYNCED, + FLAG_BROADCAST_RESYNC_PENDING, FLAG_BASE_RECEIVED, FLAG_PA_SYNCING, FLAG_PA_SYNCED, FLAG_SCANNING, - FLAG_NUM, }; -ATOMIC_DEFINE(flags, FLAG_NUM); +static uint32_t flags; + +static inline bool flag_test(uint8_t bit) +{ + return (flags & (1u << bit)) != 0; +} + +static inline void flag_set(uint8_t bit) +{ + flags |= (1u << bit); +} + +static inline void flag_clear(uint8_t bit) +{ + flags &= ~(1u << bit); +} + +static inline bool flag_test_and_set(uint8_t bit) +{ + bool was = flag_test(bit); + flag_set(bit); + return was; +} 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; uint8_t broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE]; esp_ble_audio_bap_broadcast_sink_t *sink; - esp_ble_audio_cap_stream_t cap_stream; uint8_t received_base[UINT8_MAX]; uint32_t requested_bis_sync; uint32_t broadcast_id; uint16_t sync_handle; + uint8_t active_streams; } broadcast_sink = { .sync_handle = PA_SYNC_HANDLE_INIT, }; static example_audio_rx_metrics_t rx_metrics; -static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid) +static int pa_sync_without_past(const bt_addr_le_t *addr, uint8_t adv_sid) { struct ble_gap_periodic_sync_params params = {0}; ble_addr_t sync_addr = {0}; + int err; sync_addr.type = addr->type; memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val)); params.skip = PA_SYNC_SKIP; params.sync_timeout = PA_SYNC_TIMEOUT; - return ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms, + err = ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms, + example_audio_gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to create PA sync without past, err %d", err); + } + + return err; +} + +static int pa_sync_with_past(uint16_t conn_handle, + const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) +{ + struct ble_gap_periodic_sync_params params = {0}; + int err; + + params.skip = PA_SYNC_SKIP; + params.sync_timeout = PA_SYNC_TIMEOUT; + + err = ble_gap_periodic_adv_sync_receive(conn_handle, ¶ms, example_audio_gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to enable PAST receive, err %d", err); + return err; + } + + /* Tell the Assistant we are waiting for PAST so it sends + * HCI_LE_Periodic_Advertising_Sync_Transfer. + */ + err = esp_ble_audio_bap_scan_delegator_set_pa_state(recv_state->src_id, + ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ); + if (err) { + ESP_LOGE(TAG, "Failed to set PA state to INFO_REQ, err %d", err); + } + + return err; } static int pa_sync_terminate(void) @@ -119,17 +177,17 @@ int check_start_scan(void) { int err; - if (atomic_test_bit(flags, FLAG_SCANNING)) { + if (flag_test(FLAG_SCANNING)) { ESP_LOGW(TAG, "FLAG_SCANNING"); return -EALREADY; } - if (atomic_test_bit(flags, FLAG_PA_SYNCED)) { + if (flag_test(FLAG_PA_SYNCED)) { ESP_LOGW(TAG, "FLAG_PA_SYNCED"); return -EALREADY; } - if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCED)) { + if (flag_test(FLAG_BROADCAST_SYNCED)) { ESP_LOGW(TAG, "FLAG_BROADCAST_SYNCED"); return -EALREADY; } @@ -139,362 +197,11 @@ int check_start_scan(void) return err; } - atomic_set_bit(flags, FLAG_SCANNING); - - return 0; -} -#endif /* CONFIG_EXAMPLE_SCAN_SELF */ - -static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) -{ - ESP_LOGI(TAG, "[SNK #0] Stream started"); - - example_audio_rx_metrics_reset(&rx_metrics); - - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING); - atomic_set_bit(flags, FLAG_BROADCAST_SYNCED); -} - -static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) -{ - ESP_LOGI(TAG, "[SNK #0] Stream stopped, reason 0x%02x", reason); - - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING); - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED); - -#if CONFIG_EXAMPLE_SCAN_SELF - /* Defer the scan restart to broadcast_pa_lost(): if PA is still - * synced the broadcaster may just be switching streams, and a - * premature scan would race the controller's PA-loss timeout. */ - if (atomic_test_bit(flags, FLAG_PA_SYNCED) == false) { - check_start_scan(); - } -#endif /* CONFIG_EXAMPLE_SCAN_SELF */ -} - -static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream, - const esp_ble_iso_recv_info_t *info, - const uint8_t *data, uint16_t len) -{ - rx_metrics.last_sdu_len = len; - example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "SNK #0"); -} - -static int create_broadcast_sink(void) -{ - int err; - - if (broadcast_sink.sink) { - return -EALREADY; - } - - ESP_LOGI(TAG, "Creating broadcast sink for broadcast ID 0x%06X", - broadcast_sink.broadcast_id); - - err = esp_ble_audio_bap_broadcast_sink_create(broadcast_sink.sync_handle, - broadcast_sink.broadcast_id, - &broadcast_sink.sink); - if (err) { - ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err); - return err; - } + flag_set(FLAG_SCANNING); return 0; } -static void check_sync_broadcast(void) -{ - esp_ble_audio_bap_stream_t *sync_stream = &broadcast_sink.cap_stream.bap_stream; - uint32_t sync_bitfield; - int err; - - if (atomic_test_bit(flags, FLAG_BASE_RECEIVED) == false) { - ESP_LOGI(TAG, "FLAG_BASE_RECEIVED"); - return; - } - - if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCABLE) == false) { - ESP_LOGI(TAG, "FLAG_BROADCAST_SYNCABLE"); - return; - } - - if (atomic_test_bit(flags, FLAG_BROADCAST_CODE_REQUIRED) && - atomic_test_bit(flags, FLAG_BROADCAST_CODE_RECEIVED) == false) { - ESP_LOGI(TAG, "FLAG_BROADCAST_CODE_REQUIRED"); - return; - } - - if (atomic_test_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED) == false) { - ESP_LOGI(TAG, "FLAG_BROADCAST_SYNC_REQUESTED"); - return; - } - - if (atomic_test_bit(flags, FLAG_PA_SYNCED) == false) { - ESP_LOGI(TAG, "FLAG_PA_SYNCED"); - return; - } - - if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCED) || - atomic_test_bit(flags, FLAG_BROADCAST_SYNCING)) { - ESP_LOGI(TAG, "FLAG_BROADCAST_SYNCED"); - return; - } - - if (broadcast_sink.requested_bis_sync == ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF) { - uint32_t base_bis; - - /* Get the first BIS index from the BASE */ - err = esp_ble_audio_bap_base_get_bis_indexes( - (esp_ble_audio_bap_base_t *)broadcast_sink.received_base, &base_bis); - if (err) { - ESP_LOGE(TAG, "Failed to get BIS indexes from BASE, err %d", err); - return; - } - - sync_bitfield = 0; - - for (uint8_t i = ESP_BLE_ISO_BIS_INDEX_MIN; i <= ESP_BLE_ISO_BIS_INDEX_MAX; i++) { - if (base_bis & ESP_BLE_ISO_BIS_INDEX_BIT(i)) { - sync_bitfield = ESP_BLE_ISO_BIS_INDEX_BIT(i); - break; - } - } - - if (sync_bitfield == 0) { - ESP_LOGE(TAG, "No valid BIS index found in BASE"); - return; - } - } else { - sync_bitfield = broadcast_sink.requested_bis_sync; - } - - ESP_LOGI(TAG, "Syncing to broadcast with bitfield 0x%08X", sync_bitfield); - - /* Sync the BIG */ - err = esp_ble_audio_bap_broadcast_sink_sync(broadcast_sink.sink, - sync_bitfield, &sync_stream, - broadcast_sink.broadcast_code); - if (err) { - ESP_LOGE(TAG, "Failed to sync the broadcast sink, err %d", err); - return; - } - - atomic_set_bit(flags, FLAG_BROADCAST_SYNCING); -} - -static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink, - const esp_ble_audio_bap_base_t *base, - size_t base_size) -{ - if (base_size > sizeof(broadcast_sink.received_base)) { - ESP_LOGE(TAG, "Too large BASE (%u > %u)", - base_size, sizeof(broadcast_sink.received_base)); - return; - } - - memcpy(broadcast_sink.received_base, base, base_size); - - if (!atomic_test_and_set_bit(flags, FLAG_BASE_RECEIVED)) { - ESP_LOGI(TAG, "BASE received"); - - check_sync_broadcast(); - } -} - -static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, - const esp_ble_iso_biginfo_t *biginfo) -{ - if (biginfo->encryption == false) { - atomic_clear_bit(flags, FLAG_BROADCAST_CODE_REQUIRED); - } else { - atomic_set_bit(flags, FLAG_BROADCAST_CODE_REQUIRED); - -#if CONFIG_EXAMPLE_SCAN_SELF - /* If self-scanning is enabled, local broadcast code will be used */ - memset(broadcast_sink.broadcast_code, 0, ESP_BLE_ISO_BROADCAST_CODE_SIZE); - memcpy(broadcast_sink.broadcast_code, TARGET_BROADCAST_CODE, - MIN(ESP_BLE_ISO_BROADCAST_CODE_SIZE, strlen(TARGET_BROADCAST_CODE))); - - atomic_set_bit(flags, FLAG_BROADCAST_CODE_RECEIVED); -#endif /* CONFIG_EXAMPLE_SCAN_SELF */ - } - - if (!atomic_test_and_set_bit(flags, FLAG_BROADCAST_SYNCABLE)) { - ESP_LOGI(TAG, "BIGInfo received"); - - check_sync_broadcast(); - } -} - -static int pa_sync_without_past(const bt_addr_le_t *addr, uint8_t adv_sid) -{ - int err; - - err = pa_sync_create(addr, adv_sid); - if (err) { - ESP_LOGE(TAG, "Failed to create PA sync without past, err %d", err); - } - - return err; -} - -static void recv_state_updated_cb(esp_ble_conn_t *conn, - const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) -{ - ESP_LOGI(TAG, "Receive state updated, pa_sync 0x%02x encrypt 0x%02x", - recv_state->pa_sync_state, recv_state->encrypt_state); - - for (uint8_t i = 0; i < recv_state->num_subgroups; i++) { - ESP_LOGI(TAG, "subgroup %d bis_sync: 0x%08x", i, recv_state->subgroups[i].bis_sync); - } - - if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) { - broadcast_sink.recv_state = recv_state; - } -} - -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) -{ - int err; - - ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailable): %u", - past_available ? "" : "not ", recv_state->pa_sync_state); - - broadcast_sink.recv_state = recv_state; - - 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 || - broadcast_sink.sync_handle != PA_SYNC_HANDLE_INIT) { - /* Already syncing */ - ESP_LOGW(TAG, "Rejecting PA sync request"); - return -EALREADY; - } - - if (past_available) { - ESP_LOGW(TAG, "Currently not support PAST"); - return -ENOTSUP; - } else { - err = pa_sync_without_past(&recv_state->addr, recv_state->adv_sid); - if (err) { - return err; - } - - ESP_LOGI(TAG, "Syncing without PAST"); - } - - broadcast_sink.broadcast_id = recv_state->broadcast_id; - atomic_set_bit(flags, FLAG_PA_SYNCING); - - return 0; -} - -static int pa_sync_term_req_cb(esp_ble_conn_t *conn, - const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) -{ - int err; - - ESP_LOGI(TAG, "Received request to terminate PA sync"); - - broadcast_sink.recv_state = recv_state; - - err = pa_sync_terminate(); - if (err) { - return -EIO; - } - - broadcast_sink.sync_handle = PA_SYNC_HANDLE_INIT; - - return 0; -} - -static void broadcast_code_cb(esp_ble_conn_t *conn, - const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state, - const uint8_t broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE]) -{ - ESP_LOGI(TAG, "Broadcast code received (src_id %u)", recv_state->src_id); - - broadcast_sink.recv_state = recv_state; - - memcpy(broadcast_sink.broadcast_code, broadcast_code, - ESP_BLE_ISO_BROADCAST_CODE_SIZE); - - atomic_set_bit(flags, FLAG_BROADCAST_CODE_RECEIVED); -} - -static uint32_t get_req_bis_sync(const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]) -{ - uint32_t bis_sync = 0; - - for (size_t i = 0; i < CONFIG_BT_BAP_BASS_MAX_SUBGROUPS; i++) { - bis_sync |= bis_sync_req[i]; - } - - return bis_sync; -} - -static int bis_sync_req_cb(esp_ble_conn_t *conn, - const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state, - const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]) -{ - const uint32_t new_bis_sync_req = get_req_bis_sync(bis_sync_req); - esp_err_t err; - - ESP_LOGI(TAG, "BIS sync request received (src_id %u): 0x%08lx", - recv_state->src_id, bis_sync_req[0]); - - if (new_bis_sync_req != ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF && - __builtin_popcount(new_bis_sync_req) > 1) { - ESP_LOGW(TAG, "Rejecting BIS sync request for 0x%08lx as we do not support that", - new_bis_sync_req); - return -ENOTSUP; - } - - if (broadcast_sink.requested_bis_sync == new_bis_sync_req) { - return 0; /* no op */ - } - - if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCED)) { - /* If the BIS sync request is received while we are already - * synced, it means that the requested BIS sync has changed. - */ - - /* The stream stopped callback will be called as part of this, - * and we do not need to wait for any events from the controller. - * Thus, when this returns, the broadcast sink is stopped. - */ - err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink.sink); - if (err) { - ESP_LOGE(TAG, "Failed to stop Broadcast Sink, err %d", err); - return -EIO; - } - - err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink); - if (err) { - ESP_LOGE(TAG, "Failed to delete Broadcast Sink, err %d", err); - return -EIO; - } - - broadcast_sink.sink = NULL; - - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED); - } - - broadcast_sink.requested_bis_sync = new_bis_sync_req; - - if (broadcast_sink.requested_bis_sync != 0) { - atomic_set_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED); - - check_sync_broadcast(); - } else { - atomic_clear_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED); - } - - return 0; -} - -#if CONFIG_EXAMPLE_SCAN_SELF struct scan_recv_data { bool target_matched; bool broadcast_id_found; @@ -552,7 +259,7 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) return; } - if (atomic_test_bit(flags, FLAG_PA_SYNCING) == false && + if (flag_test(FLAG_PA_SYNCING) == false && broadcast_sink.recv_state == NULL) { /* Since we are scanning ourselves, we consider this as * broadcast sync has been requested. @@ -563,20 +270,439 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) addr.type = event->ext_scan_recv.addr.type; memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val)); - err = pa_sync_create(&addr, event->ext_scan_recv.sid); + err = pa_sync_without_past(&addr, event->ext_scan_recv.sid); if (err) { - ESP_LOGE(TAG, "Failed to create PA sync, err %d", err); return; } ESP_LOGI(TAG, "Syncing without PAST from scan"); - atomic_set_bit(flags, FLAG_PA_SYNCING); - atomic_set_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED); + flag_set(FLAG_PA_SYNCING); + flag_set(FLAG_BROADCAST_SYNC_REQUESTED); } } #endif /* CONFIG_EXAMPLE_SCAN_SELF */ +static void broadcast_sink_reset(void) +{ + broadcast_sink.recv_state = NULL; + broadcast_sink.sink = NULL; + broadcast_sink.requested_bis_sync = 0; + broadcast_sink.sync_handle = PA_SYNC_HANDLE_INIT; + broadcast_sink.active_streams = 0; + + flag_clear(FLAG_BROADCAST_SYNCABLE); + flag_clear(FLAG_BROADCAST_SYNCING); + flag_clear(FLAG_BROADCAST_SYNCED); + flag_clear(FLAG_PA_SYNCED); + flag_clear(FLAG_PA_SYNCING); + flag_clear(FLAG_BASE_RECEIVED); + flag_clear(FLAG_BROADCAST_CODE_REQUIRED); + flag_clear(FLAG_BROADCAST_CODE_RECEIVED); + flag_clear(FLAG_BROADCAST_SYNC_REQUESTED); + flag_clear(FLAG_BROADCAST_RESYNC_PENDING); + flag_clear(FLAG_SCANNING); + +#if CONFIG_EXAMPLE_SCAN_SELF + check_start_scan(); +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ +} + +static void check_sync_broadcast(void) +{ + esp_ble_audio_bap_stream_t *streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; + uint32_t sync_bitfield; + uint8_t stream_count; + int err; + + if (flag_test(FLAG_BASE_RECEIVED) == false) { + ESP_LOGI(TAG, "Waiting for BASE"); + return; + } + + if (flag_test(FLAG_BROADCAST_SYNCABLE) == false) { + ESP_LOGI(TAG, "Waiting for BIGInfo"); + return; + } + + if (flag_test(FLAG_BROADCAST_CODE_REQUIRED) && + flag_test(FLAG_BROADCAST_CODE_RECEIVED) == false) { + ESP_LOGI(TAG, "Waiting for broadcast code"); + return; + } + + if (flag_test(FLAG_BROADCAST_SYNC_REQUESTED) == false) { + ESP_LOGI(TAG, "Waiting for sync request"); + return; + } + + if (flag_test(FLAG_PA_SYNCED) == false) { + ESP_LOGI(TAG, "Waiting for PA sync"); + return; + } + + if (flag_test(FLAG_BROADCAST_SYNCING)) { + ESP_LOGI(TAG, "Already syncing"); + return; + } + + if (flag_test(FLAG_BROADCAST_SYNCED)) { + ESP_LOGI(TAG, "Already synced"); + return; + } + + if (broadcast_sink.requested_bis_sync == ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF) { + uint32_t base_bis; + + /* Get the first BIS index from the BASE */ + err = esp_ble_audio_bap_base_get_bis_indexes( + (esp_ble_audio_bap_base_t *)broadcast_sink.received_base, &base_bis); + if (err) { + ESP_LOGE(TAG, "Failed to get BIS indexes from BASE, err %d", err); + return; + } + + sync_bitfield = 0; + + for (uint8_t i = ESP_BLE_ISO_BIS_INDEX_MIN; i <= ESP_BLE_ISO_BIS_INDEX_MAX; i++) { + if (base_bis & ESP_BLE_ISO_BIS_INDEX_BIT(i)) { + sync_bitfield = ESP_BLE_ISO_BIS_INDEX_BIT(i); + break; + } + } + + if (sync_bitfield == 0) { + ESP_LOGE(TAG, "No valid BIS index found in BASE"); + return; + } + } else { + sync_bitfield = broadcast_sink.requested_bis_sync; + } + + stream_count = __builtin_popcount(sync_bitfield); + if (stream_count > CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT) { + ESP_LOGE(TAG, "Bitfield 0x%08X needs %u streams, have %u", + sync_bitfield, stream_count, CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT); + return; + } + + for (uint8_t i = 0; i < stream_count; i++) { + streams[i] = &broadcast_sink.cap_streams[i].bap_stream; + } + + ESP_LOGI(TAG, "Syncing to broadcast %p with bitfield 0x%08X (%u streams)", + broadcast_sink.sink, sync_bitfield, stream_count); + + /* Sync the BIG */ + err = esp_ble_audio_bap_broadcast_sink_sync(broadcast_sink.sink, + sync_bitfield, streams, + broadcast_sink.broadcast_code); + if (err) { + ESP_LOGE(TAG, "Failed to sync the broadcast sink, err %d", err); + return; + } + + flag_set(FLAG_BROADCAST_SYNCING); +} + +static uint8_t broadcast_stream_idx(const esp_ble_audio_bap_stream_t *stream) +{ + for (uint8_t i = 0; i < CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT; i++) { + if (&broadcast_sink.cap_streams[i].bap_stream == stream) { + return i; + } + } + return 0xFF; +} + +static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) +{ + ESP_LOGI(TAG, "[SNK #%u] Stream started", broadcast_stream_idx(stream)); + + example_audio_rx_metrics_reset(&rx_metrics); + + broadcast_sink.active_streams++; + flag_clear(FLAG_BROADCAST_SYNCING); + flag_set(FLAG_BROADCAST_SYNCED); +} + +static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) +{ + esp_err_t err; + + ESP_LOGI(TAG, "[SNK #%u] Stream stopped, reason 0x%02x", + broadcast_stream_idx(stream), reason); + + if (broadcast_sink.active_streams > 0) { + broadcast_sink.active_streams--; + } + + /* Multi-BIS: defer teardown / re-sync until every stream of this sink + * has stopped. Calling _delete or _sync while another BIS is still + * active hits the lib's "sink not idle" path. + */ + if (broadcast_sink.active_streams > 0) { + return; + } + + flag_clear(FLAG_BROADCAST_SYNCING); + flag_clear(FLAG_BROADCAST_SYNCED); + + if (flag_test(FLAG_PA_SYNCED) == false) { + /* Both PA and BIS are gone — no path to recover BIGInfo, so the + * sink can no longer drive a new BIG sync. Delete it and clear all + * state. + */ + if (broadcast_sink.sink) { + err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink); + if (err) { + ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); + return; + } + } + + broadcast_sink_reset(); + return; + } + + /* PA still synced. Drive the re-sync only when WE initiated the stop + * for a bitmap change (bis_sync_req_cb sets FLAG_BROADCAST_RESYNC_PENDING). On + * spontaneous BIG drops per BASS § 3.2.1.9 we just expose the loss via + * BIS_Sync_State and wait for the assistant to act (Modify Source). + */ + if (flag_test(FLAG_BROADCAST_RESYNC_PENDING)) { + flag_clear(FLAG_BROADCAST_RESYNC_PENDING); + check_sync_broadcast(); + } +} + +static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream, + const esp_ble_iso_recv_info_t *info, + const uint8_t *data, uint16_t len) +{ + char obj_name[10]; + + rx_metrics.last_sdu_len = len; + snprintf(obj_name, sizeof(obj_name), "SNK #%u", broadcast_stream_idx(stream)); + example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, obj_name); +} + +static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink, + const esp_ble_audio_bap_base_t *base, + size_t base_size) +{ + if (base_size > sizeof(broadcast_sink.received_base)) { + ESP_LOGE(TAG, "Too large BASE (%u > %u)", + base_size, sizeof(broadcast_sink.received_base)); + return; + } + + memcpy(broadcast_sink.received_base, base, base_size); + + if (!flag_test_and_set(FLAG_BASE_RECEIVED)) { + ESP_LOGI(TAG, "BASE received"); + + check_sync_broadcast(); + } +} + +static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, + const esp_ble_iso_biginfo_t *biginfo) +{ + if (biginfo->encryption == false) { + flag_clear(FLAG_BROADCAST_CODE_REQUIRED); + } else { + flag_set(FLAG_BROADCAST_CODE_REQUIRED); + +#if CONFIG_EXAMPLE_SCAN_SELF + /* If self-scanning is enabled, local broadcast code will be used */ + memset(broadcast_sink.broadcast_code, 0, ESP_BLE_ISO_BROADCAST_CODE_SIZE); + memcpy(broadcast_sink.broadcast_code, TARGET_BROADCAST_CODE, + MIN(ESP_BLE_ISO_BROADCAST_CODE_SIZE, strlen(TARGET_BROADCAST_CODE))); + + flag_set(FLAG_BROADCAST_CODE_RECEIVED); +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ + } + + if (!flag_test_and_set(FLAG_BROADCAST_SYNCABLE)) { + ESP_LOGI(TAG, "BIGInfo received"); + + check_sync_broadcast(); + } +} + +static void recv_state_updated_cb(esp_ble_conn_t *conn, + const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) +{ + ESP_LOGI(TAG, "Receive state updated, pa_sync 0x%02x encrypt 0x%02x", + recv_state->pa_sync_state, recv_state->encrypt_state); + + for (uint8_t i = 0; i < recv_state->num_subgroups; i++) { + ESP_LOGI(TAG, "subgroup %d bis_sync: 0x%08x", i, recv_state->subgroups[i].bis_sync); + } + + if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) { + broadcast_sink.recv_state = recv_state; + } +} + +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) +{ + int err; + + ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailable): %u", + past_available ? "" : "not ", recv_state->pa_sync_state); + + broadcast_sink.recv_state = recv_state; + + 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 || + broadcast_sink.sync_handle != PA_SYNC_HANDLE_INIT) { + /* Already syncing */ + ESP_LOGW(TAG, "Rejecting PA sync request"); + return -EALREADY; + } + + if (past_available) { + err = pa_sync_with_past(conn->handle, recv_state); + if (err) { + return -EIO; + } + + ESP_LOGI(TAG, "Waiting for PAST..."); + } else { + err = pa_sync_without_past(&recv_state->addr, recv_state->adv_sid); + if (err) { + return err; + } + + ESP_LOGI(TAG, "Syncing without PAST"); + } + + broadcast_sink.broadcast_id = recv_state->broadcast_id; + flag_set(FLAG_PA_SYNCING); + + return 0; +} + +static int pa_sync_term_req_cb(esp_ble_conn_t *conn, + const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) +{ + int err; + + ESP_LOGI(TAG, "Received request to terminate PA sync"); + + broadcast_sink.recv_state = recv_state; + + err = pa_sync_terminate(); + if (err) { + return -EIO; + } + + /* Let BLE_GAP_EVENT_PERIODIC_SYNC_LOST drive cleanup via broadcast_pa_lost + * (it gates on the original sync_handle). Resetting sync_handle here would + * make that gate miss and leak broadcast_sink.sink. + */ + return 0; +} + +static void broadcast_code_cb(esp_ble_conn_t *conn, + const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state, + const uint8_t broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE]) +{ + ESP_LOGI(TAG, "Broadcast code received (src_id %u)", recv_state->src_id); + + broadcast_sink.recv_state = recv_state; + + memcpy(broadcast_sink.broadcast_code, broadcast_code, + ESP_BLE_ISO_BROADCAST_CODE_SIZE); + + flag_set(FLAG_BROADCAST_CODE_RECEIVED); + + /* If BIGInfo arrived before the code, nothing else will retry the sync. */ + check_sync_broadcast(); +} + +static uint32_t get_req_bis_sync(const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]) +{ + uint32_t bis_sync = 0; + + for (size_t i = 0; i < CONFIG_BT_BAP_BASS_MAX_SUBGROUPS; i++) { + bis_sync |= bis_sync_req[i]; + } + + return bis_sync; +} + +static int bis_sync_req_cb(esp_ble_conn_t *conn, + const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state, + const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]) +{ + const uint32_t new_bis_sync_req = get_req_bis_sync(bis_sync_req); + esp_err_t err; + + ESP_LOGI(TAG, "BIS sync request received (src_id %u): 0x%08lx", + recv_state->src_id, bis_sync_req[0]); + + if (new_bis_sync_req != ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF && + __builtin_popcount(new_bis_sync_req) > CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT) { + ESP_LOGW(TAG, "Rejecting BIS sync 0x%08lx: needs %u streams, max %u", + new_bis_sync_req, __builtin_popcount(new_bis_sync_req), + CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT); + return -ENOMEM; + } + + if (broadcast_sink.requested_bis_sync == new_bis_sync_req) { + ESP_LOGI(TAG, "BIS sync request unchanged (0x%08lx), no-op", new_bis_sync_req); + return 0; + } + + /* Update requested state BEFORE calling _stop. The lib can fire + * stream_stopped_cb synchronously from within _stop, and that + * callback drives the deferred re-sync via check_sync_broadcast. + * Updating first ensures the callback reads the new bitfield rather + * than the stale one. + */ + ESP_LOGI(TAG, "BIS sync request 0x%08lx -> 0x%08lx", + broadcast_sink.requested_bis_sync, new_bis_sync_req); + + broadcast_sink.requested_bis_sync = new_bis_sync_req; + + if (broadcast_sink.requested_bis_sync != 0) { + flag_set(FLAG_BROADCAST_SYNC_REQUESTED); + } else { + flag_clear(FLAG_BROADCAST_SYNC_REQUESTED); + } + + if (flag_test(FLAG_BROADCAST_SYNCED)) { + /* BIS sync request changed while streaming. Stop the BIG sync but + * keep the sink object: BIGINFO_RECEIVED / qos / BASE are still + * valid for the next sync on the same PA + broadcast_id. + * + * Leave FLAG_BROADCAST_SYNCED set; stream_stopped_cb clears it. + * If the new bitmap is non-zero, mark FLAG_BROADCAST_RESYNC_PENDING so that + * stream_stopped_cb drives a re-sync once BIG teardown completes. + * Otherwise (bitmap = 0, assistant unsubscribed) there is nothing + * to re-sync to. + */ + if (broadcast_sink.requested_bis_sync != 0) { + flag_set(FLAG_BROADCAST_RESYNC_PENDING); + } + err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink.sink); + if (err) { + flag_clear(FLAG_BROADCAST_RESYNC_PENDING); + ESP_LOGE(TAG, "Failed to stop Broadcast Sink, err %d", err); + return -EIO; + } + } else if (flag_test(FLAG_BROADCAST_SYNC_REQUESTED)) { + check_sync_broadcast(); + } + + return 0; +} + void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event) { bt_addr_le_t addr = {0}; @@ -596,8 +722,8 @@ void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event) broadcast_sink.sync_handle = event->pa_sync.sync_handle; } - atomic_set_bit(flags, FLAG_PA_SYNCED); - atomic_clear_bit(flags, FLAG_PA_SYNCING); + flag_set(FLAG_PA_SYNCED); + flag_clear(FLAG_PA_SYNCING); #if CONFIG_EXAMPLE_SCAN_SELF err = ext_scan_stop(); @@ -605,20 +731,58 @@ void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event) ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); /* Continue anyway - scan stop failure should not block sink creation */ } else { - atomic_clear_bit(flags, FLAG_SCANNING); + flag_clear(FLAG_SCANNING); } #endif /* CONFIG_EXAMPLE_SCAN_SELF */ - err = create_broadcast_sink(); - if (err && err != -EALREADY) { - ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err); - return; + if (broadcast_sink.sink == NULL) { + ESP_LOGI(TAG, "Creating broadcast sink for broadcast ID 0x%06X", + broadcast_sink.broadcast_id); + + err = esp_ble_audio_bap_broadcast_sink_create(broadcast_sink.sync_handle, + broadcast_sink.broadcast_id, + &broadcast_sink.sink); + if (err) { + ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err); + return; + } } check_sync_broadcast(); } } +void broadcast_pa_sync_failed(esp_ble_audio_gap_app_event_t *event) +{ + esp_ble_audio_bap_pa_state_t pa_state; + esp_err_t err; + + (void)event; + + flag_clear(FLAG_PA_SYNCING); + + if (broadcast_sink.recv_state == NULL) { + ESP_LOGI(TAG, "PA sync failed with no recv_state to report to"); + return; + } + + /* BASS § 3.2.1.7: report failure to the Assistant. Use NO_PAST when + * we were waiting for an Assistant-driven PAST (recv state had + * pa_sync_state = INFO_REQ), FAILED otherwise. + */ + if (broadcast_sink.recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ) { + pa_state = ESP_BLE_AUDIO_BAP_PA_STATE_NO_PAST; + } else { + pa_state = ESP_BLE_AUDIO_BAP_PA_STATE_FAILED; + } + + err = esp_ble_audio_bap_scan_delegator_set_pa_state(broadcast_sink.recv_state->src_id, + pa_state); + if (err) { + ESP_LOGE(TAG, "Failed to set PA state on sync failure, err %d", err); + } +} + void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) { esp_err_t err; @@ -627,44 +791,51 @@ void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) return; } - if (broadcast_sink.recv_state) { + /* Per BASS § 3.2.1.6 / § 3.2.1.9: PA_Sync_State and BIS_Sync_State are + * independent. PA loss only requires updating PA_Sync_State; the BIG + * keeps running as long as the broadcaster still transmits it. + * + * Skip set_pa_state when an Assistant-driven Modify Source already moved + * the state to NOT_SYNCED (BASS updates recv_state in place before + * PERIODIC_SYNC_LOST fires) — calling it again hits the lib's + * "no state by SID" error path. + */ + if (broadcast_sink.recv_state && + broadcast_sink.recv_state->pa_sync_state != ESP_BLE_AUDIO_BAP_PA_STATE_NOT_SYNCED) { err = esp_ble_audio_bap_scan_delegator_set_pa_state(broadcast_sink.recv_state->src_id, ESP_BLE_AUDIO_BAP_PA_STATE_NOT_SYNCED); if (err) { ESP_LOGE(TAG, "Failed to set PA state to ESP_BLE_AUDIO_BAP_PA_STATE_NOT_SYNCED, err %d", err); } - - err = esp_ble_audio_bap_scan_delegator_rem_src(broadcast_sink.recv_state->src_id); - if (err) { - ESP_LOGE(TAG, "Failed to remove receive state source, err %d", err); - } - - if (broadcast_sink.sink) { - err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink); - if (err) { - ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); - } - } } - broadcast_sink.recv_state = NULL; - broadcast_sink.sink = NULL; - broadcast_sink.requested_bis_sync = 0; broadcast_sink.sync_handle = PA_SYNC_HANDLE_INIT; + flag_clear(FLAG_PA_SYNCED); + flag_clear(FLAG_PA_SYNCING); - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCABLE); - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING); - atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED); - atomic_clear_bit(flags, FLAG_PA_SYNCED); - atomic_clear_bit(flags, FLAG_PA_SYNCING); - atomic_clear_bit(flags, FLAG_BASE_RECEIVED); - atomic_clear_bit(flags, FLAG_BROADCAST_CODE_REQUIRED); - atomic_clear_bit(flags, FLAG_BROADCAST_CODE_RECEIVED); - atomic_clear_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED); + /* BIS still active → BIG keeps running per § 3.2.1.9, leave the sink; + * stream_stopped_cb will tear it down when BIS eventually stops. + * Otherwise the sink is bound to a dead PA handle and its cached + * BASE / BIGInfo are stale: delete the sink and clear the PA-derived + * flags so the next PA sync creates a fresh sink and lets the lib + * redeliver BASE / BIGInfo. + */ + if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED)) { + return; + } -#if CONFIG_EXAMPLE_SCAN_SELF - check_start_scan(); -#endif /* CONFIG_EXAMPLE_SCAN_SELF */ + if (broadcast_sink.sink) { + err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink); + if (err) { + ESP_LOGW(TAG, "Deferred sink delete pending after PA loss, err %d", err); + return; + } + broadcast_sink.sink = NULL; + } + + flag_clear(FLAG_BASE_RECEIVED); + flag_clear(FLAG_BROADCAST_SYNCABLE); + flag_clear(FLAG_BROADCAST_CODE_REQUIRED); } int cap_acceptor_broadcast_init(void) @@ -702,8 +873,14 @@ int cap_acceptor_broadcast_init(void) return err; } - esp_ble_audio_cap_stream_ops_register(&broadcast_sink.cap_stream, - &broadcast_stream_ops); + for (uint8_t i = 0; i < CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT; i++) { + err = esp_ble_audio_cap_stream_ops_register(&broadcast_sink.cap_streams[i], + &broadcast_stream_ops); + if (err) { + ESP_LOGE(TAG, "Failed to register broadcast stream ops [%u], err %d", i, err); + return err; + } + } cbs_registered = true; } diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c index 6ba3f290fe0..b611e2045e1 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c @@ -56,11 +56,24 @@ static struct peer_config peer = { 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_ASCS_VAL & 0xFF), - ((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF), - (ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF), - ((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF), + /* Incomplete List of 16-bit Service UUIDs: + * CAS always; ASCS only when unicast role is built; + * BASS only when broadcast role is built. + */ +#if CONFIG_EXAMPLE_UNICAST && CONFIG_EXAMPLE_BROADCAST + 0x07, EXAMPLE_AD_TYPE_UUID16_SOME, + (ESP_BLE_AUDIO_UUID_ASCS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF), + (ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF), + (ESP_BLE_AUDIO_UUID_BASS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_BASS_VAL >> 8) & 0xFF), +#elif CONFIG_EXAMPLE_UNICAST + 0x05, EXAMPLE_AD_TYPE_UUID16_SOME, + (ESP_BLE_AUDIO_UUID_ASCS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF), + (ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF), +#elif CONFIG_EXAMPLE_BROADCAST + 0x05, EXAMPLE_AD_TYPE_UUID16_SOME, + (ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF), + (ESP_BLE_AUDIO_UUID_BASS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_BASS_VAL >> 8) & 0xFF), +#endif /* Service Data - 16-bit UUID */ 0x04, EXAMPLE_AD_TYPE_SERVICE_DATA16, (ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF), ((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF), @@ -240,6 +253,7 @@ 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); + broadcast_pa_sync_failed(event); return; } @@ -291,6 +305,7 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) break; #endif /* CONFIG_EXAMPLE_SCAN_SELF */ case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC: + case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_PAST: pa_sync(event); break; case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_LOST: diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_unicast.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_unicast.c index eaa72ed5e65..64afcd7fe69 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_unicast.c +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_unicast.c @@ -603,7 +603,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event) int err; if (event->security_change.status) { - ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); + example_audio_security_failed_recover(TAG, event->security_change.conn_handle, + event->security_change.status); return; } diff --git a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c index 84fc1728e5e..8a01ac4f0c3 100644 --- a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c +++ b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c @@ -32,6 +32,8 @@ int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg) event->type == BLE_GAP_EVENT_PERIODIC_SYNC || event->type == BLE_GAP_EVENT_PERIODIC_REPORT || event->type == BLE_GAP_EVENT_PERIODIC_SYNC_LOST || + event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER || + event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 || event->type == BLE_GAP_EVENT_CONNECT || event->type == BLE_GAP_EVENT_DISCONNECT || event->type == BLE_GAP_EVENT_ENC_CHANGE) { @@ -41,11 +43,49 @@ int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg) event->type == BLE_GAP_EVENT_NOTIFY_TX || event->type == BLE_GAP_EVENT_SUBSCRIBE) { esp_ble_audio_gatt_app_post_event(event->type, event); + } else if (event->type == BLE_GAP_EVENT_REPEAT_PAIRING) { + /* Peer wants to re-pair on top of an existing bond (e.g. phone + * cleared its LTK via "Forget device" but our NVS still has it). + * Delete our stale bond and tell NimBLE to retry pairing. + */ + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; } return 0; } +void example_audio_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status) +{ + struct ble_gap_conn_desc desc = {0}; + int rc; + + /* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical + * cause is asymmetric bond state — the two sides bonded previously, + * then one side erased its NVS while the other kept the LTK. The side + * that still has the bond tries encryption with the cached key; the + * other side has nothing to match it against, so SMP times out. Drop + * our entry for this peer and disconnect; the next connection runs + * fresh pairing (peer side recovers via REPEAT_PAIRING). + */ + ESP_LOGE(tag, "Security change failed, status %u, clearing local bond and reconnecting", status); + + rc = ble_gap_conn_find(conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + + /* Tear down the link: SMP cannot be retried on the same connection + * after a failed/timed-out exchange. The next reconnect starts a + * fresh SMP procedure with the now-cleared bond state. + */ + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); +} + static void print_hex(const uint8_t *ptr, size_t len) { while (len--) { diff --git a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h index 04a457f5219..c23d32fd7d0 100644 --- a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h +++ b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h @@ -21,6 +21,7 @@ #include "host/ble_gap.h" #include "host/ble_hs_adv.h" +#include "host/ble_store.h" #include "esp_ble_audio_bap_api.h" @@ -53,6 +54,8 @@ int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg); +void example_audio_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status); + void example_print_codec_cfg(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg); void example_print_codec_cap(const char *tag, const esp_ble_audio_codec_cap_t *codec_cap); diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c index 54a7c9d9f86..4d1233b5d57 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c @@ -253,7 +253,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event) int err; if (event->security_change.status) { - ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); + example_audio_security_failed_recover(TAG, event->security_change.conn_handle, + event->security_change.status); return; } diff --git a/examples/bluetooth/esp_ble_iso/cis_central/main/main.c b/examples/bluetooth/esp_ble_iso/cis_central/main/main.c index 9a24ae075b1..23a0f2e422e 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/main.c @@ -352,7 +352,8 @@ static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) static void security_change(esp_ble_iso_gap_app_event_t *event) { if (event->security_change.status) { - ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); + example_iso_security_failed_recover(TAG, event->security_change.conn_handle, + event->security_change.status); return; } diff --git a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c index 831d5410dd6..5807a0a01f8 100644 --- a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c +++ b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c @@ -28,15 +28,55 @@ int example_iso_gap_event_cb(struct ble_gap_event *event, void *arg) event->type == BLE_GAP_EVENT_PERIODIC_SYNC || event->type == BLE_GAP_EVENT_PERIODIC_REPORT || event->type == BLE_GAP_EVENT_PERIODIC_SYNC_LOST || + event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER || + event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 || event->type == BLE_GAP_EVENT_CONNECT || event->type == BLE_GAP_EVENT_DISCONNECT || event->type == BLE_GAP_EVENT_ENC_CHANGE) { esp_ble_iso_gap_app_post_event(event->type, event); + } else if (event->type == BLE_GAP_EVENT_REPEAT_PAIRING) { + /* Peer wants to re-pair on top of an existing bond (e.g. phone + * cleared its LTK via "Forget device" but our NVS still has it). + * Delete our stale bond and tell NimBLE to retry pairing. + */ + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; } return 0; } +void example_iso_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status) +{ + struct ble_gap_conn_desc desc = {0}; + int rc; + + /* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical + * cause is asymmetric bond state — the two sides bonded previously, + * then one side erased its NVS while the other kept the LTK. The side + * that still has the bond tries encryption with the cached key; the + * other side has nothing to match it against, so SMP times out. Drop + * our entry for this peer and disconnect; the next connection runs + * fresh pairing (peer side recovers via REPEAT_PAIRING). + */ + ESP_LOGE(tag, "Security change failed, status %u, clearing local bond and reconnecting", status); + + rc = ble_gap_conn_find(conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + + /* Tear down the link: SMP cannot be retried on the same connection + * after a failed/timed-out exchange. The next reconnect starts a + * fresh SMP procedure with the now-cleared bond state. + */ + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); +} + static void example_iso_tx_work_handler(struct k_work *work) { example_iso_tx_scheduler_t *scheduler = work->user_data; diff --git a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h index 9a329483df4..dc7fc7169f8 100644 --- a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h +++ b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h @@ -20,6 +20,7 @@ #include "host/ble_gap.h" #include "host/ble_hs_adv.h" +#include "host/ble_store.h" #include "esp_ble_iso_common_api.h" @@ -52,6 +53,8 @@ int example_iso_gap_event_cb(struct ble_gap_event *event, void *arg); +void example_iso_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status); + /** * @brief TX scheduler for periodic ISO data transmission. *